claude-code - 💡(How to fix) Fix [BUG] Cowork agent-mode spawns duplicate MCP server processes, breaking MCP Apps that share state between iframe and agent transports [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54513Fetched 2026-04-30 06:43:35
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
1
Timeline (top)
labeled ×5commented ×1subscribed ×1

When a Claude Desktop session uses both an MCP App (iframe served via ui://...mcp-app.html) and Cowork's local-agent-mode at the same time, Cowork spawns two independent OS processes of the same configured MCP server. The iframe's MCP transport is wired to one process; the agent's tool calls are wired to the other. Any state the server keeps in module-level memory — request queues, pending callbacks, session/view registries, auth tokens, in-flight bridges — is split-brained between the two processes.

The MCP App appears to work for stateless calls (tools/list, simple request/response tools) and silently fails for any flow that requires state to be shared between an agent-issued tool call and an iframe-issued follow-up. This is the common case for any MCP App that presents an interactive UI and lets the model drive it.

Related to but distinct from #50422 (cold-launch duplicate spawn, marked invalid). That issue is about regular MCP servers spawning twice on app launch with a ~2 minute gap. This issue is specifically about Cowork's agent-mode-on-top-of-iframe-host scenario, where both processes are spawned concurrently, one per logical transport.

Root Cause

Same viewUUID, two different processes' queues, both empty from the other's perspective. The iframe is polling correctly; the agent enqueued correctly; they are talking past each other because the host bound them to different processes.

Fix Action

Workaround

Server authors can move all shared state into an external store (UNIX socket leader/follower, SQLite, etc.) and treat the in-process MCP server as a thin frontend. This is significant extra complexity, isn't documented anywhere as a requirement, and adds latency to every iframe ↔ server round-trip.

Code Example

PID  PPID  COMMAND
4109  4106  node /path/to/dist/index.js --stdio ...
4162  4161  node /path/to/dist/index.js --stdio ...

---

02:36:00  CALL  display_document               → OK viewUUID=1806fccd-...
02:37:07  CALL  interact {viewUUID:1806fccd-...} 
02:37:32  ERR   "Viewer never connected for viewUUID 1806fccd-... (no poll within 8s).
                 The iframe likely failed to mount."

---

02:36:27  initialize / tools/list / resources/list
02:36:29  resources/read mcp-app.html
02:36:32  poll_commands {viewUUID:1806fccd-...}  ← iframe IS polling for this view
02:37:02"0 command(s)"                       ← long-poll returns empty after 30s
RAW_BUFFERClick to expand / collapse

Summary

When a Claude Desktop session uses both an MCP App (iframe served via ui://...mcp-app.html) and Cowork's local-agent-mode at the same time, Cowork spawns two independent OS processes of the same configured MCP server. The iframe's MCP transport is wired to one process; the agent's tool calls are wired to the other. Any state the server keeps in module-level memory — request queues, pending callbacks, session/view registries, auth tokens, in-flight bridges — is split-brained between the two processes.

The MCP App appears to work for stateless calls (tools/list, simple request/response tools) and silently fails for any flow that requires state to be shared between an agent-issued tool call and an iframe-issued follow-up. This is the common case for any MCP App that presents an interactive UI and lets the model drive it.

Related to but distinct from #50422 (cold-launch duplicate spawn, marked invalid). That issue is about regular MCP servers spawning twice on app launch with a ~2 minute gap. This issue is specifically about Cowork's agent-mode-on-top-of-iframe-host scenario, where both processes are spawned concurrently, one per logical transport.

Reproduction

The transport and packaging format don't matter — verified with both an .mcpb install and a direct claude_desktop_config.json entry using {"command":"node","args":["dist/index.js","--stdio"]}.

  1. Configure any MCP server that:
    • Registers a ui://...mcp-app.html resource (so it's an MCP App).
    • Holds state in module-level memory keyed off something the iframe later references — e.g. a session id returned by tool A, read back by tool B that the iframe calls.
  2. Open a Cowork project, enable local-agent-mode, ensure the MCP App's iframe is mounted in the same conversation.
  3. Have the agent call the state-mutating tool. Have the iframe (or the agent) issue a follow-up that depends on that state.
  4. The follow-up returns empty / "no such session" / times out — the state was written into a different process's memory than the one the follow-up call lands on.

Evidence the host is double-spawning

ps during a reproduction shows two concurrent processes for the same configured server, both children of the Claude helper:

PID  PPID  COMMAND
4109  4106  node /path/to/dist/index.js --stdio ...
4162  4161  node /path/to/dist/index.js --stdio ...

Each runs its own copy of the server module. Claude Desktop's mcp.log shows only one transport (the iframe's, clientInfo: "claude-ai"); the agent's transport is wired to the second process.

When the server logs its own pid alongside in-memory state mutations, the asymmetry is plain: an enqueue event logs pid A, the matching poll/dequeue logs pid B, both for the same logical session id.

Cross-referenced concretely:

Agent process (audit.jsonl)

02:36:00  CALL  display_document               → OK viewUUID=1806fccd-...
02:37:07  CALL  interact {viewUUID:1806fccd-...} 
02:37:32  ERR   "Viewer never connected for viewUUID 1806fccd-... (no poll within 8s).
                 The iframe likely failed to mount."

Iframe-host process (mcp.log, [server-name], clientInfo: claude-ai)

02:36:27  initialize / tools/list / resources/list
02:36:29  resources/read mcp-app.html
02:36:32  poll_commands {viewUUID:1806fccd-...}  ← iframe IS polling for this view
02:37:02  → "0 command(s)"                       ← long-poll returns empty after 30s

Same viewUUID, two different processes' queues, both empty from the other's perspective. The iframe is polling correctly; the agent enqueued correctly; they are talking past each other because the host bound them to different processes.

Why this is a problem for MCP Apps in general

The MCP Apps pattern requires shared state across the iframe and agent transports for any non-trivial UX:

  • An agent tool returns a session/view id; the iframe reads it from _meta and starts polling or subscribing.
  • The agent enqueues commands for the iframe to render or execute.
  • The iframe reports user actions / form values back via an internal tool the agent then reads.
  • The server tracks an authentication token that the iframe must echo back.

Every one of these patterns assumes the iframe and the agent are talking to the same server instance — the natural reading of MCP. The current behavior silently violates that assumption only when local-agent-mode is active alongside the iframe, so it's invisible during single-process testing and ships undetected.

Expected behavior (any of)

  1. Single instance per configured server entry. Cowork reuses the same MCP server connection and process for both the iframe and the agent. State works as written. (Strongly preferred.)
  2. Documented contract that MCP Apps must externalize state. If multi-process is intentional, the spec/docs need to say so clearly, and an SDK-supplied shared-state primitive (UNIX socket / SQLite-backed) should ship so server authors don't reinvent it.
  3. Routing semantics for app-only tools that match server expectation. If "the iframe calls back into the same server" is part of the MCP Apps mental model — and it is, per existing app-visibility scoping — then the host should ensure that route lands on the same process the agent is using.

Workaround

Server authors can move all shared state into an external store (UNIX socket leader/follower, SQLite, etc.) and treat the in-process MCP server as a thin frontend. This is significant extra complexity, isn't documented anywhere as a requirement, and adds latency to every iframe ↔ server round-trip.

Severity

Medium-High for any MCP App author. Failure mode is silent in dev and only triggers in agent-mode sessions, so it tends to ship undetected. Stateful MCP Apps are a primary use case for the MCP Apps pattern, so this affects the core value proposition.

Environment

  • Claude Desktop (April 2026 build), macOS
  • MCP App built with @modelcontextprotocol/ext-apps
  • Cowork local-agent-mode active in the same conversation as the iframe

extent analysis

TL;DR

The most likely fix is to ensure that Cowork reuses the same MCP server connection and process for both the iframe and the agent, allowing shared state to work as expected.

Guidance

  • Verify that the issue is indeed caused by the double-spawning of the MCP server process by checking the ps output and the mcp.log file for evidence of two concurrent processes.
  • Consider implementing a workaround by moving all shared state into an external store, such as a UNIX socket or SQLite database, to ensure that state is shared between the two processes.
  • Review the MCP Apps pattern and documentation to ensure that it clearly specifies the expected behavior for shared state and provides guidance on how to implement it.
  • Investigate the possibility of modifying the Cowork local-agent-mode to reuse the existing MCP server connection and process instead of spawning a new one.

Example

No code snippet is provided as the issue is more related to the architecture and configuration of the system rather than a specific code problem.

Notes

The issue is specific to the combination of Cowork local-agent-mode and the MCP Apps pattern, and may not affect all users. The workaround of using an external store for shared state may add complexity and latency to the system.

Recommendation

Apply the workaround of moving shared state into an external store, as it is the most feasible solution given the current architecture and configuration of the system. This will ensure that state is shared between the two processes and allow the MCP Apps pattern to work as expected.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] Cowork agent-mode spawns duplicate MCP server processes, breaking MCP Apps that share state between iframe and agent transports [1 comments, 2 participants]