mcp-fs-shell-windows 0.2.19 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -190
- package/dist/run_javascript/handler.js +124 -0
- package/dist/run_javascript/schema.js +28 -0
- package/dist/server.js +22 -1
- package/dist/shell/handler.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,190 +1,201 @@
|
|
|
1
|
-
# mcp-fs-shell-windows — filesystem + standalone Windows shell MCP server
|
|
2
|
-
|
|
3
|
-
A Windows-first MCP server that gives AI clients **comprehensive filesystem access** (batched read/write/search/patch across your chosen drives and UNC shares) plus **reliable local shell execution** (synchronous, background, and interactive) — in a single Node.js process with no client-side permission toggles.
|
|
4
|
-
|
|
5
|
-
Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended), which itself extends the official [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem). Developed and tested primarily on **Windows**: drive-letter roots (`C:\`, `D:\`) and UNC network-share roots (`\\server\\share`) are first-class, and the shell tools run commands through `cmd.exe` byte-for-byte.
|
|
6
|
-
|
|
7
|
-
**npm:** [mcp-fs-shell-windows](https://www.npmjs.com/package/mcp-fs-shell-windows) · **Source:** [gerarddeluca/mcp-fs-shell-windows](https://github.com/gerarddeluca/mcp-fs-shell-windows)
|
|
8
|
-
|
|
9
|
-
## Provenance
|
|
10
|
-
|
|
11
|
-
| Layer | Source |
|
|
12
|
-
|---|---|
|
|
13
|
-
| Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
|
|
14
|
-
| Base fork | [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended) @ `b6c317a` (MIT) |
|
|
15
|
-
| This fork | v0.2.1 tool parity + `transfer_files` rename, robustness patches, 8 shell tools (v0.2.2), verbatim argv (v0.2.4), `launch_file` launcher (v0.2.7), raw-byte access + `dir_diff` + text encodings (v0.2.9–v0.2.11), `wait_for_file` (v0.2.12), `replace_regex` (v0.2.14), reparse-point & long-path hardening (v0.2.15–v0.2.16), Recycle-Bin deletes (v0.2.17), `create_link` (v0.2.18) — see [Differences](#differences-from-the-base-fork) |
|
|
16
|
-
|
|
17
|
-
## Features
|
|
18
|
-
|
|
19
|
-
- **30 filesystem tools** — batched read/write/append/delete (including raw-byte access, per-file text encodings, Recycle-Bin deletes, and link creation), move/copy, line-number patching, search (name / glob / regex / fuzzy / wait-for-file), in-place regex replacement, tree, counts, checksums, file and directory diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
|
|
20
|
-
- **8 shell tools** — synchronous, background, and interactive command execution implemented *inside the MCP server process*: no client-side permission toggles, survives client restarts, works with any MCP client.
|
|
21
|
-
- **1
|
|
22
|
-
- **Windows-
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
- `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
|
110
|
-
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
- `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
**v0.2.
|
|
162
|
-
|
|
163
|
-
**v0.2.
|
|
164
|
-
|
|
165
|
-
**v0.2.
|
|
166
|
-
|
|
167
|
-
**v0.2.
|
|
168
|
-
|
|
169
|
-
**v0.2.
|
|
170
|
-
|
|
171
|
-
**v0.2.
|
|
172
|
-
|
|
173
|
-
**v0.2.
|
|
174
|
-
|
|
175
|
-
**v0.2.
|
|
176
|
-
|
|
177
|
-
**v0.2.
|
|
178
|
-
|
|
179
|
-
**v0.2.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1
|
+
# mcp-fs-shell-windows — filesystem + standalone Windows shell MCP server
|
|
2
|
+
|
|
3
|
+
A Windows-first MCP server that gives AI clients **comprehensive filesystem access** (batched read/write/search/patch across your chosen drives and UNC shares) plus **reliable local shell execution** (synchronous, background, and interactive) — in a single Node.js process with no client-side permission toggles.
|
|
4
|
+
|
|
5
|
+
Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended), which itself extends the official [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem). Developed and tested primarily on **Windows**: drive-letter roots (`C:\`, `D:\`) and UNC network-share roots (`\\server\\share`) are first-class, and the shell tools run commands through `cmd.exe` byte-for-byte.
|
|
6
|
+
|
|
7
|
+
**npm:** [mcp-fs-shell-windows](https://www.npmjs.com/package/mcp-fs-shell-windows) · **Source:** [gerarddeluca/mcp-fs-shell-windows](https://github.com/gerarddeluca/mcp-fs-shell-windows)
|
|
8
|
+
|
|
9
|
+
## Provenance
|
|
10
|
+
|
|
11
|
+
| Layer | Source |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
|
|
14
|
+
| Base fork | [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended) @ `b6c317a` (MIT) |
|
|
15
|
+
| This fork | v0.2.1 tool parity + `transfer_files` rename, robustness patches, 8 shell tools (v0.2.2), verbatim argv (v0.2.4), `launch_file` launcher (v0.2.7), raw-byte access + `dir_diff` + text encodings (v0.2.9–v0.2.11), `wait_for_file` (v0.2.12), `replace_regex` (v0.2.14), reparse-point & long-path hardening (v0.2.15–v0.2.16), Recycle-Bin deletes (v0.2.17), `create_link` (v0.2.18), `run_javascript_free` unrestricted code execution (v0.2.20) — see [Differences](#differences-from-the-base-fork) |
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **30 filesystem tools** — batched read/write/append/delete (including raw-byte access, per-file text encodings, Recycle-Bin deletes, and link creation), move/copy, line-number patching, search (name / glob / regex / fuzzy / wait-for-file), in-place regex replacement, tree, counts, checksums, file and directory diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
|
|
20
|
+
- **8 shell tools** — synchronous, background, and interactive command execution implemented *inside the MCP server process*: no client-side permission toggles, survives client restarts, works with any MCP client.
|
|
21
|
+
- **1 code-execution tool** (`run_javascript_free`) — run a JavaScript/TypeScript snippet on the Deno runtime with **unrestricted** permissions (full filesystem, network, env, child processes, arbitrary imports): the "free" counterpart to sandboxed `run_javascript` tools that confine code to a working directory.
|
|
22
|
+
- **1 launcher tool** (`launch_file`, Windows-only) — opens files, folders, and http(s) links in their OS default app, fully detached: returns the moment the opener is spawned (never waits for the app), no console window, no stdio inheritance; filesystem targets are confined to the allowed directories.
|
|
23
|
+
- **Windows-first** — drive roots, UNC roots, long paths beyond the 260-char `MAX_PATH` limit, `cmd.exe /d /c` with verbatim argv, whole-process-tree kills (`taskkill /T /F`), detached interactive console windows.
|
|
24
|
+
- **NAS-safe startup** — an allowed directory that is missing or offline (e.g., a powered-down network share) logs a warning and the server continues instead of exiting.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js >= 20 (developed and tested on Node 22).
|
|
29
|
+
- Windows for the shell tools (they spawn `cmd.exe`, `taskkill`, and `powershell`). The filesystem tools work on any platform Node runs on.
|
|
30
|
+
- `shell_python` requires Python on the system PATH.
|
|
31
|
+
- `run_javascript_free` requires a Deno runtime: the `$DENO_PATH` env var, LM Studio's bundled binary (`<home>/.internal/utils/deno(.exe)` — auto-located by walking up from the server CWD or the user home), or `deno` on the PATH.
|
|
32
|
+
|
|
33
|
+
## Quick start (npm)
|
|
34
|
+
|
|
35
|
+
Published on npm — `npx` downloads the server (and all dependencies) and runs it:
|
|
36
|
+
|
|
37
|
+
```bat
|
|
38
|
+
npx -y mcp-fs-shell-windows C:\ D:\
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
MCP client config (Claude Desktop / LM Studio `mcp.json` style):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"filesystem": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "mcp-fs-shell-windows", "C:\\", "D:\\", "\\\\server\\share"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The filesystem tools can only touch the passed directories (and anything under them). A root that cannot be accessed at startup logs a warning and is skipped; the server keeps running. Note that the shell tools are **not** root-restricted — see [Security](#security).
|
|
55
|
+
|
|
56
|
+
## Build from source
|
|
57
|
+
|
|
58
|
+
For developers, or to run uncommitted changes:
|
|
59
|
+
|
|
60
|
+
```bat
|
|
61
|
+
git clone https://github.com/gerarddeluca/mcp-fs-shell-windows
|
|
62
|
+
cd mcp-fs-shell-windows
|
|
63
|
+
npm ci
|
|
64
|
+
npm run build
|
|
65
|
+
node dist\index.js <allowed-dir> [more-dirs...]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then point your MCP config at `node <path-to-repo>\dist\index.js` with the allowed directories as the remaining args (same shape as the npx example above).
|
|
69
|
+
|
|
70
|
+
### Filesystem tools (30)
|
|
71
|
+
|
|
72
|
+
| Tool | Purpose |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `read_files` | Read one or more files, line-numbered; `offset`/`limit` paging; per-file `encoding` (utf8/utf16le/utf16be/cp1252, auto-detected when omitted) |
|
|
75
|
+
| `write_new_files` | Create files (per-file `overwrite`, `textEncoding` utf8/utf16le/utf16be/cp1252, optional `base64` for binary) |
|
|
76
|
+
| `append_files` | Append to files (creates them if missing) |
|
|
77
|
+
| `delete_files` | Delete files/directories (`recursive`; junctions/symlinks are removed as links, never followed; `toRecycleBin` - Windows: send to Recycle Bin instead of permanent delete) |
|
|
78
|
+
| `copy_file` | Copy files/directories (`recursive`, `overwrite`) |
|
|
79
|
+
| `transfer_files` | Move/rename items (`overwrite`, creates parent dirs) |
|
|
80
|
+
| `patch_files` | Patch by line ranges (`replace`/`insertBefore`/`delete`, `dryRun`, git-diff output) |
|
|
81
|
+
| `create_directories` | Create directory paths (nested chains) |
|
|
82
|
+
| `list_directory` | Single-level listing ([FILE]/[DIR]/[LINK] prefixes, sizes, pagination, type filter, ignore globs) |
|
|
83
|
+
| `directory_tree` | Recursive tree view (JSON) with exclusion patterns; includes empty dirs and link nodes |
|
|
84
|
+
| `search_files` | Recursive case-insensitive name-substring search |
|
|
85
|
+
| `search_regex` | Content grep over a single file or directory tree (paged; total + per-file match counts, `countOnly` mode) |
|
|
86
|
+
| `search_glob` | Glob-based file search (e.g. `**/*.ts`, paged) |
|
|
87
|
+
| `fuzzy_find_files` | Typo-tolerant name/path search (Levenshtein) |
|
|
88
|
+
| `delete_files_by_pattern` | Regex delete within a single directory |
|
|
89
|
+
| `count_lines` | Line counts with filters |
|
|
90
|
+
| `file_diff` | Unified diff of two files |
|
|
91
|
+
| `content_diff` | Unified diff of two strings |
|
|
92
|
+
| `checksum_files` | md5/sha1/sha256/sha512 for files |
|
|
93
|
+
| `checksum_files_verif` | Verify files against expected hashes |
|
|
94
|
+
| `get_file_info` | Size, timestamps, type, permissions, `isReparsePoint`, (Windows) NTFS attributes |
|
|
95
|
+
| `edit_files` | Exact-match text replacement, EOL-tolerant (dominant line ending preserved) |
|
|
96
|
+
| `read_bytes` | Read raw bytes at (offset, length) as base64 JSON — any binary file, any size |
|
|
97
|
+
| `write_bytes` | Write raw bytes (base64) into an existing file at an offset — in place, or `extend` past EOF (gap zero-filled) |
|
|
98
|
+
| `replace_regex` | In-place regex replacement across one or more files; `dryRun` defaults to true (unified-diff preview); preserves dominant line endings |
|
|
99
|
+
| `dir_diff` | Recursive two-directory comparison (added/removed/changed/identical; unified diff for changed text files; `ignore` globs, `maxFiles` cap) |
|
|
100
|
+
| `convert_encoding` | Convert a file's text encoding (utf8/utf16le/utf16be/cp1252, source auto-detected) to a new file |
|
|
101
|
+
| `wait_for_file` | Poll until at least one file matching a glob appears under a directory (configurable timeout/interval) |
|
|
102
|
+
| `create_link` | Create a hardlink, directory junction, or file/directory symlink to an existing target (`auto`/`symlink`/`junction`/`hardlink`) |
|
|
103
|
+
| `list_allowed_directories` | List the allowed root directories |
|
|
104
|
+
|
|
105
|
+
### Shell tools (8, Windows-only)
|
|
106
|
+
|
|
107
|
+
User commands are spawned as `cmd.exe /d /c <command>` with **verbatim argv** — quotes, redirects, and metacharacters arrive byte-for-byte (Node `windowsVerbatimArguments`), and `/d` skips AutoRun registry entries.
|
|
108
|
+
|
|
109
|
+
| Tool | Purpose |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `shell_run` | Synchronous command (default 5 s, max 28 s); non-zero exit or timeout returns the captured stdout/stderr instead of hiding it |
|
|
112
|
+
| `shell_test` | Test wrapper (runs with `CI=true`); never errors on a failing test |
|
|
113
|
+
| `shell_start` | Background job — returns immediately with a job ID; auto-kills at `timeout_hours` (max 10); 256 KB in-memory tail + log file |
|
|
114
|
+
| `shell_check` | Job status, stdout/stderr tails, log file path |
|
|
115
|
+
| `shell_cancel` | Kill the whole process tree (`taskkill /T /F`, with PID-liveness fallback) |
|
|
116
|
+
| `shell_terminal` | Open a visible, separate, interactive `cmd /k` console window running the command; the window persists after the command finishes; close it with `shell_cancel` |
|
|
117
|
+
| `shell_python` | Run a Python snippet (temp `.py` + system Python) |
|
|
118
|
+
| `shell_cwd` | Get/set the default working directory for the other shell tools (every tool also accepts a per-call `cwd`) |
|
|
119
|
+
|
|
120
|
+
Practical notes:
|
|
121
|
+
|
|
122
|
+
- Background jobs live in an **in-memory registry** inside the server process: the table is lost on server restart, but job log files persist (under `%TEMP%\mcp-fs-shell-windows\shell-jobs`).
|
|
123
|
+
- `timeout /t` can fail when stdin is not an interactive console — use `ping -n N 127.0.0.1 >nul` for sleeps.
|
|
124
|
+
- Synchronous calls are bounded (max 28 s) so they cannot wedge the MCP request channel; anything longer should go through `shell_start`.
|
|
125
|
+
|
|
126
|
+
### Code-execution tool (1)
|
|
127
|
+
|
|
128
|
+
| Tool | Purpose |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `run_javascript_free` | Run a JS/TS snippet on the Deno runtime with **unrestricted permissions**: full filesystem read/write (any drive or UNC path the server user can reach), network, environment, child processes, and arbitrary imports (`node:*`, `npm:*`, `https:*`). Temp `.ts` file + `deno run --allow-read --allow-write --allow-net --allow-env --allow-sys --allow-run --allow-ffi --allow-import`. Default 5 s timeout, max 60 s; optional `cwd` (defaults to the `shell_cwd` default). Non-zero exit or timeout returns an error that still includes the captured stdout/stderr (100 KB / 20 KB caps). The Deno binary is located via `$DENO_PATH` → `<ancestor>/.internal/utils/deno(.exe)` (walk-up) → `<home>/.lmstudio/.internal/utils/deno(.exe)` → PATH. |
|
|
131
|
+
|
|
132
|
+
### Launcher tool (1, Windows-only)
|
|
133
|
+
|
|
134
|
+
| Tool | Purpose |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `launch_file` | Launch a file, folder, or http(s) link via the Windows shell opener (`explorer.exe` — the OS default association: VLC for .mkv, Explorer for folders, default browser for web links). The target is passed as a single argv element (spaces, Unicode, `&`, apostrophes, parentheses all safe) and the call returns as soon as the opener process is actually spawned — it never waits for the opened app, creates no console window, and inherits no stdio pipes. Filesystem paths are subject to the allowed-root policy and checked for existence; only `http://`/`https://` links are launched, every other URL scheme (`file:`, `javascript:`, `ms-*`, `steam:`, …) is rejected, so no `file:` URI can bypass the filesystem policy. A failed opener spawn errors instead of reporting success. The response reports `dispatched: true` (the opener process was created) — not that the app handled the target. A host-settable `LAUNCH_FILE_OPENER` env var overrides the opener executable (test seam). |
|
|
137
|
+
|
|
138
|
+
## Docker
|
|
139
|
+
|
|
140
|
+
```bat
|
|
141
|
+
docker build -t mcp-fs-shell-windows .
|
|
142
|
+
docker run -i --rm -v /path/to/dir:/data mcp-fs-shell-windows /data
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The filesystem tools work in the image; the shell tools require a Windows host with `cmd.exe`.
|
|
146
|
+
|
|
147
|
+
## Differences from the base fork
|
|
148
|
+
|
|
149
|
+
**v0.2.1 — official-server parity:** head/tail reads (`offset`/`limit` in `read_files`), sizes in listings, `overwrite` in `write_new_files`, exact-match text editing (`edit_files`).
|
|
150
|
+
|
|
151
|
+
**`transfer_files` (renamed from `move_files`):** the canonical move/rename tool. Renamed because at least one MCP client (observed in LM Studio) silently drops tools named `move_files`/`mv_files` from the exposed tool list; `transfer_files` is registered last in the tools array.
|
|
152
|
+
|
|
153
|
+
**Robustness patches:**
|
|
154
|
+
|
|
155
|
+
- Missing/offline allowed roots warn at startup instead of exiting (NAS-down safe).
|
|
156
|
+
- `validatePath` walks to the nearest existing ancestor, so writes under brand-new nested directories work.
|
|
157
|
+
- `ensureDirectoryExists` at all `mkdir` sites (Node EPERM quirk on drive-root writes).
|
|
158
|
+
- Move-handler destination checks use errno `ENOENT`, so moves to fresh destinations succeed.
|
|
159
|
+
- `zod` pinned to `^3.23.8` (zod v4 breaks the SDK's schema conversion).
|
|
160
|
+
|
|
161
|
+
**v0.2.2 — shell tools** as listed above, with whole-process-tree kills and the detached interactive console window.
|
|
162
|
+
|
|
163
|
+
**v0.2.4 — verbatim argv:** `windowsVerbatimArguments` + `/d` on all user-command spawns (run/test/start and the terminal launcher).
|
|
164
|
+
|
|
165
|
+
**v0.2.7 — `launch_file`:** the detached OS launcher (see above). Renamed from a local `open_file` prototype so it cannot collide with other servers' `open_file` tools. Hardened: spawn-failure returns an error (the handler awaits the child's `spawn`/`error` events — it never reports success for a process Windows refused to create), URLs are restricted to http(s) so `file:` URIs cannot bypass the allowed-root policy, and non-Windows platforms fail with a clear error.
|
|
166
|
+
|
|
167
|
+
**v0.2.8 — line-ending preservation:** `patch_files` and `edit_files` preserve the file's dominant line ending (CRLF or LF) on write.
|
|
168
|
+
|
|
169
|
+
**v0.2.9 — `read_bytes` / `write_bytes`:** raw binary access at (offset, length) — base64 reads from any part of a file (any size, e.g. GGUFs and blobs) and in-place byte writes into existing files (`extend: true` grows the file past EOF, zero-filling the gap).
|
|
170
|
+
|
|
171
|
+
**v0.2.10 — `dir_diff`:** recursive comparison of two directory trees — added/removed/changed/identical, unified diffs for changed text files, `ignore` glob patterns, and a `maxFiles` cap.
|
|
172
|
+
|
|
173
|
+
**v0.2.11 — text encoding support:** per-file `encoding` in `read_files` (utf8/utf16le/utf16be/cp1252, auto-detected from BOMs when omitted), `textEncoding` in `write_new_files`, and `convert_encoding` to transcode a file into a new encoding.
|
|
174
|
+
|
|
175
|
+
**v0.2.12 — `wait_for_file`:** polling file watcher — waits (up to `timeoutMs`, default 30 s, max 300 s) until at least one file matching a glob appears under a directory.
|
|
176
|
+
|
|
177
|
+
**v0.2.13 — `search_regex` match totals:** every pattern occurrence is counted (a line matching N times counts N); responses end with the total and per-file counts, and `countOnly: true` returns counts without the matching line text.
|
|
178
|
+
|
|
179
|
+
**v0.2.14 — `replace_regex`:** in-place regex replacement across one or more files (directories are rejected). `dryRun` defaults to `true` with a unified-diff preview; applied replacements preserve each file's dominant line ending; a missing path or invalid regex errors without writing anything.
|
|
180
|
+
|
|
181
|
+
**v0.2.15 — reparse-point safety:** junctions/symlinks are never followed. `delete_files` removes them as links (including nested links inside recursive deletes) without touching their targets; `list_directory` shows `[LINK]` entries; `directory_tree` reports link nodes and includes empty directories; `get_file_info` adds `isReparsePoint` and (on Windows) NTFS attributes.
|
|
182
|
+
|
|
183
|
+
**v0.2.16 — long-path support:** paths beyond the 260-char `MAX_PATH` limit work on Windows via `\\?\` extended-length form (drive and UNC), transparent to callers.
|
|
184
|
+
|
|
185
|
+
**v0.2.17 — `delete_files` `toRecycleBin`:** send deletes to the Windows Recycle Bin instead of permanent removal. Reparse points are still removed as links (the bin cannot hold them), and any path that cannot be recycled is reported as an error — never silently hard-deleted.
|
|
186
|
+
|
|
187
|
+
**v0.2.18 — `create_link`:** create hardlinks, directory junctions, and file/directory symlinks (`type: auto | symlink | junction | hardlink`) with strict checks: the target must exist, the link path must not, and the link path's parent must exist.
|
|
188
|
+
|
|
189
|
+
**v0.2.20 — `run_javascript_free`:** unrestricted JS/TS code execution on the Deno runtime — full filesystem, network, env, child-process, and import permissions (no working-directory sandbox). Reuses the shell layer's spawn/capture machinery (temp `.ts` in the shell-jobs dir, whole-tree kill on timeout, 100 KB / 20 KB output caps). The Deno binary is auto-located (`$DENO_PATH` → LM Studio's bundled `.internal/utils/deno(.exe)` via CWD walk-up or user home → PATH); missing binary yields a clear error. Default 5 s timeout, max 60 s.
|
|
190
|
+
|
|
191
|
+
## Security
|
|
192
|
+
|
|
193
|
+
- The filesystem tools are confined to the allowed directories passed at startup (and their subtrees).
|
|
194
|
+
- The shell tools are **not** confined: anything the server process's user can execute will run, including against reachable network shares. Grant this server the same trust you would grant an unsandboxed shell.
|
|
195
|
+
- `launch_file` targets are confined to the allowed directories (for paths), but the opened application runs with the full rights of the server user — like anything you double-click.
|
|
196
|
+
- `run_javascript_free` is **not** confined: the snippet runs as an unsandboxed Deno process with the full rights of the server user (filesystem, network, processes). Grant this server the same trust you would grant an unsandboxed shell.
|
|
197
|
+
- The server communicates over stdio and opens no network endpoints of its own.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT — see [LICENSE](./LICENSE). This project derives from MIT-licensed code (Anthropic, PBC; fabienvauchelles); all copyright notices are in the LICENSE file.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// run_javascript/handler.ts — unrestricted JS/TS snippet execution (MCP filesystem fork).
|
|
2
|
+
//
|
|
3
|
+
// Runs a snippet on the Deno runtime with FULL permissions (no sandbox):
|
|
4
|
+
// the script may read/write anywhere the server user can reach (drive letters
|
|
5
|
+
// and UNC paths), use the network, read the environment, spawn processes, and
|
|
6
|
+
// import arbitrary modules (node:*, npm:*, https:*). This is the "free"
|
|
7
|
+
// counterpart to the other run_javascript tools, which confine snippets to a
|
|
8
|
+
// working directory with net/env/sys/run/ffi denied.
|
|
9
|
+
//
|
|
10
|
+
// Engine resolution (first hit wins; result is cached):
|
|
11
|
+
// 1. $DENO_PATH env var (absolute path to a deno binary)
|
|
12
|
+
// 2. <ancestor>/.internal/utils/deno(.exe) walking up from the server CWD
|
|
13
|
+
// (LM Studio bundles deno at <home>/.internal/utils)
|
|
14
|
+
// 3. <user home>/.lmstudio/.internal/utils/deno(.exe)
|
|
15
|
+
// 4. deno on PATH
|
|
16
|
+
//
|
|
17
|
+
// Reuses the shell layer's spawn/capture machinery (runCaptured + caps).
|
|
18
|
+
import fsp from "fs/promises";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import nodePath from "path";
|
|
21
|
+
import os from "os";
|
|
22
|
+
import crypto from "crypto";
|
|
23
|
+
import { RUN_JS_MAX_TIMEOUT_SEC } from "./schema.js";
|
|
24
|
+
import { cap, CAP_OK, CAP_ERR, SHELL_JOBS_DIR, ensureJobsDir, resolveCwd, runCaptured, } from "../shell/handler.js";
|
|
25
|
+
function denoName() {
|
|
26
|
+
return process.platform === "win32" ? "deno.exe" : "deno";
|
|
27
|
+
}
|
|
28
|
+
let cachedDenoPath;
|
|
29
|
+
export function resolveDenoPath() {
|
|
30
|
+
if (cachedDenoPath !== undefined)
|
|
31
|
+
return cachedDenoPath;
|
|
32
|
+
let found = null;
|
|
33
|
+
// 1. explicit override
|
|
34
|
+
const env = process.env.DENO_PATH;
|
|
35
|
+
if (env && fs.existsSync(env))
|
|
36
|
+
found = env;
|
|
37
|
+
// 2. walk up from the server CWD: <dir>/.internal/utils/deno(.exe)
|
|
38
|
+
if (!found) {
|
|
39
|
+
let dir = process.cwd();
|
|
40
|
+
for (let i = 0; i < 8 && !found; i++) {
|
|
41
|
+
const candidate = nodePath.join(dir, ".internal", "utils", denoName());
|
|
42
|
+
if (fs.existsSync(candidate))
|
|
43
|
+
found = candidate;
|
|
44
|
+
const parent = nodePath.dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
break;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 3. typical LM Studio user install: <home>/.lmstudio/.internal/utils/deno(.exe)
|
|
51
|
+
if (!found) {
|
|
52
|
+
const candidate = nodePath.join(os.homedir(), ".lmstudio", ".internal", "utils", denoName());
|
|
53
|
+
if (fs.existsSync(candidate))
|
|
54
|
+
found = candidate;
|
|
55
|
+
}
|
|
56
|
+
// 4. PATH lookup
|
|
57
|
+
if (!found) {
|
|
58
|
+
const dirs = (process.env.PATH ?? "").split(nodePath.delimiter).filter(Boolean);
|
|
59
|
+
for (const d of dirs) {
|
|
60
|
+
const candidate = nodePath.join(d, denoName());
|
|
61
|
+
if (fs.existsSync(candidate)) {
|
|
62
|
+
found = candidate;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
cachedDenoPath = found;
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
export async function handleRunJavascriptFree(javascript, timeoutSeconds, cwd) {
|
|
71
|
+
const deno = resolveDenoPath();
|
|
72
|
+
if (!deno) {
|
|
73
|
+
throw new Error("No Deno runtime found. Set the DENO_PATH environment variable to a deno binary, " +
|
|
74
|
+
"install deno on PATH, or run this server from inside an LM Studio installation " +
|
|
75
|
+
"(which bundles deno at <home>/.internal/utils/deno.exe).");
|
|
76
|
+
}
|
|
77
|
+
const abs = await resolveCwd(cwd);
|
|
78
|
+
await ensureJobsDir();
|
|
79
|
+
const file = nodePath.join(SHELL_JOBS_DIR, `js-${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}.ts`);
|
|
80
|
+
await fsp.writeFile(file, javascript, "utf-8");
|
|
81
|
+
const timeout = Math.min(Math.max(timeoutSeconds ?? 5, 0.1), RUN_JS_MAX_TIMEOUT_SEC);
|
|
82
|
+
const started = Date.now();
|
|
83
|
+
try {
|
|
84
|
+
const r = await runCaptured(deno, [
|
|
85
|
+
"run",
|
|
86
|
+
"--no-prompt",
|
|
87
|
+
"--allow-read",
|
|
88
|
+
"--allow-write",
|
|
89
|
+
"--allow-net",
|
|
90
|
+
"--allow-env",
|
|
91
|
+
"--allow-sys",
|
|
92
|
+
"--allow-run",
|
|
93
|
+
"--allow-ffi",
|
|
94
|
+
"--allow-import",
|
|
95
|
+
file,
|
|
96
|
+
], {
|
|
97
|
+
cwd: abs,
|
|
98
|
+
timeoutMs: timeout * 1000,
|
|
99
|
+
env: { ...process.env, NO_COLOR: "true" },
|
|
100
|
+
});
|
|
101
|
+
const durationMs = Date.now() - started;
|
|
102
|
+
if (r.spawnError) {
|
|
103
|
+
throw new Error(`Failed to launch deno: ${r.spawnError}`);
|
|
104
|
+
}
|
|
105
|
+
if (r.timedOut) {
|
|
106
|
+
throw new Error(`Process timed out after ${timeout}s.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
|
|
107
|
+
}
|
|
108
|
+
if (r.code !== 0) {
|
|
109
|
+
throw new Error(`Process exited with code ${r.code ?? "unknown"}.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
|
|
110
|
+
}
|
|
111
|
+
return JSON.stringify({
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
stdout: cap(r.stdout, CAP_OK),
|
|
114
|
+
stderr: cap(r.stderr, CAP_OK),
|
|
115
|
+
timedOut: false,
|
|
116
|
+
duration_ms: durationMs,
|
|
117
|
+
cwd: abs,
|
|
118
|
+
deno,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await fsp.unlink(file).catch(() => undefined);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// run_javascript/schema.ts — argument schema for the unrestricted JS/TS execution tool.
|
|
2
|
+
// (MCP filesystem fork: the "free" run_javascript — no working-directory sandbox.)
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// Unlike the shell tools (28 s cap), this tool may run up to 60 s: the MCP
|
|
5
|
+
// client request window was measured >63 s (a ~63 s in-harness tool call
|
|
6
|
+
// survived, 2026-08-29), and 60 s matches the sibling run_javascript tools.
|
|
7
|
+
export const RUN_JS_MAX_TIMEOUT_SEC = 60;
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Tool description (verbatim — pasted into server.ts ListTools entry):
|
|
10
|
+
//
|
|
11
|
+
// run_javascript_free:
|
|
12
|
+
// "Run a JavaScript/TypeScript code snippet on the Deno runtime with UNRESTRICTED permissions: full filesystem read/write (any drive or UNC path the server user can reach), network access, environment variables, child processes, and arbitrary imports (node:*, npm:*, https:*) — unlike the other run_javascript tools, there is no working-directory sandbox. The code is written to a temp .ts file and run; returns exit code, stdout, and stderr. Default 5 s timeout, max 60 s; optional cwd (defaults to the shell_cwd default). Non-zero exit or timeout returns an error that still includes the captured output. Requires a Deno binary: $DENO_PATH, <LM Studio home>/.internal/utils/deno(.exe) (found by walking up from the server CWD or the user home), or deno on PATH."
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export const RunJavascriptFreeArgsSchema = z.object({
|
|
15
|
+
javascript: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe("JavaScript (or TypeScript) code to execute. Runs as a Deno script: Deno.* APIs, console.log, and imports (node:*, npm:*, https:*) all work."),
|
|
18
|
+
timeout_seconds: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(0.1)
|
|
21
|
+
.max(RUN_JS_MAX_TIMEOUT_SEC)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Timeout in seconds (default: 5, max: 60)."),
|
|
24
|
+
cwd: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Working directory for the snippet (default: the shell_cwd default)."),
|
|
28
|
+
});
|
package/dist/server.js
CHANGED
|
@@ -42,6 +42,7 @@ import { ShellCancelArgsSchema } from "./shell/schema.js";
|
|
|
42
42
|
import { ShellTerminalArgsSchema } from "./shell/schema.js";
|
|
43
43
|
import { ShellPythonArgsSchema } from "./shell/schema.js";
|
|
44
44
|
import { ShellCwdArgsSchema } from "./shell/schema.js";
|
|
45
|
+
import { RunJavascriptFreeArgsSchema } from "./run_javascript/schema.js";
|
|
45
46
|
// Import all handlers
|
|
46
47
|
import { handleReadFiles } from "./read_files/handler.js";
|
|
47
48
|
import { handleWriteNewFiles } from "./write_new_files/handler.js";
|
|
@@ -81,6 +82,7 @@ import { handleShellCancel } from "./shell/handler.js";
|
|
|
81
82
|
import { handleShellTerminal } from "./shell/handler.js";
|
|
82
83
|
import { handleShellPython } from "./shell/handler.js";
|
|
83
84
|
import { handleShellCwd } from "./shell/handler.js";
|
|
85
|
+
import { handleRunJavascriptFree } from "./run_javascript/handler.js";
|
|
84
86
|
const ToolInputSchema = ToolSchema.shape.inputSchema;
|
|
85
87
|
export class FilesystemServer {
|
|
86
88
|
server;
|
|
@@ -89,7 +91,7 @@ export class FilesystemServer {
|
|
|
89
91
|
this.allowedDirectories = allowedDirectories;
|
|
90
92
|
this.server = new Server({
|
|
91
93
|
name: "secure-filesystem-server",
|
|
92
|
-
version: "0.2.
|
|
94
|
+
version: "0.2.20",
|
|
93
95
|
}, {
|
|
94
96
|
capabilities: {
|
|
95
97
|
tools: {},
|
|
@@ -467,6 +469,15 @@ export class FilesystemServer {
|
|
|
467
469
|
"Only works within allowed directories.",
|
|
468
470
|
inputSchema: zodToJsonSchema(WaitForFileArgsSchema),
|
|
469
471
|
},
|
|
472
|
+
{
|
|
473
|
+
name: "run_javascript_free",
|
|
474
|
+
description: "Run a JavaScript/TypeScript code snippet on the Deno runtime with UNRESTRICTED permissions: full filesystem read/write (any drive or UNC path the server user can reach), network access, environment variables, child processes, and arbitrary imports (node:*, npm:*, https:*) - unlike the other run_javascript tools, there is no working-directory sandbox. " +
|
|
475
|
+
"The code is written to a temp .ts file and run; returns exit code, stdout, and stderr. " +
|
|
476
|
+
"Default 5 s timeout, max 60 s; optional cwd (defaults to the shell_cwd default). " +
|
|
477
|
+
"Non-zero exit or timeout returns an error that still includes the captured output. " +
|
|
478
|
+
"Requires a Deno binary: $DENO_PATH, <LM Studio home>/.internal/utils/deno(.exe) (found by walking up from the server CWD or the user home), or deno on PATH.",
|
|
479
|
+
inputSchema: zodToJsonSchema(RunJavascriptFreeArgsSchema),
|
|
480
|
+
},
|
|
470
481
|
],
|
|
471
482
|
};
|
|
472
483
|
});
|
|
@@ -869,6 +880,16 @@ export class FilesystemServer {
|
|
|
869
880
|
content: [{ type: "text", text: result }],
|
|
870
881
|
};
|
|
871
882
|
}
|
|
883
|
+
case "run_javascript_free": {
|
|
884
|
+
const parsed = RunJavascriptFreeArgsSchema.safeParse(args);
|
|
885
|
+
if (!parsed.success) {
|
|
886
|
+
throw new Error(`Invalid arguments for run_javascript_free: ${parsed.error}`);
|
|
887
|
+
}
|
|
888
|
+
const result = await handleRunJavascriptFree(parsed.data.javascript, parsed.data.timeout_seconds, parsed.data.cwd);
|
|
889
|
+
return {
|
|
890
|
+
content: [{ type: "text", text: result }],
|
|
891
|
+
};
|
|
892
|
+
}
|
|
872
893
|
default:
|
|
873
894
|
throw new Error(`Unknown tool: ${name}`);
|
|
874
895
|
}
|
package/dist/shell/handler.js
CHANGED
|
@@ -36,19 +36,19 @@ catch {
|
|
|
36
36
|
// Job dir may not exist yet — that's fine.
|
|
37
37
|
}
|
|
38
38
|
/** Keep only the last `max` chars; mark truncation. */
|
|
39
|
-
function cap(text, max) {
|
|
39
|
+
export function cap(text, max) {
|
|
40
40
|
if (text.length <= max)
|
|
41
41
|
return text;
|
|
42
42
|
return `...[truncated] ` + text.slice(-max);
|
|
43
43
|
}
|
|
44
|
-
const CAP_OK = 100000; // cap for stdout/stderr in success payloads
|
|
45
|
-
const CAP_ERR = 20000; // cap for output embedded in error messages
|
|
44
|
+
export const CAP_OK = 100000; // cap for stdout/stderr in success payloads
|
|
45
|
+
export const CAP_ERR = 20000; // cap for output embedded in error messages
|
|
46
46
|
const TAIL = 262144; // per-stream in-memory job tail (256 KB)
|
|
47
47
|
function newJobId() {
|
|
48
48
|
return `sj-${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}`;
|
|
49
49
|
}
|
|
50
50
|
/** Resolve the effective working directory (param override or module default) and validate it. */
|
|
51
|
-
async function resolveCwd(cwd) {
|
|
51
|
+
export async function resolveCwd(cwd) {
|
|
52
52
|
const abs = cwd ? nodePath.resolve(cwd) : defaultShellCwd;
|
|
53
53
|
const st = await fsp.stat(abs).catch(() => null);
|
|
54
54
|
if (!st || !st.isDirectory()) {
|
|
@@ -189,14 +189,14 @@ function makeLineFeeder(logStream) {
|
|
|
189
189
|
};
|
|
190
190
|
return { feed, flush };
|
|
191
191
|
}
|
|
192
|
-
async function ensureJobsDir() {
|
|
192
|
+
export async function ensureJobsDir() {
|
|
193
193
|
await fsp.mkdir(SHELL_JOBS_DIR, { recursive: true }).catch(() => undefined);
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
196
|
* Spawn a process, capture stdout/stderr (utf-8), enforce a timeout via killTree.
|
|
197
197
|
* Resolves (never rejects) with the outcome; spawn failures are reported in spawnError.
|
|
198
198
|
*/
|
|
199
|
-
async function runCaptured(exe, args, opts) {
|
|
199
|
+
export async function runCaptured(exe, args, opts) {
|
|
200
200
|
const result = { stdout: "", stderr: "", code: null, timedOut: false };
|
|
201
201
|
await new Promise((resolve) => {
|
|
202
202
|
let proc;
|
package/package.json
CHANGED