Skip to content

Windows: stdio MCP servers fail with "spawn npx ENOENT" (v2 engine regression) #3236

Open

Description

@kimi-agent-bot

Summary

On Windows, stdio MCP servers whose command is a batch shim (npx, uvx, …) fail to start with spawn npx ENOENT on the v2 engine (default since 0.33.0). The v1 engine does not have this problem because it delegates spawning to the official MCP SDK, which internally uses cross-spawn.

Environment

  • Windows 11, Kimi Code CLI 0.38.0 (v2 engine, the default)
  • Node v24.18.0 via nvm4w
  • User-level ~/.kimi-code/mcp.json following the official docs:
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"]
    }
  }
}

Steps to reproduce

  1. On Windows, configure any stdio MCP server with command: "npx" as above.
  2. Start Kimi Code.
  3. The server never comes up; the log shows:
ERROR mcp server unavailable  server=playwright status=failed reason="spawn npx ENOENT"

Root cause

npx on Windows is a batch file (npx.cmd). node:child_process.spawn without shell: true goes through CreateProcess, which cannot execute batch files and does not consult PATHEXT → ENOENT. (Node ≥ 20.12 tightened batch-file spawning further after CVE-2024-27980.) This is not a Node version issue — it reproduces on Node v24.18.0.

Engine comparison (verified on main @ f4dc9ba, identical in 0.38.0):

  • v1 works: packages/agent-core/src/mcp/client-stdio.ts:65 uses the official StdioClientTransport from @modelcontextprotocol/sdk (1.29). The SDK spawns via cross-spawn, which resolves .cmd via PATHEXT and launches batch files through cmd.exe with proper escaping — the standard post-CVE-2024-27980 approach.
  • v2 fails: packages/agent-core-v2/src/mcpCore/client-stdio.ts:188 implements its own RuntimeStdioTransport over IHostProcess → HostProcessService.spawn (packages/agent-core-v2/src/os/backends/node-local/hostProcessService.ts:174), which calls raw node:child_process.spawn with shell unset → .cmd commands can never start on Windows.

Since 0.33.0 defaults to v2, this is a v2 regression relative to v1. Other clients (e.g. Claude Code) handle .cmd resolution internally; Kimi Code v2 does not.

Suggested fix

  • Minimal: in the v2 stdio transport, spawn via cross-spawn (already in the dependency tree via the MCP SDK), or resolve .cmd/.bat via PATHEXT and launch through cmd.exe with correct argument escaping.
  • Broader: swap node:child_process.spawn for cross-spawn inside HostProcessService so every local spawn (MCP, rg, git context, external hooks, workspace fs) benefits uniformly on Windows. Larger blast radius — needs regression testing.
  • Avoid shell: true as the fix (argument-escaping / injection surface).

Secondary items

  1. mcp.json is only loaded at session start; edits don't hot-apply. Note that a full exit is not required — /reload or /new picks up the changes (this is what the TUI hint already says, apps/kimi-code/src/tui/commands/plugins.ts:164). A config file watcher, or a hint when the file changes, would still help discoverability.
  2. Failure-reason visibility is already addressed in 0.38.0: /mcp renders an error: … line (including the captured stderr tail) for failed servers (apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts:139-145).

Workarounds (verified by the reporter)

Bypass npx — install globally and point command at node.exe with the absolute entry path:

npm i -g @playwright/mcp
{
  "mcpServers": {
    "playwright": {
      "command": "C:\\nvm4w\\nodejs\\node.exe",
      "args": ["C:\\nvm4w\\nodejs\\node_modules\\@playwright\\mcp\\cli.js", "…其他参数"],
      "startupTimeoutMs": 120000
    }
  }
}

Or wrap with cmd /c: "command": "cmd", "args": ["/c", "npx", "-y", "@playwright/mcp@latest", ...]. Restart (or /reload) to apply.

Original report

Forwarded user feedback (Chinese original):

