Skip to content

Windows: stdio MCP servers fail to spawn (spawn npx ENOENT / EINVAL) in 1.0.56-1 #3576

@jamesdooleymsft

Description

@jamesdooleymsft

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":

  1. Easy: always pass { shell: true } to spawn. Caveat: requires escaping args since CVE‑2024‑27980 mitigation no longer applies.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions