CCD/Cowork requires system Node.js for local MCP extensions while Chat uses built-in UtilityProcess

Resolved 💬 2 comments Opened Mar 17, 2026 by ssalzman-kobold Closed Apr 15, 2026

Bug Description

Local MCP extensions that use Node.js (e.g., server.type: "node", command: "node") work in Chat but fail silently in CCD (Claude Code Desktop) and Cowork on machines without a system Node.js installation.

Root Cause

The Chat path and CCD path use completely different mechanisms to spawn Node.js MCP servers:

Chat path (pDe) — uses Electron's built-in UtilityProcess:

const u = await wNe(t);
if (u.type === "built-in-node") {
    l = new zMt(h, g, {...i.env});
    // zMt uses xe.utilityProcess.fork(nodeHostPath, ...)
    // nodeHostPath = "app.asar/.vite/build/mcp-runtime/nodeHost.js"
    // env: {...process.env, ...this.env} — full environment
    // No system node binary needed — runs JS inside Electron's Node.js runtime
}

CCD path (LocalMcpServerManager.createConnection) — always uses StdioClientTransport (bNe):

const {cmd, args, cwd} = await iDe(e, s);
// iDe() calls wNe() which returns {type: "built-in-node"}
// But then iDe() just calls Go("node", args, pathDirs) — searching for system "node" binary
// The "built-in-node" result is effectively ignored
const f = new bNe({command: l.cmd, args: l.args, env: ...});
// Spawns via child_process.spawn() — requires real "node" on PATH

The CCD path calls wNe() and correctly identifies the extension as "built-in-node", but then iDe() resolves "node" as a system command via Go() anyway. The "built-in-node" type only triggers the UtilityProcess branch in the Chat path — the CCD path has no such branch.

Additionally, the CCD path uses a stripped environment ($Z() — only HOME, LOGNAME, PATH, SHELL, TERM, USER on macOS) vs Chat's full process.env.

Compatibility Checker Says "All Good" Even When CCD Will Fail

The extension compatibility checker (NPt) evaluates whether an extension's node requirements are met. For extensions where command === "node", it checks if builtInNodeVersion (Electron's embedded Node.js) satisfies the version requirement. If yes, it returns null — no compatibility issue. This means extensions appear fully compatible in the UI even on machines without system Node.js, because the checker correctly knows Electron has built-in Node. But CCD never actually uses the built-in Node — it spawns node as a system command. So the compatibility checker is giving a green light for a configuration that will silently fail in CCD/Cowork.

function NPt(t, e) {
    const r = t.server.mcp_config.command;
    const {builtInNodeVersion: n, nodeVersions: i} = e;
    const s = t.compatibility?.runtimes?.node;
    // ...
    if (n) { // builtInNodeVersion always exists in Electron
        const c = r && /^node(\.exe)?$/.test(r); // command is "node"
        const l = !s || $a.satisfies(n, s); // built-in node meets version req
        return l ? null : "builtin-node-version-mismatch";
        // Returns null (no issue) — but CCD doesn't use built-in node!
    }
}

Impact

  • Affects all users without system Node.js installed (common for non-developer users)
  • Chat tab works perfectly for these users (UtilityProcess needs no system node)
  • CCD shows local: 0 plugins and MCP connections fail with MCP error -32000: Connection closed
  • The failure is silent from the user's perspective — tools just don't appear in CCD/Cowork
  • The extension compatibility UI shows no warnings, since it checks built-in node (which CCD doesn't use)

Reproduction

  1. Install a Node.js-based MCP extension (any extension with server.type: "node")
  2. Verify it works in Chat tab
  3. Uninstall system Node.js (brew uninstall node, remove nvm, etc.)
  4. Restart Claude Desktop
  5. Chat tab still works (UtilityProcess)
  6. CCD/Cowork fails — extension tools are missing, main.log shows connection failure

Suggested Fix

Have LocalMcpServerManager.createConnection() use UtilityProcess (via zMt) for extensions where wNe() returns {type: "built-in-node"}, matching the Chat path's behavior. The nodeHost.js and zMt class already exist and work — they just need to be used in the CCD path as well.

Environment

  • Claude Desktop on macOS (likely affects Windows too if system node isn't installed)
  • Tested with Node.js MCP extension (JupyterHub MCP, server.type: "node")
  • Multiple affected users confirmed across an organization

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