Measurement
On a 48 GiB workstation running ~35 concurrent Claude Code sessions:
unified-mcp-server.mjs : 259 processes, 10.45 GiB (mean 41 MiB)
That is one process per configured provider slot, per session — 7 slots × ~35 sessions. Every one of them runs the identical script. The machine was at 0.0 GiB free with swap 93% full, and MCP servers were the single largest consumer on it — larger than the Claude sessions they serve (12.3 GiB) and larger than everything else combined.
This is duplication, not capability: 259 copies of one server, not 259 different tools.
Root cause
~/.claude.json configures one MCP entry per provider, each setting PROVIDER_SLOT:
"claude-1": { "command": "node", "args": ["…/unified-mcp-server.mjs"],
"env": { "PROVIDER_SLOT": "claude-1", … } },
"codex-1": { …"PROVIDER_SLOT": "codex-1"… },
"copilot-1": { …"PROVIDER_SLOT": "copilot-1"… },
… 7 total
MCP stdio spawns a child process per client, so this multiplies by session count.
Notably this contradicts the documented design. docs/dev/requirements-coverage.md:1842:
The unified MCP server (bin/unified-mcp-server.mjs) is the single entrypoint for all 11 MCP provider servers.
The server already implements that — PROVIDER_SLOT unset exposes all providers as slot-named tools. The slot-per-entry configuration is a deviation from the intended architecture.
Mitigation available today (no code change)
Replacing the 7 slot entries with a single entry that omits PROVIDER_SLOT:
259 processes, 10.45 GiB → ~35 processes, ~1.4 GiB (~9 GiB recovered)
Verified working against the installed build:
initialize → {"name":"unified-mcp-server","version":"1.0.0"}
tools/list → claude-1 codex-1 copilot-1 antigravity-1 claude-z-ai
claude-minimax claude-kimi gemini-1 opencode-1 kimi-1
Nothing is lost, and three providers that were not previously exposed (gemini-1, opencode-1, kimi-1) become available. Costs: tool names change from mcp__claude-1__claude to mcp__unified-1__claude-1, and the per-slot health_check/help/identity/ping tools collapse (42 tools → 10).
Two things make this safe, both verified in the source rather than assumed:
- Every spawn builds its own env —
const env = { ...process.env, ...resolveEnvPlaceholders(provider.env ?? {}) } (bin/unified-mcp-server.mjs:415). Credentials are resolved per provider at call time.
- The global
process.env mutation that would race under consolidation is guarded by if (SLOT …) (:1001, :1025) and never runs in unified mode.
Proposed fix: an HTTP transport, so the server is a true machine-wide singleton
Unified mode still costs one process per session. An HTTP transport makes it one process per machine, and — the real point — flat regardless of session count:
| sessions |
slot mode |
unified stdio |
HTTP singleton |
| 35 |
10.4 GiB |
1.4 GiB |
~41 MiB |
| 100 |
30 GiB |
4 GiB |
~41 MiB |
The transport surface is already small enough to make this contained:
handleRequest(req) (:909) is the single dispatcher.
send() (:110) is the single response sink; sendResult/sendError wrap it, and all 15 response call sites go through those three functions.
- The stdio reader (
:1051-1066) only parses lines and calls handleRequest.
Sketch:
- Make the response sink per-request via
AsyncLocalStorage, so send() resolves the current sink and falls back to stdout. No change to the 15 call sites or to handleRequest's signature.
- Add an HTTP listener behind an env flag (e.g.
MCP_HTTP_PORT), keeping stdio as the default so existing installs are untouched.
- Match the streamable-HTTP MCP profile Claude Code already speaks — it connects to HTTP MCP servers with
{"type":"http","url":…}, SSE-framed replies and mcp-session-id.
- Daemon lifecycle: start-on-demand with a lockfile or a user agent, plus restart handling. This is the part that needs the most care — a singleton becomes a single point of failure for every session on the machine.
Per-call work is already stateless (spawn per request, scoped env), so concurrency across sessions does not require a state redesign.
Suggested sequencing
The config mitigation is independent of the transport work and recovers ~87% of the memory immediately. The HTTP singleton is worth doing on its own merits because it stays flat as session count grows, but it is a feature with protocol and lifecycle surface, not a quick patch.
Measurement
On a 48 GiB workstation running ~35 concurrent Claude Code sessions:
That is one process per configured provider slot, per session — 7 slots × ~35 sessions. Every one of them runs the identical script. The machine was at 0.0 GiB free with swap 93% full, and MCP servers were the single largest consumer on it — larger than the Claude sessions they serve (12.3 GiB) and larger than everything else combined.
This is duplication, not capability: 259 copies of one server, not 259 different tools.
Root cause
~/.claude.jsonconfigures one MCP entry per provider, each settingPROVIDER_SLOT:MCP stdio spawns a child process per client, so this multiplies by session count.
Notably this contradicts the documented design.
docs/dev/requirements-coverage.md:1842:The server already implements that —
PROVIDER_SLOTunset exposes all providers as slot-named tools. The slot-per-entry configuration is a deviation from the intended architecture.Mitigation available today (no code change)
Replacing the 7 slot entries with a single entry that omits
PROVIDER_SLOT:Verified working against the installed build:
Nothing is lost, and three providers that were not previously exposed (
gemini-1,opencode-1,kimi-1) become available. Costs: tool names change frommcp__claude-1__claudetomcp__unified-1__claude-1, and the per-slothealth_check/help/identity/pingtools collapse (42 tools → 10).Two things make this safe, both verified in the source rather than assumed:
const env = { ...process.env, ...resolveEnvPlaceholders(provider.env ?? {}) }(bin/unified-mcp-server.mjs:415). Credentials are resolved per provider at call time.process.envmutation that would race under consolidation is guarded byif (SLOT …)(:1001,:1025) and never runs in unified mode.Proposed fix: an HTTP transport, so the server is a true machine-wide singleton
Unified mode still costs one process per session. An HTTP transport makes it one process per machine, and — the real point — flat regardless of session count:
The transport surface is already small enough to make this contained:
handleRequest(req)(:909) is the single dispatcher.send()(:110) is the single response sink;sendResult/sendErrorwrap it, and all 15 response call sites go through those three functions.:1051-1066) only parses lines and callshandleRequest.Sketch:
AsyncLocalStorage, sosend()resolves the current sink and falls back to stdout. No change to the 15 call sites or tohandleRequest's signature.MCP_HTTP_PORT), keeping stdio as the default so existing installs are untouched.{"type":"http","url":…}, SSE-framed replies andmcp-session-id.Per-call work is already stateless (
spawnper request, scoped env), so concurrency across sessions does not require a state redesign.Suggested sequencing
The config mitigation is independent of the transport work and recovers ~87% of the memory immediately. The HTTP singleton is worth doing on its own merits because it stays flat as session count grows, but it is a feature with protocol and lifecycle surface, not a quick patch.