fix(mcp): corrected Windows cmd shim spawning
Passed batch commands and arguments separately to cmd.exe /c so cmd.exe no longer strips a wrapper quote into the command token. Fixes #5696
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Windows stdio MCP servers launched through `.cmd` shims failing with `Transport closed`; `cmd.exe /c` now receives the command and arguments as separate spawn arguments instead of a `/s /c` string with a quoted command token ([#5696](https://github.com/can1357/oh-my-pi/issues/5696)).
|
||||
|
||||
## [17.0.1] - 2026-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -203,23 +203,6 @@ async function resolveWindowsNpmShimCommand(
|
||||
};
|
||||
}
|
||||
|
||||
function quoteCmdArg(value: string): string {
|
||||
if (value.length === 0) return '""';
|
||||
let result = '"';
|
||||
for (const char of value) {
|
||||
if (char === '"') {
|
||||
result += '^"';
|
||||
} else if (char === "^") {
|
||||
result += "^^";
|
||||
} else if (char === "%") {
|
||||
result += "^%";
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return `${result}"`;
|
||||
}
|
||||
|
||||
function isWindowsBatchCommand(command: string): boolean {
|
||||
return WINDOWS_BATCH_EXTENSIONS.has(path.extname(command).toLowerCase());
|
||||
}
|
||||
@@ -229,12 +212,6 @@ function resolveComSpec(env: Record<string, string | undefined>): string {
|
||||
return comspec && comspec.length > 0 ? comspec : "cmd.exe";
|
||||
}
|
||||
|
||||
/** `cmd /s /c` strips one outer quote pair; keep inner argv quotes intact. */
|
||||
function buildCmdExeCommand(command: string, args: readonly string[]): string {
|
||||
const quotedCommand = [command, ...args].map(quoteCmdArg).join(" ");
|
||||
return `"${quotedCommand}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the subprocess argv used to launch an MCP stdio server.
|
||||
*
|
||||
@@ -271,7 +248,7 @@ export async function resolveStdioSpawnCommand(
|
||||
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide, detached };
|
||||
|
||||
return {
|
||||
cmd: [resolveComSpec(options.env), "/d", "/s", "/c", buildCmdExeCommand(resolvedCommand, args)],
|
||||
cmd: [resolveComSpec(options.env), "/d", "/c", resolvedCommand, ...args],
|
||||
windowsHide,
|
||||
detached,
|
||||
};
|
||||
|
||||
@@ -25,13 +25,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""${shim}" "serve" "--mcp""`,
|
||||
]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", shim, "serve", "--mcp"]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -61,7 +55,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", `""${localShim}" "serve""`]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", localShim, "serve"]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -112,7 +106,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", `""${shim}" "-y" "mcp-gdb""`]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", shim, "-y", "mcp-gdb"]);
|
||||
expect(result.windowsHide).toBe(false);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -206,7 +200,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", `""${shim}" "serve""`]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", shim, "serve"]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -214,7 +208,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("escapes percent-delimited args before routing .cmd shims through cmd.exe", async () => {
|
||||
it("preserves percent-delimited args when routing .cmd shims through cmd.exe", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-percent-"));
|
||||
try {
|
||||
const shim = path.join(tempDir, "codegraph.cmd");
|
||||
@@ -236,9 +230,11 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""${shim}" "serve" "--header" "Authorization=^%TOKEN^%""`,
|
||||
shim,
|
||||
"serve",
|
||||
"--header",
|
||||
"Authorization=%TOKEN%",
|
||||
]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
@@ -247,7 +243,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("escapes quoted JSON args before routing .cmd shims through cmd.exe", async () => {
|
||||
it("preserves quoted JSON args when routing .cmd shims through cmd.exe", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-quotes-"));
|
||||
try {
|
||||
const shim = path.join(tempDir, "codegraph.cmd");
|
||||
@@ -266,13 +262,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""${shim}" "--config" "{^"a^":^"b&c|d^"}""`,
|
||||
]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", shim, "--config", '{"a":"b&c|d"}']);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -307,13 +297,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""${shim}" "serve" "--mcp""`,
|
||||
]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", shim, "serve", "--mcp"]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
} finally {
|
||||
@@ -321,7 +305,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("wraps explicit Windows .cmd commands with cmd.exe while preserving quoted argv", async () => {
|
||||
it("passes explicit Windows .cmd commands and argv separately to cmd.exe", async () => {
|
||||
const result = await resolveStdioSpawnCommand(
|
||||
{ type: "stdio", command: "codegraph.cmd", args: ["serve", "--mcp"] },
|
||||
{
|
||||
@@ -335,13 +319,7 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""codegraph.cmd" "serve" "--mcp""`,
|
||||
]);
|
||||
expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/c", "codegraph.cmd", "serve", "--mcp"]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
});
|
||||
@@ -371,9 +349,10 @@ describe("resolveStdioSpawnCommand", () => {
|
||||
expect(result.cmd).toEqual([
|
||||
"C:\\Windows\\System32\\cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`""npx" "-y" "cloakbrowser-mcp@latest""`,
|
||||
"npx",
|
||||
"-y",
|
||||
"cloakbrowser-mcp@latest",
|
||||
]);
|
||||
expect(result.windowsHide).toBe(true);
|
||||
expect(result.detached).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user