|
| 1 | +# filesystem-ts |
| 2 | + |
| 3 | +Standalone TypeScript server implementing the Model Context Protocol (MCP) for filesystem operations with robust security, encoding support, and cross-platform path handling. |
| 4 | + |
| 5 | +## Provenance |
| 6 | + |
| 7 | +Originally derived from [`modelcontextprotocol/servers/src/filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem). This fork is maintained independently and is not affiliated with or published by the upstream MCP servers project. |
| 8 | + |
| 9 | +## Features |
| 10 | + |
| 11 | +- **Read/write/append files** with encoding support, BOM detection, binary content warnings, parent directory creation, and safe overwrite via atomic writes |
| 12 | +- **Line-aware reading and editing** with ranges, head/tail, around-line context, match extraction, numbered output, line-range replacement, and insertion |
| 13 | +- **Edit files** with exact match, whitespace-trimmed match (indentation-preserving), fuzzy matching with actionable feedback, and optional hash guards |
| 14 | +- **Read media files** as MCP image/audio content with automatic MIME type detection for supported raster/audio formats |
| 15 | +- **Create/list directories** recursively, with metadata, sorting, depth limits, hidden-file filtering, structured entries, and summary statistics |
| 16 | +- **Directory trees** as JSON with glob-based exclusion patterns and depth limiting |
| 17 | +- **Rename files/directories** within the same allowed directory root with destination-exists protection |
| 18 | +- **Search files and contents** by substring, regex, or glob with case sensitivity, max results, path/content targeting, context lines, and truncation warnings |
| 19 | +- **Get file metadata** including batch metadata, symlink detection, MIME type, and line count |
| 20 | +- **Assistant-friendly structured output** on core tools, while preserving text-first defaults for existing clients |
| 21 | +- **MCP Roots protocol** support for dynamic allowed root updates (`file://` directories and files) |
| 22 | +- **Cross-platform** path normalization (macOS, Linux, Windows, UNC, WSL) |
| 23 | + |
| 24 | +> **Security**: The server only allows operations within configured filesystem roots. Directory roots allow their descendants, file roots allow that exact file, symlink targets are validated before content reads and metadata reads, Unix device/system paths are blocked, null bytes are rejected, and atomic writes prevent data corruption when a directory root permits safe temporary files. |
| 25 | +
|
| 26 | +## Crash Cleanup and Memory Handling |
| 27 | + |
| 28 | +- The server does not keep a persistent cache of file contents. Allowed roots are kept in process memory; file data read from disk is held only for the active tool call and is then released to normal JavaScript garbage collection. |
| 29 | +- Full-file reads, `read_multiple_files`, edits, diffs, and media responses may hold complete file contents in memory for that request. `head`, `tail`, file line counting, and directory/content search use chunking or limits where practical; content search skips files above `max_file_bytes`. |
| 30 | +- Overwrites use same-directory temporary files followed by rename when the target is inside an allowed directory root. Temporary files are tracked while in flight, removed on write/rename failure, and cleaned up best-effort on `SIGHUP`, `SIGINT`, `SIGTERM`, Windows `SIGBREAK`, uncaught exceptions, and unhandled rejections. |
| 31 | +- Hard termination such as `SIGKILL`, process abort, OS crash, or power loss cannot run cleanup handlers. In those cases an in-flight atomic write may leave a sibling file matching `target.<random>.tmp`; the original target should remain intact unless the filesystem itself failed during rename. |
| 32 | +- The public tools do not expose file deletion. Internal cleanup only unlinks temp files that the server created and tracked for an active atomic write. `rename_file` uses rename only; cross-root renames and cross-device copy/delete emulation are refused. |
| 33 | +- `create_backup: true` backups are intentionally persistent and are not cleaned up automatically. |
| 34 | + |
| 35 | +## Architecture |
| 36 | + |
| 37 | +The server is modularized into four source files: |
| 38 | + |
| 39 | +| Module | Purpose | |
| 40 | +| --- | --- | |
| 41 | +| `constants.ts` | Error codes, supported encodings, thresholds | |
| 42 | +| `path-utils.ts` | Path normalization, comparison, home expansion | |
| 43 | +| `lib.ts` | State management, validation, file operations, helpers | |
| 44 | +| `index.ts` | Server setup, schemas, tool registrations, MCP Roots, startup | |
| 45 | + |
| 46 | +## Error Codes |
| 47 | + |
| 48 | +When a tool call results in an error (`isError: true`), the `result` object contains an `errorCode` field: |
| 49 | + |
| 50 | +- **`INVALID_ARGS`** — Input arguments failed validation or contained logical inconsistencies |
| 51 | +- **`INVALID_ENCODING`** — Unsupported encoding specified |
| 52 | +- **`ACCESS_DENIED`** — Path outside allowed roots or symlink restriction |
| 53 | +- **`PERMISSION_DENIED`** — Insufficient filesystem permissions |
| 54 | +- **`PATH_NOT_FOUND`** — Specified path or parent does not exist |
| 55 | +- **`DESTINATION_EXISTS`** — Target path already exists (for `rename_file` or `write_file` without `overwrite: true`) |
| 56 | +- **`EDIT_MATCH_UNCERTAIN`** — Fuzzy match found (>=80% similarity) but no exact/trimmed match; includes context snippet |
| 57 | +- **`EDIT_MATCH_NOT_FOUND`** — No match found at all; includes file line count and searched text |
| 58 | +- **`ENCODING_ERROR`** — File content could not be decoded with the requested encoding |
| 59 | +- **`FILESYSTEM_ERROR`** — Generic underlying filesystem error |
| 60 | + |
| 61 | +## Tools |
| 62 | + |
| 63 | +Most text-returning tools include `structuredContent.content` with the same text shown in the MCP content block. Newer assistant-facing options add richer structured fields such as `entries`, `files`, `matches`, `lines`, and truncation metadata. The optional `output_format` flag accepts: |
| 64 | + |
| 65 | +- `text` (default): keep the normal human-readable text response |
| 66 | +- `structured`: render the structured object as JSON text |
| 67 | +- `both`: keep normal text and return structured fields |
| 68 | + |
| 69 | +The `structuredContent` object is returned regardless of `output_format`; the flag only changes the rendered text block. |
| 70 | + |
| 71 | +### read_file |
| 72 | + |
| 73 | +Read file contents. Supports full file, line range, head (first N lines), tail (last N lines), around-line context, and match extraction. |
| 74 | + |
| 75 | +- **Inputs:** `path`, `mode?`, `start_line?`, `end_line?`, `head?`, `tail?`, `line?`, `before?`, `after?`, `pattern?`, `search_mode?`, `case_sensitive?`, `max_matches?`, `max_bytes?`, `max_lines?`, `show_line_numbers?`, `output_format?`, `encoding?` |
| 76 | +- **Constraints:** `head`/`tail` are mutually exclusive with `start_line`/`end_line` and with each other |
| 77 | +- **Mode inference:** if `mode` is omitted, the server infers `head`, `tail`, `range`, or `full` from the legacy arguments |
| 78 | +- **Around-line read:** `mode: "around"`, `line`, optional `before`/`after` |
| 79 | +- **Match read:** `mode: "matches"`, `pattern`, `search_mode` (`substring`/`regex`), optional context controls |
| 80 | +- **Structured fields:** `lines` for line-based reads, `matches` for match reads, `lineCount`, `startLine`, `endLine` |
| 81 | +- **Result:** may include `warningCode: 'END_OUT_OF_BOUNDS'`, `CONTENT_TRUNCATED`, `bomDetected`, or `'BINARY_CONTENT_DETECTED'` |
| 82 | + |
| 83 | +### read_text_file |
| 84 | + |
| 85 | +Read a text file with optional head/tail line limiting. Simpler interface than `read_file`. |
| 86 | + |
| 87 | +- **Inputs:** `path`, `head?`, `tail?` |
| 88 | + |
| 89 | +### read_media_file |
| 90 | + |
| 91 | +Read a supported raster image or audio file as MCP image/audio content with automatic MIME type detection. |
| 92 | + |
| 93 | +- **Inputs:** `path` |
| 94 | +- **Returns:** MCP content with `type` (`image` or `audio`), `data` (base64), and `mimeType` |
| 95 | +- **Unknown or active-content file types:** returns a text notice plus structured `{ mimeType, size }`; arbitrary binary data is not read into the response or exposed through this tool |
| 96 | + |
| 97 | +### read_multiple_files |
| 98 | + |
| 99 | +Read multiple files simultaneously. Individual errors are reported inline without stopping the batch. |
| 100 | + |
| 101 | +- **Inputs:** `paths`, `encoding?`, `output_format?` |
| 102 | +- **Structured fields:** `files` array with `{ path, success, content? }` or `{ path, success: false, errorCode, error }` |
| 103 | + |
| 104 | +### write_file |
| 105 | + |
| 106 | +Create, append, or overwrite files with atomic writes and `wx` flag safety. |
| 107 | + |
| 108 | +- **Inputs:** `path`, `content`, `encoding?`, `append?`, `create_parents?`, `overwrite?`, `dryRun?`, `expected_hash?`, `expected_modified?`, `create_backup?`, `return_diff?`, `output_format?` |
| 109 | +- **Error:** `DESTINATION_EXISTS` if file exists and `overwrite` is not `true` (message includes file size and modification time) |
| 110 | +- **Dry run:** validates and returns the proposed diff without writing |
| 111 | +- **Hash/time guards:** reject the write if the current SHA-256 hash or modified timestamp does not match |
| 112 | +- **Backups:** `create_backup: true` creates a sibling backup before changing an existing file; exact-file roots cannot create backup siblings unless a directory root also allows them |
| 113 | + |
| 114 | +### edit_file |
| 115 | + |
| 116 | +Make edits with exact text matching, line-range replacement, or line insertion. |
| 117 | + |
| 118 | +- **Inputs:** `path`, `edits`, `encoding?`, `dryRun?`, `expected_hash?`, `show_line_numbers_on_error?`, `output_format?` |
| 119 | +- **Text edit form:** `{ oldText, newText }` tries exact match, whitespace-trimmed match, then fuzzy match |
| 120 | +- **Line replacement form:** `{ start_line, end_line, newText }` replaces the inclusive 1-based line range |
| 121 | +- **Insertion form:** `{ insert_at_line, newText }` inserts before the given 1-based line |
| 122 | +- **Returns:** Git-style diff on success |
| 123 | +- **Errors:** `EDIT_MATCH_UNCERTAIN` (with context), `EDIT_MATCH_NOT_FOUND` (with line count) |
| 124 | + |
| 125 | +### create_directory |
| 126 | + |
| 127 | +Create a directory, including parents. Succeeds silently if exists. |
| 128 | + |
| 129 | +- **Inputs:** `path` |
| 130 | + |
| 131 | +### list_directory |
| 132 | + |
| 133 | +List directory contents with optional recursion, metadata, sorting, and summary. |
| 134 | + |
| 135 | +- **Inputs:** `path`, `recursive?`, `include_metadata?`, `include_hidden?`, `include_errors?`, `maxDepth?`, `fields?`, `sortBy?` (name/size/modified), `order?` (asc/desc), `output_format?` |
| 136 | +- **Defaults:** hidden files are included by default to preserve existing behavior; set `include_hidden: false` to hide dotfiles |
| 137 | +- **Depth:** with recursive listing, `maxDepth: 1` lists direct children only |
| 138 | +- **Structured fields:** `entries` array with path, relative path, type, depth, optional size/modified, and error details |
| 139 | +- **Summary line** (when `include_metadata` is true): total files, directories, and size |
| 140 | + |
| 141 | +### list_directory_with_sizes |
| 142 | + |
| 143 | +List directory contents with file sizes and modification times. |
| 144 | + |
| 145 | +- **Inputs:** `path`, `sortBy?` (name/size) |
| 146 | + |
| 147 | +### directory_tree |
| 148 | + |
| 149 | +Get a recursive directory tree as JSON with exclusion patterns and depth limiting. |
| 150 | + |
| 151 | +- **Inputs:** `path`, `excludePatterns?`, `maxDepth?` |
| 152 | + |
| 153 | +### rename_file |
| 154 | + |
| 155 | +Rename files/directories within the same allowed directory root. Fails if destination exists. Cross-root renames are refused so the tool cannot be used to remove a file from one root as a delete workaround. Cross-device renames are also refused because the server does not emulate rename with copy/delete. |
| 156 | + |
| 157 | +- **Inputs:** `source`, `destination` |
| 158 | + |
| 159 | +### search_files |
| 160 | + |
| 161 | +Recursively search file/directory names, relative paths, file contents, or all targets. |
| 162 | + |
| 163 | +- **Inputs:** `path`, `pattern`, `search_mode?` (substring/regex/glob), `excludePatterns?`, `case_sensitive?`, `max_results?` (default 1000), `match_path?`, `target?`, `content_pattern?`, `include_context?`, `before?`, `after?`, `show_line_numbers?`, `max_file_bytes?`, `max_matches_per_file?`, `binary_policy?`, `output_format?` |
| 164 | +- **Targets:** `name`, `path`, `content`, or `all`; default is `name`, or `path` when legacy `match_path: true` is used |
| 165 | +- **Content search:** supports `substring` and `regex`; `glob` remains for path/name matching |
| 166 | +- **Structured fields:** `matches`, `paths`, `contentMatches`, `totalMatches`, `truncated`, and `skippedFiles` |
| 167 | +- **Result:** May include `warningCode: 'RESULTS_TRUNCATED'` with `totalMatches` |
| 168 | + |
| 169 | +### get_file_info |
| 170 | + |
| 171 | +Get detailed file/directory metadata including symlink detection and MIME type. Supports single-path and batch metadata calls. |
| 172 | + |
| 173 | +- **Inputs:** `path?`, `paths?`, `resolve?`, `explain_access?`, `include_line_count?`, `include_mime?`, `include_symlink?`, `output_format?` |
| 174 | +- **Single path:** behaves like the legacy call and errors at the top level if metadata cannot be read |
| 175 | +- **Batch paths:** returns structured per-path success/error entries without failing the whole batch |
| 176 | +- **Returns:** size, dates, permissions, `isSymlink?`, `symlinkTarget?`, `resolvedPath?`, `mimeType?`, `lineCount?` |
| 177 | + |
| 178 | +### list_allowed_directories |
| 179 | + |
| 180 | +Returns the list of filesystem roots the server is allowed to access. The tool keeps the standard name for client compatibility, but roots may be directories or exact-file roots. |
| 181 | + |
| 182 | +- **Inputs:** None |
| 183 | + |
| 184 | +## Supported Encodings |
| 185 | + |
| 186 | +`utf8`, `utf-8`, `ascii`, `latin1`, `base64`, `hex`, `ucs2`, `utf16le` |
| 187 | + |
| 188 | +BOM detection and stripping is automatic for UTF-8, UTF-16 BE, and UTF-16 LE. |
| 189 | + |
| 190 | +## Usage |
| 191 | + |
| 192 | +This fork is intended to run from a local checkout. Do not configure clients to fetch it from a public package registry. |
| 193 | + |
| 194 | +### Build The Local Checkout |
| 195 | + |
| 196 | +From this repository: |
| 197 | + |
| 198 | +```bash |
| 199 | +npm run build |
| 200 | +``` |
| 201 | + |
| 202 | +This writes the runnable server to `dist/index.js`. |
| 203 | + |
| 204 | +### MCP Client Configuration |
| 205 | + |
| 206 | +```json |
| 207 | +{ |
| 208 | + "mcpServers": { |
| 209 | + "filesystem": { |
| 210 | + "command": "node", |
| 211 | + "args": [ |
| 212 | + "/path/to/filesystem-ts/dist/index.js", |
| 213 | + "/Users/username/Desktop", |
| 214 | + "/path/to/other/allowed/dir" |
| 215 | + ] |
| 216 | + } |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +If your MCP client supplies roots through the MCP Roots protocol, omit the filesystem root arguments and point the client at the local built server: |
| 222 | + |
| 223 | +```json |
| 224 | +{ |
| 225 | + "mcpServers": { |
| 226 | + "filesystem": { |
| 227 | + "command": "node", |
| 228 | + "args": [ |
| 229 | + "/path/to/filesystem-ts/dist/index.js" |
| 230 | + ] |
| 231 | + } |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +### MCP Roots Protocol |
| 237 | + |
| 238 | +If no command-line directories are provided, the server waits for the client to supply roots via the MCP roots protocol. MCP roots must use `file://` URIs and may refer to directories or files. Directory roots allow operations within the directory; file roots allow operations on that exact file. Relative tool paths resolve against configured directory roots. The server also supports dynamic updates via `roots/list_changed` notifications. |
| 239 | + |
| 240 | +## License |
| 241 | + |
| 242 | +This MCP server is licensed under the MIT License. The original filesystem server code was derived from the Model Context Protocol servers project; see [Provenance](#provenance) and [LICENSE](LICENSE). |
0 commit comments