claude-code - ✅(Solved) Fix Late response to cancelled MCP request drops stdio transport instead of being ignored [1 pull requests]

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…

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

Error Message

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)

Root Cause

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

Fix Action

Fixed

PR fix notes

PR #40: feat: cancel-aware response filter (drops late responses for cancelled ids)

Description (problem / solution / changelog)

Summary

  • Enforces the MCP cancellation spec's canceller-side SHOULD on the wire: notifications/cancelled ids seen on stdin are tracked (60 s TTL), and any late JSON-RPC response carrying one of those ids is dropped before reaching the downstream client.
  • Also shields clients from the reciprocal receiver-side violation (cf. python-sdk#2480 — server still sends an ErrorData response on cancel).
  • Works around claude-code#51073 where Claude Code drops the stdio transport on late responses instead of ignoring them.
  • Enabled by default; --no-cancel-filter opt-out for debugging the raw upstream wire.
  • Applies to both Streamable HTTP (run()) and legacy SSE (run_sse()) transports.

Closes #39.

Design

  • _CancelTracker — thread-safe id→monotonic-timestamp dict, 60 s TTL, size-triggered GC at 256 entries. Shared between the main loop and the SSE reader thread.
  • _extract_cancel_id(line) — regex fast path ("method":"notifications/cancelled") gates a full json.loads verify so the slow path runs only on actual cancels.
  • _emit(line, tracker) — single gate replacing every response-bearing print in relay.py (5 call sites). Only proper JSON-RPC responses (object with id + result/error) are eligible for dropping; notifications, server-initiated requests, JSON-RPC batches, and anything that fails to parse pass through. mcp-stdio's own synthesized error responses (_error_response) intentionally bypass the gate.

Out of scope (future work)

  • notifications/progress suppression (separate progressToken keyed map)
  • Aborting paginated list fetches mid-merge on cancel
  • JSON-RPC batch support (current MCP HTTP transport forbids batches)

Test plan

  • pytest tests/ -v → 293 passed, 3 skipped (local, macOS/Python 3.12)
  • TestCancelTracker covers add/contains/TTL/GC/thread-safety, plus id=0 and string ids
  • TestExtractCancelId covers int/string requestId, malformed json, missing params, non-dict params, and substring false-positive guard
  • TestEmit covers passthrough, drop, error response, uncancelled id, notification without id, server-initiated request, malformed json, json batch
  • TestRunCancelFilter — Streamable HTTP end-to-end: cancel-then-late-response drop, cancel forwarded upstream, --no-cancel-filter disables filter
  • TestRunSseCancelFilter — SSE end-to-end: async response via reader thread dropped when id is cancelled, passes through otherwise
  • TestCli--no-cancel-filter wires to both run() and run_sse()
  • Manual smoke test against a real MCP server (optional — behavior is covered by the end-to-end tests)

Changed files

  • CLAUDE.md (modified, +1/-0)
  • README.ja.md (modified, +1/-0)
  • README.md (modified, +1/-0)
  • src/mcp_stdio/cli.py (modified, +18/-1)
  • src/mcp_stdio/relay.py (modified, +164/-10)
  • tests/test_cli.py (modified, +39/-0)
  • tests/test_relay.py (modified, +421/-0)

Code Example

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)
RAW_BUFFERClick to expand / collapse

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

extent analysis

TL;DR

Update Claude Code to ignore JSON-RPC responses for cancelled requests by logging at debug level and dropping the message instead of treating it as a transport error.

Guidance

  • Review the MCP specification for cancellation notifications and ensure that the sender of the cancellation notification ignores any response to the request that arrives afterward.
  • Modify the JSON-RPC response handling in Claude Code to check if the response ID is still in the in-flight map before processing it.
  • If the ID is no longer in the map, log the event at debug level and drop the message instead of closing the transport.
  • Verify that the updated code correctly handles cancelled requests and does not trigger unnecessary reconnects.

Example

No code snippet is provided as the issue does not contain sufficient information about the implementation details of Claude Code.

Notes

The suggested fix assumes that the issue is caused by Claude Code not following the MCP specification for cancellation notifications. The actual implementation may vary depending on the specifics of the Claude Code architecture.

Recommendation

Apply the suggested fix to update Claude Code to ignore JSON-RPC responses for cancelled requests, as this aligns with the MCP specification and should prevent unnecessary reconnects.

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