diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 26a7a32..d2ef1c8 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -425,9 +425,11 @@ export function createMcpSpawnSpec( if (platform === "win32") { return { // On Windows, shell: true lets cmd.exe resolve the command via PATHEXT - // (npx -> npx.cmd, etc.). Pass one quoted command line with no spawn - // args to avoid Node 24 DEP0190. - command: [command, ...args].map(quoteWindowsShellArg).join(" "), + // (npx -> npx.cmd, etc.). Join command and args into a single string + // with empty spawn args to avoid Node 24 DEP0190. + // Only quote arguments that contain spaces or double-quotes to prevent + // double-wrapping by Node.js's own shell quoting. + command: [command, ...args].map(quoteWindowsArgIfNeeded).join(" "), args: [], shell: true, windowsHide: true, @@ -441,6 +443,9 @@ export function createMcpSpawnSpec( }; } -function quoteWindowsShellArg(arg: string): string { - return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; +function quoteWindowsArgIfNeeded(arg: string): string { + if (arg.includes(" ") || arg.includes('"')) { + return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; + } + return arg; } diff --git a/src/tests/mcp-client.test.ts b/src/tests/mcp-client.test.ts index e161aad..29151d3 100644 --- a/src/tests/mcp-client.test.ts +++ b/src/tests/mcp-client.test.ts @@ -10,9 +10,9 @@ test("createMcpSpawnSpec keeps non-Windows MCP launches shell-free", () => { }); }); -test("createMcpSpawnSpec avoids Windows shell args for Node 24", () => { +test("createMcpSpawnSpec joins args without quoting when spaces are absent (Windows)", () => { assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "win32"), { - command: '"npx" "-y" "@playwright/mcp@latest"', + command: "npx -y @playwright/mcp@latest", args: [], shell: true, windowsHide: true,