Cowork MCP client reuses JSON-RPC request ID for concurrent tool calls
Description
When multiple sub-agents make concurrent MCP tool calls through the same Cowork session, all requests are sent with the same JSON-RPC id (always 1). This violates the MCP specification and causes responses to be lost on servers using Streamable HTTP transport.
MCP Spec Reference
From MCP Specification 2025-06-18, Base Protocol, Messages, Requests:
The request ID MUST NOT have been previously used by the requestor within the same session.
Reproduction
- Connect Cowork to an MCP server using Streamable HTTP transport (stateful sessions)
- Run a task that spawns multiple sub-agents making concurrent tool calls (e.g., an eval that dispatches parallel research queries)
- Observe server-side logs: all concurrent
tools/callrequests arrive with"id": 1
Impact
The MCP Python SDK's StreamableHTTPServerTransport uses the JSON-RPC request ID as the key for per-request SSE response streams (_request_streams dict). When two concurrent requests arrive with id: 1:
- First request creates SSE stream keyed to
"1" - Second request overwrites that stream with a new one
- First request's response arrives but its stream is gone
- Server logs:
Request stream 1 not found for message. Still processing message as the client might reconnect and replay. - Client sees a 60s timeout
This affects any long-running tool call (e.g., execute_code calling external APIs) when sub-agents run in parallel.
Evidence
Server logs showing every response has "id":1 (sampled across an entire day, not just one session):
grep 'Request stream.*not found' gateway.log | grep -o 'Request stream [^ ]*' | sort | uniq -c
73 Request stream 1
Always stream 1, never 2, 3, etc.
Expected Behavior
Each concurrent tool call should use a unique incrementing request ID per session (e.g., id: 1, id: 2, id: 3), as the Python MCP SDK's BaseSession does in send_request().
Workaround
We implemented a server-side monkey-patch on StreamableHTTPServerTransport.handle_request that remaps colliding request IDs to unique internal values while preserving true parallelism:
- On incoming POST: peek at the JSON-RPC
id. If a request stream already exists for that ID, rewrite the body'sidto a unique value (e.g.,1becomes1_a3b2c1d0). - The SDK creates a separate stream keyed to the unique ID. Both concurrent calls coexist with independent streams.
- On outgoing SSE response: intercept the ASGI
sendand rewrite theidback to the original value (1) so the client receives the ID it expects.
This approach has no serialization overhead. Calls run fully in parallel. The patch is a no-op when IDs are unique (short-circuits on the _request_streams check).
An earlier attempt using asyncio.Lock per ID to serialize concurrent calls fixed the lost-response issue but introduced cascading 60s timeouts under load, since all id: 1 calls queued behind each other.
Environment
- Claude Desktop / Cowork (latest as of 2026-03-26)
- MCP server: Python MCP SDK 1.26.0, Streamable HTTP transport, stateful sessions
- Observed with both single-user and multi-sub-agent workloads
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