【反馈】Windows 下配置 Playwright MCP 一直不生效,排查结论 + 临时解法
环境:Windows 11 + Kimi Code CLI + nvm4w(Node v24.18.0),按官方文档在用户级 ~/.kimi-code/mcp.json 配置 playwright server。
踩了三个坑:

  1. 配置只在会话启动时加载。改完 mcp.json 当前会话不会生效,必须完全退出重开(文档有写,但容易忽略,建议加载失败时在 TUI 里给个提示)。
  2. Windows 下 spawn npx ENOENT(核心问题)。日志里报错:ERROR mcp server unavailable server=playwright status=failed reason="spawn npx ENOENT"。原因:npx 本体是批处理 npx.cmd,Node 的 spawn() 不带 shell: true 时走 CreateProcess,执行不了批处理、也不会按 PATHEXT 补后缀。这不是 Node 版本问题(我用 v24.18.0 照样复现),Node 20.12+ 因 CVE-2024-27980 对批处理 spawn 更严格了。Claude Code 等客户端在 Windows 上内部处理了 .cmd 的情况,建议 Kimi Code 也跟进:Windows 下 spawn MCP server 时加 shell: true 或自行解析 .cmd。
  3. 排查入口不方便。最后是自己翻 ~/.kimi-code/logs/kimi-code.log 才看到报错,如果 /mcp 能直接显示失败原因会好很多。
    临时解法(已验证可用):绕过 npx,全局安装后用绝对路径直指 node + 入口文件,或者用 cmd /c 包装。改完重启 Kimi Code 生效。

