mcp-fs-shell-windows 0.2.6 → 0.2.7
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 +11 -1
- package/dist/launch_file/handler.js +152 -0
- package/dist/launch_file/schema.js +4 -0
- package/dist/server.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,12 +12,13 @@ Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://gith
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
|
|
14
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) — see [Differences](#differences-from-the-base-fork) |
|
|
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) — see [Differences](#differences-from-the-base-fork) |
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
19
|
- **23 filesystem tools** — batched read/write/append/delete, move/copy, line-number patching, search (name / glob / regex / fuzzy), tree, counts, checksums, diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
|
|
20
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 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.
|
|
21
22
|
- **Windows-first** — drive roots, UNC roots, `cmd.exe /d /c` with verbatim argv, whole-process-tree kills (`taskkill /T /F`), detached interactive console windows.
|
|
22
23
|
- **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.
|
|
23
24
|
|
|
@@ -113,6 +114,12 @@ Practical notes:
|
|
|
113
114
|
- `timeout /t` can fail when stdin is not an interactive console — use `ping -n N 127.0.0.1 >nul` for sleeps.
|
|
114
115
|
- Synchronous calls are bounded (max 28 s) so they cannot wedge the MCP request channel; anything longer should go through `shell_start`.
|
|
115
116
|
|
|
117
|
+
### Launcher tool (1, Windows-only)
|
|
118
|
+
|
|
119
|
+
| Tool | Purpose |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `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). |
|
|
122
|
+
|
|
116
123
|
## Docker
|
|
117
124
|
|
|
118
125
|
```bat
|
|
@@ -140,10 +147,13 @@ The filesystem tools work in the image; the shell tools require a Windows host w
|
|
|
140
147
|
|
|
141
148
|
**v0.2.4 — verbatim argv:** `windowsVerbatimArguments` + `/d` on all user-command spawns (run/test/start and the terminal launcher).
|
|
142
149
|
|
|
150
|
+
**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.
|
|
151
|
+
|
|
143
152
|
## Security
|
|
144
153
|
|
|
145
154
|
- The filesystem tools are confined to the allowed directories passed at startup (and their subtrees).
|
|
146
155
|
- 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.
|
|
156
|
+
- `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.
|
|
147
157
|
- The server communicates over stdio and opens no network endpoints of its own.
|
|
148
158
|
|
|
149
159
|
## License
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { validatePath } from "../helpers/path.js";
|
|
5
|
+
/**
|
|
6
|
+
* Launch a file, directory, or web link in its Windows-associated
|
|
7
|
+
* application (the OS default: e.g. VLC for .mkv/.mp4, Explorer for
|
|
8
|
+
* folders, the default browser for http(s) links).
|
|
9
|
+
*
|
|
10
|
+
* Windows-only. Mechanism: spawn the shell opener with the target as a
|
|
11
|
+
* single argv element and stdio ignored:
|
|
12
|
+
* %SystemRoot%\explorer.exe <target>
|
|
13
|
+
*
|
|
14
|
+
* - Default association: explorer.exe performs a ShellExecute "open" -
|
|
15
|
+
* exactly what a double-click does. Nothing is hardcoded.
|
|
16
|
+
* - No pipe inheritance: stdio is "ignore", so the launched process never
|
|
17
|
+
* receives the MCP server's stdin/stdout/stderr. The handler resolves
|
|
18
|
+
* the moment Windows has spawned the opener - it does NOT stay pending
|
|
19
|
+
* until the opened application exits (the exact bug the cmd+start route
|
|
20
|
+
* hit: a long-lived GUI child held the pipes open).
|
|
21
|
+
* - No intermediary console: the opener is a GUI process and the spawn
|
|
22
|
+
* uses windowsHide, so no blank cmd.exe / PowerShell window appears.
|
|
23
|
+
* - Verbatim arguments: the target is one argv element, so spaces,
|
|
24
|
+
* Unicode, apostrophes, ampersands, parentheses, and every other
|
|
25
|
+
* filename character are passed through exactly (no cmd/PowerShell
|
|
26
|
+
* quoting to get wrong).
|
|
27
|
+
* - Security policy: filesystem targets are routed through validatePath
|
|
28
|
+
* (same allowed-roots + symlink resolution as every other tool). Only
|
|
29
|
+
* http:// and https:// links are dispatched to the OS directly; every
|
|
30
|
+
* other URL scheme (file:, javascript:, ms-*, steam:, vlc:, ...) is
|
|
31
|
+
* rejected so the tool cannot reach arbitrary Windows protocol
|
|
32
|
+
* handlers or bypass the allowed-roots policy.
|
|
33
|
+
*/
|
|
34
|
+
// Standard-form web links - the ONLY URLs dispatched to the OS opener.
|
|
35
|
+
const WEB_URL = /^https?:\/\//i;
|
|
36
|
+
// A Windows drive path: single letter, ':', then a separator or end of
|
|
37
|
+
// string, e.g. "M:\\...", "C:/...", or a bare "M:".
|
|
38
|
+
const WINDOWS_DRIVE = /^[A-Za-z]:([\\/]|$)/;
|
|
39
|
+
// A URI scheme: 2+ scheme chars then ':'. (A bare drive letter is exactly
|
|
40
|
+
// one char, so "M:foo" is NOT mistaken for a scheme and still resolves as
|
|
41
|
+
// a drive-relative path through validatePath.)
|
|
42
|
+
const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]+:/;
|
|
43
|
+
/**
|
|
44
|
+
* Windows-only guard; returns the full path to the shell opener.
|
|
45
|
+
*
|
|
46
|
+
* The opener defaults to %SystemRoot%\explorer.exe. The host-settable
|
|
47
|
+
* LAUNCH_FILE_OPENER environment variable overrides it - a test seam that
|
|
48
|
+
* makes the spawn-failure path testable end-to-end without touching the
|
|
49
|
+
* real environment (note: faking SystemRoot itself is impossible; Node's
|
|
50
|
+
* Windows CSPRNG init aborts unless SystemRoot is the real Windows dir).
|
|
51
|
+
* Only the opener executable changes; the target still passes the full
|
|
52
|
+
* URL/path policy below.
|
|
53
|
+
*/
|
|
54
|
+
function openerPath() {
|
|
55
|
+
if (process.platform !== "win32") {
|
|
56
|
+
throw new Error("launch_file is supported only on Windows");
|
|
57
|
+
}
|
|
58
|
+
const override = process.env.LAUNCH_FILE_OPENER;
|
|
59
|
+
if (override) {
|
|
60
|
+
return override;
|
|
61
|
+
}
|
|
62
|
+
const sysRoot = process.env.SystemRoot || "C:\\Windows";
|
|
63
|
+
return path.join(sysRoot, "explorer.exe");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Spawn the opener fully detached and settle the promise only when
|
|
67
|
+
* Windows has actually created the process ("spawn" event). A failed
|
|
68
|
+
* process creation ("error" event, e.g. opener executable missing)
|
|
69
|
+
* REJECTS - it never falsely reports success. Never waits for the opener
|
|
70
|
+
* to exit.
|
|
71
|
+
*
|
|
72
|
+
* Both listeners are removed on settlement, so there is no double
|
|
73
|
+
* settlement and no listener leak. Exported so tests can exercise both
|
|
74
|
+
* settlement paths directly.
|
|
75
|
+
*/
|
|
76
|
+
export function launchDetached(opener, target) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
let child;
|
|
79
|
+
try {
|
|
80
|
+
child = spawn(opener, [target], {
|
|
81
|
+
stdio: "ignore",
|
|
82
|
+
detached: true,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
reject(err);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const onSpawn = () => {
|
|
91
|
+
child.removeListener("error", onError);
|
|
92
|
+
child.unref();
|
|
93
|
+
resolve();
|
|
94
|
+
};
|
|
95
|
+
const onError = (err) => {
|
|
96
|
+
child.removeListener("spawn", onSpawn);
|
|
97
|
+
child.unref();
|
|
98
|
+
reject(err);
|
|
99
|
+
};
|
|
100
|
+
child.once("spawn", onSpawn);
|
|
101
|
+
child.once("error", onError);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function handleLaunchFile(target, allowedDirectories) {
|
|
105
|
+
const trimmed = (target ?? "").trim();
|
|
106
|
+
if (!trimmed) {
|
|
107
|
+
throw new Error("path must not be empty");
|
|
108
|
+
}
|
|
109
|
+
const opener = openerPath();
|
|
110
|
+
// --- Web link: only http/https; ask the OS to open it (default browser) ---
|
|
111
|
+
if (WEB_URL.test(trimmed)) {
|
|
112
|
+
try {
|
|
113
|
+
await launchDetached(opener, trimmed);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new Error(`Failed to launch URL: ${errorMessage(err)}`);
|
|
117
|
+
}
|
|
118
|
+
return JSON.stringify({ target: trimmed, kind: "url", dispatched: true });
|
|
119
|
+
}
|
|
120
|
+
// --- Other URL schemes: reject. This keeps file: URLs (and any other
|
|
121
|
+
// registered protocol handler) from bypassing the filesystem policy. ---
|
|
122
|
+
if (URI_SCHEME.test(trimmed) && !WINDOWS_DRIVE.test(trimmed)) {
|
|
123
|
+
throw new Error("Unsupported URL: only http:// and https:// links can be launched. " +
|
|
124
|
+
'Filesystem targets must be plain paths (e.g. "M:\\..." or "\\\\server\\share"), ' +
|
|
125
|
+
"not file: URLs or other protocol schemes.");
|
|
126
|
+
}
|
|
127
|
+
// --- Filesystem target: enforce the existing path/security policy ---
|
|
128
|
+
const resolved = await validatePath(trimmed, allowedDirectories);
|
|
129
|
+
// Useful error for a target that does not exist.
|
|
130
|
+
let stats;
|
|
131
|
+
try {
|
|
132
|
+
stats = await fs.stat(resolved);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error(`Cannot launch: path does not exist: ${resolved}`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await launchDetached(opener, resolved);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
throw new Error(`Failed to launch target: ${errorMessage(err)}`);
|
|
142
|
+
}
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
target: trimmed,
|
|
145
|
+
kind: stats.isDirectory() ? "directory" : "file",
|
|
146
|
+
resolved,
|
|
147
|
+
dispatched: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function errorMessage(err) {
|
|
151
|
+
return err instanceof Error ? err.message : String(err);
|
|
152
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { GetFileInfoArgsSchema } from "./file_info/schema.js";
|
|
|
25
25
|
import { EditFilesArgsSchema } from "./edit_files/schema.js";
|
|
26
26
|
import { FuzzyFindFilesArgsSchema } from "./fuzzy_find_files/schema.js";
|
|
27
27
|
import { DeleteFilesByPatternArgsSchema } from "./delete_files_by_pattern/schema.js";
|
|
28
|
+
import { LaunchFileArgsSchema } from "./launch_file/schema.js";
|
|
28
29
|
import { ShellRunArgsSchema } from "./shell/schema.js";
|
|
29
30
|
import { ShellTestArgsSchema } from "./shell/schema.js";
|
|
30
31
|
import { ShellStartArgsSchema } from "./shell/schema.js";
|
|
@@ -56,6 +57,7 @@ import { handleGetFileInfo } from "./file_info/handler.js";
|
|
|
56
57
|
import { handleEditFiles } from "./edit_files/handler.js";
|
|
57
58
|
import { handleFuzzyFindFiles } from "./fuzzy_find_files/handler.js";
|
|
58
59
|
import { handleDeleteFilesByPattern } from "./delete_files_by_pattern/handler.js";
|
|
60
|
+
import { handleLaunchFile } from "./launch_file/handler.js";
|
|
59
61
|
import { handleShellRun } from "./shell/handler.js";
|
|
60
62
|
import { handleShellTest } from "./shell/handler.js";
|
|
61
63
|
import { handleShellStart } from "./shell/handler.js";
|
|
@@ -72,7 +74,7 @@ export class FilesystemServer {
|
|
|
72
74
|
this.allowedDirectories = allowedDirectories;
|
|
73
75
|
this.server = new Server({
|
|
74
76
|
name: "secure-filesystem-server",
|
|
75
|
-
version: "0.2.
|
|
77
|
+
version: "0.2.7",
|
|
76
78
|
}, {
|
|
77
79
|
capabilities: {
|
|
78
80
|
tools: {},
|
|
@@ -354,6 +356,14 @@ export class FilesystemServer {
|
|
|
354
356
|
"The default starts as the MCP server process's CWD and resets when the server restarts.",
|
|
355
357
|
inputSchema: zodToJsonSchema(ShellCwdArgsSchema),
|
|
356
358
|
},
|
|
359
|
+
{
|
|
360
|
+
name: "launch_file",
|
|
361
|
+
description: "Launch a file, folder, or web link in its Windows-associated application (the OS default: e.g. VLC for .mkv/.mp4, Explorer for folders, the default browser for http(s) URLs). " +
|
|
362
|
+
"File/folder paths must be within the allowed directories (C:/, D:/, M:, NAS UNC) and are checked for existence; only http:// and https:// links are launched - file: URLs and other protocol schemes (javascript:, ms-*, steam:, vlc:) are rejected. " +
|
|
363
|
+
"The launch is fully detached: it returns once Windows has spawned the shell opener - it never waits for the opened application, does not inherit the server's stdio pipes, creates no intermediary console window, and passes file names verbatim (spaces, Unicode, apostrophes, ampersands, parentheses all safe). " +
|
|
364
|
+
"Errors if the path does not exist, is outside the allowed directories, the URL scheme is not http/https, or the opener fails to spawn.",
|
|
365
|
+
inputSchema: zodToJsonSchema(LaunchFileArgsSchema),
|
|
366
|
+
},
|
|
357
367
|
],
|
|
358
368
|
};
|
|
359
369
|
});
|
|
@@ -676,6 +686,16 @@ export class FilesystemServer {
|
|
|
676
686
|
content: [{ type: "text", text: result }],
|
|
677
687
|
};
|
|
678
688
|
}
|
|
689
|
+
case "launch_file": {
|
|
690
|
+
const parsed = LaunchFileArgsSchema.safeParse(args);
|
|
691
|
+
if (!parsed.success) {
|
|
692
|
+
throw new Error(`Invalid arguments for launch_file: ${parsed.error}`);
|
|
693
|
+
}
|
|
694
|
+
const result = await handleLaunchFile(parsed.data.path, this.allowedDirectories);
|
|
695
|
+
return {
|
|
696
|
+
content: [{ type: "text", text: result }],
|
|
697
|
+
};
|
|
698
|
+
}
|
|
679
699
|
default:
|
|
680
700
|
throw new Error(`Unknown tool: ${name}`);
|
|
681
701
|
}
|
package/package.json
CHANGED