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 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
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export const LaunchFileArgsSchema = z.object({
3
+ path: z.string(),
4
+ });
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.4",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-fs-shell-windows",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "MCP filesystem server: batched file tools + standalone Windows shell tools (extended fork)",
5
5
  "license": "MIT",
6
6
  "author": "Gerard DeLuca",