Activity

  1. losewayy commented on Sep 26, 2026

    @losewayy

    Traced the failure path on current main — root cause confirmed in-tree, and the regression window checks out at commit level.

    packages/agent-core-v2/src/os/backends/node-local/hostProcessService.ts:190 calls spawn(command, args, spawnOptions) where spawnOptions.shell is only set when the caller passes options.shell (:29-31). The MCP stdio transport (packages/agent-core-v2/src/mcpCore/client-stdio.ts:188-192) calls runtime.process.spawn(this.config.command, this.config.args, { cwd, env }) — no shell option. On Windows, CreateProcess only auto-resolves *.exe; npx, pnpm, codegraph and other package-manager binaries on PATH are *.cmd shims, so the spawn fails ENOENT before any process exists.

    Why 0.36.x worked: both the legacy engine and earlier v2 snapshots spawned stdio via the MCP SDK's StdioClientTransport, which uses cross-spawn — the library that resolves .cmd/.bat shims and routes them through cmd.exe. 84da6629 (#2961, runtime-binding decouple) replaced it with RuntimeStdioTransport calling the bare spawn above, and that commit first shipped in @moonshot-ai/kimi-code@0.37.0 — squarely inside the reported regression window.

    Two related hazards sit in the same fix: a bare name gets ENOENT, but an explicitly-resolved .cmd/.bat gets EINVAL (Node refuses batch files without shell since the CVE-2024-27980 fix) — so the command needs both PATH/PATHEXT resolution and shell routing on win32. The repo already documents this class: apps/kimi-code/src/cli/update/preflight.ts:594-601 explains the .cmd-without-shell EINVAL, and source.ts:77,84 resolves npm.cmd explicitly via resolveCommandPath (with the note at :80-82 that resolving ahead of cmd.exe avoids picking up a CWD-planted shim).

    Proposed scope for a focused bug-fix PR, inside hostProcessService.spawn on Windows when options.shell is unset (external hooks pass shell: true with full command lines and must keep their semantics):

    • Resolve a bare command (no directory separator) against PATH/PATHEXT in PATHEXT order — .exe results spawn directly, only .cmd/.bat results route through process.env.ComSpec with argv quoted for cmd's rules — resolved against the effective child env (PATH/PATHEXT looked up case-insensitively on win32), keeping resolveCommandPath's cwd-hit refusal (same binary-plant concern the update path guards). resolveCommandPath lives in apps/kimi-code, so the logic gets reimplemented in agent-core-v2, not imported.
    • Putting it in the shared service fixes the whole class for every runtime.process.spawn consumer — MCP stdio (including plugin-shipped servers and kimi mcp probing) and ACP sessions, whose process service delegates non-terminal spawns to the same service. POSIX behavior untouched.
    • Regression coverage: extend the existing Windows describe in packages/agent-core-v2/test/os/backends/node-local/hostProcessService.test.ts with a .cmd-shim spawn case.

    Per CONTRIBUTING.md, could a maintainer /approve this scope?

  2. ConradLu2740 commented on Sep 27, 2026

    @ConradLu2740

    Additional data point: this also affects plugin-declared MCP servers, not just user-level mcp.json — users hit it with zero custom config.

    The official cloudbase plugin (v0.2.0) declares in its kimi.plugin.json:

    "mcpServers": {
      "cloudbase": {
        "command": "npx",
        "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
      }
    }

    Simply installing this plugin produces the failure on every app launch:

    ERROR mcp server unavailable  server=plugin-cloudbase:cloudbase transport=stdio status=failed reason="spawn npx ENOENT"
    

    Observed on Windows 11 (build 26200), Node v26.2.0 (on system PATH), Kimi Code desktop app. 31 occurrences over 10 days — exactly one per launch, correlated with app start times.

    Minimal repro of the underlying behavior on this machine (nodejs on PATH, no shell):

    spawn('npx', ['--version'])      -> ENOENT
    spawn('npx.cmd', ['--version'])  -> EINVAL   (Node >= 20.12 batch-file protection)
    

    Workarounds that work today:

    • Plugin side: wrap the shim in a shell, e.g. "command": "cmd.exe", "args": ["/c", "npx", ...] — this is exactly what the official kimi-cu-win plugin does (cmd.exe /c bin\kimi-cu-mcp.cmd).
    • User side: /plugins mcp disable cloudbase cloudbase (keeps the plugin's skills, drops the broken server).

    Since the official docs example itself uses "command": "npx" (https://www.kimi.com/code/docs/en/kimi-code-cli/customization/mcp.html), every Windows user following the docs hits this. Suggest fixing the v2 spawner (resolve via PATHEXT, or use cross-spawn like the v1 engine's MCP SDK, or auto-wrap batch shims in cmd /c), and/or warning at plugin-install time when a declared stdio command has no .exe on Windows.

  3. KaiOnCode commented on Oct 21, 2026

    @KaiOnCode

    我准备接这个 bug 修复,先把证据和方案贴在 issue 里,方便维护者判断。

    复现(真实 Windows 机器,Windows 10 build 19045 / Node v22.20.0,三次一致)

    spawn npx     (no shell) → error  code=ENOENT
    spawn npx.cmd (no shell) → throw code=EINVAL
    spawn cmd.exe /d /s /c npx --version → exit 0
    

    根因与 issue 一致:v2 的 stdio MCP 走 packages/agent-core-v2/src/mcpCore/client-stdio.ts 里自研的 RuntimeStdioTransport → lease.runtime.process.spawn → packages/agent-core-v2/src/os/backends/node-local/hostProcessService.ts 的原生 node:child_process.spawn;Windows 上 CreateProcess 只自动补 .exe、不查 PATHEXT,裸 npx(实际是 npx.cmd)只能 ENOENT,Node ≥ 20.12 还会直接拒绝 .cmd/.bat(EINVAL)。同一棵树上 MCP SDK 自带的 stdio client 用的就是 cross-spawn(@modelcontextprotocol/sdk 的 client/stdio.js 第一行),v2 这条自研 transport 少了这一步。

    打算怎么改

    在 HostProcessService.spawn 里加一步 Windows 专用的回退:先照旧原生 spawn;只有它以 ENOENT / EINVAL 失败(Windows 上找不到 .exe、或 Node 拒绝 .cmd/.bat)时,才按 PATHEXT 去找同名 .bat/.cmd(也支持配置里直接写 npx.cmd 或相对路径),找到就改由 cross-spawn 启动(负责 PATHEXT 与参数转义)。这样正常命令完全不受影响,也不会改跑别的可执行文件;POSIX 行为与错误语义都不变。

    顺带修一处 Windows 下的环境变量合并:{...process.env, ...覆盖} 在 Windows 上会留下 PATH 与 Path 两个同名变量(Windows 里它们其实是同一个),改成按不区分大小写合并。

    测试

    • 新增解析逻辑的用例(Linux CI 也会跑):PATH/PATHEXT 顺序、显式路径、相对 PATH 按 spawn cwd 解析、带引号条目、PATH/PATHEXT 大小写不敏感、可执行文件优先、带后缀的命令名、相对路径按 spawn cwd 解析、目标不存在等 16 条;
    • 新增两条只在真实 Windows 上跑的用例:写一个 .cmd shim,分别用显式路径和 PATH 上的裸命令名 spawn 它,断言 stdout 与退出码。

    red→green 已在真实 Windows 机器(Win10 build 19045 / Node v24.15.0 / pnpm 10.33.0)上验过:换回修复前的文件,该测试文件 24 个用例里 18 个失败(两条 Windows 集成用例分别报 spawn EINVAL 和 spawn kimi-probe ENOENT);换回修复后的文件,24 个用例全绿。补充一点:仓库目前把 Windows CI 关着(ci.yml 里 test-windows 是 if: false),所以那两条 Windows 专用用例在 CI 里不会自动跑,红绿是我在真机上跑出来的。

    PR 已开:#4309。

    @liruifengv 能否给个 /approve?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions