Late response to cancelled MCP request drops stdio transport instead of being ignored

Status Closed — not planned
Maintainer reply ✓ Yes — localden
Activity 6 comments · opened Apr 20, 2026 · closed May 23, 2026
💡 Likely answer: A maintainer (localden, collaborator) responded on this thread — see the highlighted reply below.

Description

When an MCP server sends a JSON-RPC response for a request that the client has already cancelled via notifications/cancelled, Claude Code logs Received a response for an unknown message ID, closes the stdio transport, and reconnects.

The cancellation spec says senders should silently ignore these:

The sender of the cancellation notification SHOULD ignore any response to the request that arrives afterward

— https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/utilities/cancellation.mdx#L46-L47

Reproduction

Any MCP server using mcp Python SDK 1.27.0 (which itself violates the receiver-side SHOULD by sending a response on cancel — separate issue against the SDK). Configure the server in ~/.claude.json, invoke a long-running tool (e.g. anything that does await asyncio.sleep), cancel it.

Sample log lines from ~/Library/Caches/claude-cli-nodejs/.../mcp-logs-<server>/<session>.jsonl:

Tool 'poll_build' failed after 57s: MCP error -32001: user-cancel
STDIO connection dropped after 279s uptime
Connection error: Received a response for an unknown message ID:
    {"jsonrpc":"2.0","id":3,"error":{"code":0,"message":"Request cancelled"}}
Closing transport (stdio transport error: Error)

Impact

  • Every cancelled MCP tool call costs 1–2 reconnects (~5–10s each, more if the server image has --pull always set).
  • Heavy use of long-running MCP tools puts the server in a near-permanent reconnect loop.
  • Defends against malformed servers (the spec puts the SHOULD-not-send burden on the receiver, but resilient sender behaviour is also called out).

Suggested fix

When a JSON-RPC response arrives with an id that's no longer in the in-flight map, log at debug and drop the message instead of treating it as a transport error. Reserve the transport-drop behaviour for actual protocol violations (malformed JSON, etc.).

Environment

  • Claude Code latest (Opus 4.7)
  • macOS, stdio transport
  • MCP server: any using mcp python SDK 1.27.0

View original on GitHub ↗

6 Comments

0xbrainkid · 5 months ago

This is a good report because it identifies a protocol-boundary overreaction. A late response to a cancelled request is exactly the kind of thing a robust client should treat as stale noise, not as a fatal transport condition.

The spec quote matters here: once cancellation has been sent, responses that arrive afterward should be ignored. So even if the server-side SDK is imperfect and still emits a response, the Claude Code client should degrade gracefully rather than dropping the whole stdio connection.

Right now the blast radius is too large:

  • one cancelled request finishes late
  • client sees an unknown message ID
  • stdio transport is closed
  • reconnect churn follows

That turns a routine async race into a connection-level incident.

This is another example of boundary handling being the real product. In asynchronous systems, late, duplicate, or stale messages are normal. The client has to tolerate them without treating them as existential protocol failure.

shigechika · 5 months ago

As a workaround for anyone hitting this in production: mcp-stdio v0.7.0 sits between Claude Code and the HTTP MCP server over stdio and drops any late JSON-RPC response whose id was cancelled via notifications/cancelled. That turns this canceller-side SHOULD violation into a no-op on the wire — Claude Code never sees the late response, so the stdio transport stays up and the reconnect loop never starts.

How it works:

  • stdin side — when notifications/cancelled is seen, params.requestId is added to a thread-safe, TTL-bounded (60 s) set. The cancel itself is still forwarded upstream so a spec-compliant server stops its work.
  • response side — every JSON-RPC response on the wire is checked; if its id is in the cancel set, it's dropped before reaching stdout. Notifications, server-initiated requests (e.g. sampling/createMessage), and mcp-stdio's own error responses are never filtered.
  • The same gate also compensates for the reciprocal receiver-side SHOULD violation in modelcontextprotocol/python-sdk#2480, which is where most late responses originate today.

Enabled by default; --no-cancel-filter opts out for debugging raw upstream traffic. Works for both Streamable HTTP and the legacy SSE transport. One caveat — if the response arrives over the wire before Claude Code has handed the cancel to stdin (possible on SSE due to the async reader thread), the filter can't help because the cancel hasn't been registered yet; the fix for that race lives on the Claude Code side.

q-thomasdickson · 5 months ago

The workaround we used was to monkey patch the behaviour. Adding an extra dependency or service isn't a useful solution for us.

m13v · 5 months ago

hit this exact pattern running python SDK MCP servers on macOS for a desktop agent. the real cost isn't the 5-10s reconnect, it's that in-flight requests queued behind the dropped transport get silently re-dispatched on the new connection, which makes bugs look non-deterministic across runs. the monkey-patch fixes the symptom but we ended up adding a small seen-ids ring buffer client-side with a 2 minute TTL so stale ids from any source just drop at debug level. handles the mcp 1.27 case, and also catches servers that respond twice due to their own retry logic. the bigger principle: anything async across a stdio boundary needs to treat stale messages as normal, not as a protocol incident.

localden collaborator · 4 months ago

Thanks for the report. This is the same issue as #50095 (unknown-ID JSON-RPC response drops stdio transport). Consolidating tracking there.

github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.