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

    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registryarea:platform-windowsWindows-specific: PowerShell, cmd, Git Bash, WSL, Windows Terminal

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions