CC spawns project .mcp.json stdio server twice per launch (t+0 instance outside client-manager accounting) — single-connection backends lose their tools client
Summary
As of 2026-08-11, on one of our Linux boxes, every fresh interactive claude launch spawns the project .mcp.json stdio MCP server TWICE from the same claude process — one instance immediately at launch (t+0s, outside the MCP client manager's accounting: it never appears in ~/.cache/claude-cli-nodejs/<cwd>/mcp-logs-<server>/), and one ~2s later via the normal client manager (which does log). Our stdio server's backend enforces one connection per client identity, so the second (legitimate) connection is rejected and the session comes up with zero tools (Failed to fetch tools: Not connected). The early instance's connection then drops ~15s after boot.
The same binaries booted cleanly (single server instance) on the same box one day earlier, with no binary, config, plugin, or hook change in between (all verified by mtime/content) — which makes us suspect a server-side feature-gate rollout changed startup behavior.
Hard evidence (process lineage, captured at 250ms resolution during boot)
claude 3226325 (18:52:13)
├─ npm exec tsx …/mcp/index.ts 3226389 (18:52:13 — same second as claude itself)
│ └─ sh → node tsx → node 3226678 ← connects to backend 18:52:15.5 (NOT in mcp-logs)
└─ npm exec tsx …/mcp/index.ts 3226699 (18:52:15 — the client-manager spawn)
└─ sh → node tsx → node 3226778 ← connects 18:52:16.5 (IS in mcp-logs; loses to the first)
The early instance's intermediate npm/sh wrappers exit within ~5s (orphaning its node process to systemd --user); its connection drops at ~t+15s.
A/B matrix (all reproduce, 7/7 boots)
| CC version | --dangerously-load-development-channels flag | cwd | result |
|---|---|---|---|
| 2.1.227 | yes | fresh scratch dir, only .mcp.json | dup spawn |
| 2.1.226 | yes | same | dup spawn |
| 2.1.227 | no | same | dup spawn |
| 2.1.226 | no | same, pre-trusted | dup spawn |
- Same
.mcp.jsoncontent booted clean at 2026-08-10 21:24 (single instance). - Ruled out on-disk: MCP registration exists in exactly ONE scope (project
.mcp.json; user/managed/local scopes verified empty of this server); plugins unchanged (mtimes); hooks unchanged;~/.codex/config.tomlunchanged; npx cache unchanged since before the last clean boot (tsx 4.23.12). LocalMcpServerManager(from #53134): zero hits in mcp-logs and zero in the 2.1.227 binary's strings.- Not the dual-scope bug (#66474): single registration.
Client-manager log for the losing (legitimate) connection
"Starting connection with timeout of 30000ms"
"Server stderr: MCP ready..." <- server up
"Successfully connected (transport: stdio) in 2349ms"
"STDIO connection closed after 0s (cleanly)" <- backend rejected as duplicate
"tools/list failed (Connection closed); retrying in 250ms"
"Failed to fetch tools: Not connected"
Environment
- claude 2.1.226 and 2.1.227 (native installer, Linux x86_64, Ubuntu 22.04 / kernel 5.15)
.mcp.json: single stdio server,command: npx,args: ["tsx", "<path>/index.ts"]- Enterprise (Claude Enterprise) org
- Interactive TTY sessions inside tmux; also reproduces outside our normal launch wrapper
Questions
- What is the t+0 spawn? (It precedes the client manager by ~2s and is invisible to mcp-logs.) A prewarmed subagent, preflight probe, or a newly-gated second MCP consumer?
- Is there a supported way to disable it, or to make internal consumers share the client manager's connection?
- If this is a staged rollout, can the cohort be identified so affected single-connection stdio backends can prepare?
Happy to provide more forensics (we have subsecond process captures and both sides' logs for 8 boots).
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Thanks for the excellent repro. On 2.1.233 (Linux, tmux, spawn-logging stdio server) with default settings I get exactly one spawn per launch in all three of your scenarios (interactive project
.mcp.json,claude -p, user scope), with the server connected and its tool available.I could, however, confirm your report: Claude Code has a staged rollout of MCP protocol-version negotiation, and when it's active for a session the CLI briefly starts the stdio server once to probe which protocol revision it speaks, then starts the real session instance after the probe settles. The probe instance is short-lived and doesn't get its own
mcp-logsdirectory — with that mode forced on 2.1.233 I see exactly what you logged: two spawns from the same claude process, the first exiting, the second serving the session.For most servers this is invisible, but for a backend enforcing one connection per client identity the probe's registration can cause the real session connection to be rejected — the "zero tools" state you hit. Confirmed, and we're treating that interaction as a bug in the rollout. Because it's gated server-side, it can appear on one machine/account and not another with no client update, and the rollout is being held on evidence like this. If you still see double spawns on the latest version, please comment with the version number.
🤖 Generated with Claude Code
Confirming a closely related reproduction on Windows Claude Desktop.
Environment
C:\Users\mattt\AppData\Local\AnthropicClaude\app-1.34493.1\claude.exe
%APPDATA%\Claude\claude_desktop_config.json
"C:\Program Files\nodejs\node.exe" C:/Users/mattt/Working/easyeda-mcp-pro/dist/index.js
Reproduction
A single configured stdio MCP server is launched twice by the same Claude Desktop process.
Claude Desktop process:
PID: 28228
Created: 2026-08-21 12:32:54 PM
Executable:
C:\Users\mattt\AppData\Local\AnthropicClaude\app-1.34493.1\claude.exe
First MCP child:
PID: 11644
Parent PID: 28228
Created: 2026-08-21 12:32:56 PM
Command:
"C:\Program Files\nodejs\node.exe" C:/Users/mattt/Working/easyeda-mcp-pro/dist/index.js
Second MCP child:
PID: 3356
Parent PID: 28228
Created: 2026-08-21 12:32:56 PM
Command:
"C:\Program Files\nodejs\node.exe" C:/Users/mattt/Working/easyeda-mcp-pro/dist/index.js
Both children therefore have:
Log evidence
At 2026-08-21 12:32:56 PM, main.log records both of these paths:
"Launching MCP Server: easyeda-mcp-pro"
and:
"[LocalMcpServerManager] Connecting to easyeda-mcp-pro"
The LocalMcpServerManager then reports:
"[LocalMcpServerManager] Connected to easyeda-mcp-pro (117 tools)"
Only PID 11644 appears in mcp-server-easyeda-mcp-pro.log. It opens bridge WebSocket port 49621 and completes the initialize and tools/list handshake.
PID 3356 remains running but does not appear in that MCP server log. This suggests that one instance is outside LocalMcpServerManager's normal logging/accounting.
Configuration checks
I found the MCP in two possible locations initially:
I disabled the repository .mcp.json, fully terminated Claude Desktop and all existing MCP processes, restarted Claude, and reproduced the same two-process launch.
The duplication therefore persists with only the Claude Desktop configuration active.
Impact
The MCP connects to a local EasyEDA bridge/backend. Both server processes attempt to provide the same integration, but the backend connection can be claimed by the MCP instance that Claude is not actively using. Claude then communicates with the other instance
Same failure, different platform: Windows, Claude Desktop 1.40609.0.0, stdio server bridging to a remote HTTP MCP endpoint via
mcp-remote.The double-spawn alone doesn't explain the outage; the callback port does.
mcp-remotederives its local OAuth-callback listener port deterministically from the server URL, so every simultaneous copy (main chat, Cowork, Code session) binds the identical port. Three-way collision on a single port:logged right after
Couldn't start for Cowork and Code sessions. Error: Request timed out.This pattern recurs across two weeks of logs, always on the same fixed derived port for a given server. Whoever wins the local bind race gets the connection; everyone else times out.Consistent with the server-side rollout gating already described here, and it adds a second, deterministic failure mode once the race is lost. The fix isn't just sequencing the spawns, it's giving each spawned instance its own callback port instead of deriving one from the URL and letting concurrent copies fight over it.