Summary
On Windows, all stdio MCP servers whose command is npx (or any other .cmd / .ps1 / extensionless shell-script launcher on PATH) fail to start in Copilot CLI 1.0.56-1. The same configuration worked in 1.0.51.
Failure modes observed by reproducing what Copilot CLI does under the hood:
| Spawn form Copilot CLI may be using |
Result on Windows + Node ≥ 20.12.2 |
spawn("npx", [...]) |
Error: spawn npx ENOENT |
spawn("npx.cmd", [...]) |
Error: spawn EINVAL |
spawn("npx.cmd", [...], { shell: true }) |
✅ works |
spawn("cmd", ["/c", "npx", ...]) |
✅ works |
EINVAL is by design — it's the Node mitigation for CVE‑2024‑27980, which blocks direct spawning of .bat/.cmd files unless shell: true is passed.
Environment
- Copilot CLI: 1.0.56-1 (also reproduces with
1.0.56-0 in the local pkg cache)
- Last known-good Copilot CLI version: 1.0.51
- OS: Windows 11 (Windows_NT, x64)
- Node: v24.12.0
- npm / npx: 11.6.2
where npx →
C:\Program Files\nodejs\npx
C:\Program Files\nodejs\npx.cmd
Affected MCP servers in my setup
All use npx -y ...:
@azure-devops/mcp <tenant>
workiq → @microsoft/workiq mcp
kusto → @azure/mcp@latest server start
HTTP-transport and uv-based servers in the same workspace (microsoft-learn, bluebird, godot-api-docs, etc.) are unaffected.
Reproduction
Reduces to a plain child_process.spawn test — no MCP server install needed:
// test-spawn-matrix.js
const { spawn } = require("child_process");
const cases = [
{ label: "spawn('npx', ...)", cmd: "npx", args: ["--version"], opts: {} },
{ label: "spawn('npx.cmd', ...)", cmd: "npx.cmd", args: ["--version"], opts: {} },
{ label: "spawn('npx.cmd', ..., {shell:true})", cmd: "npx.cmd", args: ["--version"], opts: { shell: true } },
{ label: "spawn('cmd', ['/c','npx',...])", cmd: "cmd", args: ["/c", "npx", "--version"], opts: {} },
];
(async () => {
for (const c of cases) {
await new Promise(res => {
try {
const p = spawn(c.cmd, c.args, { ...c.opts, stdio: ["ignore", "pipe", "pipe"] });
let out = "";
p.stdout.on("data", d => out += d);
p.on("error", e => { console.log(c.label.padEnd(46), "ERROR:", e.code, e.message); res(); });
p.on("exit", code => { console.log(c.label.padEnd(46), "exit=" + code, "stdout=" + out.trim()); res(); });
} catch (e) {
console.log(c.label.padEnd(46), "THROW:", e.code, e.message);
res();
}
});
}
})();
Output on this machine:
spawn('npx', ...) ERROR: ENOENT spawn npx ENOENT
spawn('npx.cmd', ...) THROW: EINVAL spawn EINVAL
spawn('npx.cmd', ..., {shell:true}) exit=0 stdout=11.6.2
spawn('cmd', ['/c','npx',...]) exit=0 stdout=11.6.2
Reproduction inside Copilot CLI
~/.copilot/mcp-config.json:
{
"mcpServers": {
"ado-microsoft": {
"type": "local",
"command": "npx",
"args": ["-y", "@azure-devops/mcp", "microsoft"],
"tools": ["*"]
}
}
}
Start Copilot CLI → the ado-microsoft server fails to start; tool list is empty.
Workaround
Wrap every npx (or other .cmd) command with cmd /c:
{
"mcpServers": {
"ado-microsoft": {
"type": "local",
"command": "cmd",
"args": ["/c", "npx", "-y", "@azure-devops/mcp", "microsoft"],
"tools": ["*"]
}
}
}
After this change, copilot mcp get ado-microsoft reports Command: cmd /c npx -y @azure-devops/mcp microsoft and the server starts correctly.
Suggested fix
In the StdioClientTransport (or equivalent) spawn path, on process.platform === "win32":
- Easy: always pass
{ shell: true } to spawn. Caveat: requires escaping args since CVE‑2024‑27980 mitigation no longer applies.
- Better: use
cross-spawn, which already wraps .cmd/.bat invocations via cmd /c and handles arg-escaping. This is what most cross-platform Node CLIs use.
- Manual: detect when
command resolves to .cmd/.bat/.ps1 (or has no extension and matches a .cmd/.bat on PATHEXT) and rewrite the spawn as spawn("cmd", ["/c", command, ...args]) with proper quoting.
VS Code's MCP client launches the same npx-based servers from the same workspace .vscode/mcp.json without issue, which strongly suggests it uses one of these strategies.
Related (not duplicates)
References
Summary
On Windows, all stdio MCP servers whose
commandisnpx(or any other.cmd/.ps1/ extensionless shell-script launcher onPATH) fail to start in Copilot CLI 1.0.56-1. The same configuration worked in 1.0.51.Failure modes observed by reproducing what Copilot CLI does under the hood:
spawn("npx", [...])Error: spawn npx ENOENTspawn("npx.cmd", [...])Error: spawn EINVALspawn("npx.cmd", [...], { shell: true })spawn("cmd", ["/c", "npx", ...])EINVALis by design — it's the Node mitigation for CVE‑2024‑27980, which blocks direct spawning of.bat/.cmdfiles unlessshell: trueis passed.Environment
1.0.56-0in the local pkg cache)where npx→Affected MCP servers in my setup
All use
npx -y ...:@azure-devops/mcp <tenant>workiq→@microsoft/workiq mcpkusto→@azure/mcp@latest server startHTTP-transport and
uv-based servers in the same workspace (microsoft-learn,bluebird,godot-api-docs, etc.) are unaffected.Reproduction
Reduces to a plain
child_process.spawntest — no MCP server install needed:Output on this machine:
Reproduction inside Copilot CLI
~/.copilot/mcp-config.json:{ "mcpServers": { "ado-microsoft": { "type": "local", "command": "npx", "args": ["-y", "@azure-devops/mcp", "microsoft"], "tools": ["*"] } } }Start Copilot CLI → the
ado-microsoftserver fails to start; tool list is empty.Workaround
Wrap every
npx(or other.cmd) command withcmd /c:{ "mcpServers": { "ado-microsoft": { "type": "local", "command": "cmd", "args": ["/c", "npx", "-y", "@azure-devops/mcp", "microsoft"], "tools": ["*"] } } }After this change,
copilot mcp get ado-microsoftreportsCommand: cmd /c npx -y @azure-devops/mcp microsoftand the server starts correctly.Suggested fix
In the StdioClientTransport (or equivalent) spawn path, on
process.platform === "win32":{ shell: true }tospawn. Caveat: requires escapingargssince CVE‑2024‑27980 mitigation no longer applies.cross-spawn, which already wraps.cmd/.batinvocations viacmd /cand handles arg-escaping. This is what most cross-platform Node CLIs use.commandresolves to.cmd/.bat/.ps1(or has no extension and matches a.cmd/.batonPATHEXT) and rewrite the spawn asspawn("cmd", ["/c", command, ...args])with proper quoting.VS Code's MCP client launches the same npx-based servers from the same workspace
.vscode/mcp.jsonwithout issue, which strongly suggests it uses one of these strategies.Related (not duplicates)
session.disconnect()does not kill stdio MCP server processes spawned for that session #3440 — different MCP failure modesReferences
cross-spawn: https://github.com/moxystudio/node-cross-spawn