Skip to content

Commit 35ca080

Browse files
author
vampyre
committed
Initial filesystem-ts fork
Create the standalone TypeScript filesystem MCP server fork with upstream provenance, local MIT license text, hardened filesystem operations, structured assistant-friendly outputs, line-aware reads and edits, guarded rename semantics, atomic temp cleanup, updated dependencies, expanded regression coverage, and GitHub Actions CI.
0 parents  commit 35ca080

18 files changed

Lines changed: 10864 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- "**"
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
test:
15+
name: ${{ matrix.os }} / Node ${{ matrix.node-version }}
16+
runs-on: ${{ matrix.os }}
17+
timeout-minutes: 15
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
os:
22+
- ubuntu-latest
23+
- macos-latest
24+
- windows-latest
25+
node-version:
26+
- 20.x
27+
- 22.x
28+
29+
steps:
30+
- name: Check out repository
31+
uses: actions/checkout@v4
32+
33+
- name: Set up Node.js
34+
uses: actions/setup-node@v4
35+
with:
36+
node-version: ${{ matrix.node-version }}
37+
cache: npm
38+
39+
- name: Install dependencies
40+
run: npm ci
41+
42+
- name: Build
43+
run: npm run build
44+
45+
- name: Lint
46+
run: npm run lint
47+
48+
- name: Test
49+
run: npm test
50+
51+
audit:
52+
name: Dependency audit
53+
runs-on: ubuntu-latest
54+
timeout-minutes: 10
55+
56+
steps:
57+
- name: Check out repository
58+
uses: actions/checkout@v4
59+
60+
- name: Set up Node.js
61+
uses: actions/setup-node@v4
62+
with:
63+
node-version: 22.x
64+
cache: npm
65+
66+
- name: Install dependencies
67+
run: npm ci
68+
69+
- name: Audit dependencies
70+
run: npm audit --audit-level=moderate

Dockerfile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Stage 1: Builder
2+
FROM node:22.12-alpine AS builder
3+
WORKDIR /app
4+
5+
# Copy configuration and source files needed for build
6+
# Assumes the Docker build context is the 'filesystem-server' directory
7+
COPY package.json package-lock.json ./
8+
COPY tsconfig.json tsconfig.base.json ./
9+
COPY index.ts constants.ts path-utils.ts lib.ts ./
10+
11+
# Install ALL dependencies (including dev) needed for build
12+
# Use cache mount for faster subsequent builds
13+
RUN --mount=type=cache,target=/root/.npm npm install
14+
15+
# Build the TypeScript code using the script defined in package.json
16+
RUN npm run build
17+
18+
# Stage 2: Release
19+
FROM node:22-alpine AS release
20+
WORKDIR /app
21+
22+
# Copy only necessary artifacts from builder stage
23+
COPY --from=builder /app/dist ./dist
24+
COPY --from=builder /app/package.json ./package.json
25+
COPY --from=builder /app/package-lock.json ./package-lock.json
26+
27+
ENV NODE_ENV=production
28+
29+
# Install ONLY production dependencies using package-lock.json
30+
# Ensures a clean production node_modules directory
31+
RUN npm ci --ignore-scripts --omit-dev
32+
33+
# Run the server using the built JavaScript file
34+
ENTRYPOINT ["node", "./dist/index.js"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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

Comments
 (0)