Repository navigation
Description
Claude Code 2.1.143 silently hangs MCP tool calls after SSE drop — debug logs show client detects the drop but does not fail or reconnect in-flight calls (v3)
TL;DR
When the SSE channel to an MCP server drops, Claude Code logs the drop, then leaves every in-flight tool call running forever. There is no error surfaced to the user, no automatic reconnect, no failure of pending calls — only Esc/user-cancel can break out. Reproduced live with debug logging enabled; exact log lines below.
Severity / Impact
- A single transient SSE drop on a long-running session makes every MCP tool call hang silently for the rest of the session.
- Only manual user-cancel or full Claude Code restart recovers.
- Affects any deployment using SSE-transport MCP servers behind any reverse proxy.
Environment
-
Claude Code: 2.1.143 (cc_version
2.1.143.96b, macOS, Darwin 25.3.0). -
MCP server:
crystaldba/postgres-mcpv0.3.0; image digestsha256:dbbd346860d29f1543e991f30f3284bf4ab5f096d049ecc3426528f20b1b6e6b;mcpPython SDK 1.6.0 inside the container. -
Reverse proxy: nginx with
proxy_buffering off,proxy_cache off,proxy_read_timeout 86400s,proxy_send_timeout 86400s— explicitly configured to support long-lived SSE. -
Claude Code MCP configuration (
~/.claude.json, redacted):"pg-mcp": { "type": "sse", "url": "https://mcp.example.com/sse", "headers": { "Authorization": "Bearer <redacted>" } }
Reproduction
- Configure Claude Code with an MCP server over SSE (config block above).
- Make MCP tool calls — they work.
- Leave the session idle long enough that the underlying TCP connection / SSE stream is torn down on either side (in our trace: SSE channel had been up for
61937s≈ 17h12m before the drop event). - Issue any MCP tool call.
- Observe: Claude Code dispatches the call, the SSE channel is detected as dropped within a fraction of a second, but the tool call remains "running" indefinitely.
We have not yet pinned down what triggered the SSE drop itself — it could be server-side TaskGroup cancellation noise we see in our mcp SDK 1.6.0 logs (see "Server-side notes"), or container/network state. That is independent of the bug being reported here. The bug is the client's response to a drop it detected on its own.
Observed behavior (verbatim Claude Code debug log, single tool call)
2026-05-17T20:23:00.766Z [INFO] [Stall] tool_dispatch_start tool=mcp__pg-mcp__list_schemas
toolUseId=toolu_01T2JSzbbHXsXm4jAAdEnuxv permissionDecisionMs=1
2026-05-17T20:23:00.766Z [DEBUG] MCP server "pg-mcp": Calling MCP tool: list_schemas
2026-05-17T20:23:00.767Z [DEBUG] MCP server "pg-mcp": No token data found
2026-05-17T20:23:01.090Z [DEBUG] MCP server "pg-mcp": SSE connection dropped after 61937s uptime
2026-05-17T20:23:01.090Z [DEBUG] MCP server "pg-mcp": Connection error: SSE error: undefined
2026-05-17T20:23:30.768Z [DEBUG] MCP server "pg-mcp": Tool 'list_schemas' still running (30s elapsed)
2026-05-17T20:24:00.769Z [DEBUG] MCP server "pg-mcp": Tool 'list_schemas' still running (60s elapsed)
... (one such line every 30s for 23 minutes, no other MCP events for pg-mcp) ...
2026-05-17T20:46:00.934Z [DEBUG] MCP server "pg-mcp": Tool 'list_schemas' still running (1380s elapsed)
2026-05-17T20:46:12.374Z [DEBUG] [onCancel] source=local focusedInputDialog=undefined streamMode=tool-use
2026-05-17T20:46:12.382Z [DEBUG] MCP server "pg-mcp": Tool 'list_schemas' failed after 1391s:
MCP error -32001: user-cancel
2026-05-17T20:46:12.383Z [WARN] [Stall] tool_dispatch_end tool=mcp__pg-mcp__list_schemas
toolUseId=toolu_01T2JSzbbHXsXm4jAAdEnuxv outcome=error durationMs=1391617
2026-05-17T20:46:12.384Z [ERROR] McpError: McpError: MCP error -32001: user-cancel
Notes on what this shows:
- The SSE drop event is logged 324 ms after the tool call is dispatched, with a clear reason string:
SSE error: undefined. - Between the drop event and the user-cancel 23 minutes later, there are zero MCP-related log lines for
pg-mcpother than the 30 s "still running" heartbeat. No reconnect attempt, no error surfaced, no failure of pendingtoolUseId=toolu_01T2JSzbbHXsXm4jAAdEnuxv. - The only thing that ends the call is a local cancel (
[onCancel] source=local).
Diagnosed bug (no longer just a hypothesis)
When SSE connection dropped fires for an MCP server, Claude Code 2.1.143 does not:
- Fail any in-flight tool calls bound to that server with a transport-level error.
- Attempt to reopen the SSE channel.
- Surface anything to the user beyond a debug log entry.
Instead it keeps emitting the 30-second "still running" heartbeat forever, while the actual transport is dead.
Alternative root causes considered (and now ruled out as the primary issue)
| Alternative | Status | Notes |
|---|---|---|
| nginx idle timeout dropping SSE | Ruled out | nginx config verified: proxy_read_timeout 86400s, proxy_buffering off. |
| Bearer-token expiry | Ruled out | Static tokens, no TTL. |
| Container restart / OOM | Not the primary issue | Even if the SSE drop was caused by upstream state, the client behavior on drop is what makes this user-visible. |
| Multiple Claude Code processes racing | Ruled out | Server side shows one client connection per process. |
Client sent a request before MCP initialize completed |
Not the primary issue | The drop is detected by the client immediately; that is what should trigger fail/reconnect. |
| MCP Python SDK 1.6.0 server-side session-state bug | Possibly contributory | Server-side RuntimeError traces are visible in our postgres-mcp logs (see below). They explain why the underlying SSE stream tears down but do not explain why the client then sits silently for 23 minutes. |
| Normal asyncio/anyio cancellation noise | Not the primary issue | Same reason. |
All of these were live candidates in our earlier draft. The Claude Code debug log eliminates them as the user-visible cause; whatever upstream condition causes the SSE drop, the bug being reported is the client's silent response to that drop.
Suggested mitigation priorities
- Primary (Claude Code, client-side) — on
SSE connection dropped:- Mark all in-flight tool calls for that MCP server as failed with a transport-level error.
- Either auto-reopen the SSE channel and re-run the MCP
initializehandshake, or surface "MCP server disconnected — please retry" to the user. - For non-idempotent tools, do not auto-retry — fail the call and let the user decide whether to retry.
- Secondary (upstream
mcpPython SDK) — replace the bareRuntimeError: Received request before initialization was completeatmcp/server/session.py:163with a proper MCP error response so future deployments where this is the underlying cause can debug it from client-visible signals. - Operator workaround (verified viable) — switch the MCP server from
--transport=sseto--transport=http(Streamable HTTP, added upstream in commit07eb329, January 2026). Confirmed viaclaude mcp add --helpthat Claude Code 2.1.143 supports--transport http. Streamable HTTP is request/response and avoids the dropped-long-lived-channel mode entirely.
Server-side notes (separate from the client bug)
While debug logs were on we captured several instances of the following server-side exception on the same postgres-mcp container. These are likely what is triggering the SSE drops on the server side. Sharing in case the Claude Code team wants to correlate with the client side, but this is upstream mcp SDK behavior, not Claude Code's:
ERROR: Exception in ASGI application
+ Exception Group Traceback (most recent call last):
| File "/app/.venv/lib/python3.12/site-packages/mcp/server/fastmcp/server.py", line 485, in handle_sse
| File "/app/.venv/lib/python3.12/site-packages/mcp/server/sse.py", line 123, in connect_sse
| File "/app/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| File "/app/.venv/lib/python3.12/site-packages/mcp/shared/session.py", line 324, in _receive_loop
| File "/app/.venv/lib/python3.12/site-packages/mcp/server/session.py", line 163, in _received_request
| raise RuntimeError(
| RuntimeError: Received request before initialization was complete
The server returns HTTP 202 Accepted on the POST /messages/?session_id=<id> first, then the ASGI task crashes asynchronously. No MCP-level error frame is sent back. Combined with Claude Code's silent handling of the SSE drop, the net effect on the user is the indefinite hang documented above.
Notes for further investigation
Happy to share on request:
- Full Claude Code debug log file.
- Full
docker logsoutput of the MCP container around the drop window. - Repeated runs against
--transport=httpto confirm the workaround removes the hang entirely.
Activity
github-actions commentedon May 17, 2026
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
arbitermano commentedon May 17, 2026
Pushing back on the duplicate classification.
MCP SSE client hangs indefinitely on dropped connection #26368 — same symptom (SSE drop → next tool call hangs forever → only Esc recovers). But it's CLOSED, and v2.1.110 (2026-04-15) shipped what looks like the intended fix:
I'm on 2.1.143 with a fresh debug-log repro — the SSE side of that fix didn't fully land.
[BUG] Streamable HTTP MCP: in-flight tool call Promise orphaned forever on connection drop #55992 — related (same hang-on-drop family) but Streamable HTTP transport, not SSE. Different surface.
"Unhandled case: [object Object]" overlay crashes session (correlates with MCP SSE reconnect) #59307 — different bug: VS Code overlay crash on reconnect attempt. In my case there is no reconnect at all, no overlay.
New evidence vs the older #26368: the debug log on 2.1.143 shows
→ no reconnect attempt, no transport-level failure surfaced, only user-cancel ends the call.
Please keep #60061 open as a v2.1.110-fix regression / incomplete-fix repro on 2.1.143.
kcarriedo commentedon May 17, 2026
Pushing back on the duplicate flag too, and adding evidence that points at where the SSE-side fix may have regressed.
Running a separate Rust orchestrator that spawns
claude -pover MCP-SSE on long-lived sessions (Darwin 25.3.0, 2.1.143). We see the same shape: SSE drop is detected and logged, in-flight tool call never errors, onlyEsc/process kill recovers. A few observations that might narrow the diff between v2.1.110 and v2.1.143:1. The "detected the drop, did not propagate" gap. The fix shipped in v2.1.110 had to cancel pending in-flight calls when the transport went down — your debug log shows the detect side firing (
tool_dispatch_startthen nothing) but the cancel side never reaching the agent loop. That's the load-bearing detail and matches what we see: the loop is still waiting on a future that has no producer, no error path, no timeout. A regression of just the per-call cancel propagation would look exactly like this while leaving the higher-level reconnect logic intact (which is why this looks "less broken" than #26368 did before the fix).2. The 17h12m drop is informative. Long SSE channels are torn down by ~everything between the client and server (nginx keepalives, NAT tables, OS socket timeouts). Even with
proxy_read_timeout 86400sandproxy_buffering off, the failure mode you'll hit in production is "TCP RST or FIN, not a graceful close" — and a regression on the in-flight-call cancellation path only manifests on that abrupt-drop case, not the graceful close that's easier to repro on a dev box. That probably also explains why this snuck through whatever regression suite covered v2.1.110: testing graceful close passes, testing socket reset hangs.3. Workaround that's holding on our side: wrap MCP tool calls in a parent-side timeout (we use ~5 min for read calls, longer for write) and on timeout, kill+respawn the MCP server subprocess from outside the agent. Ugly but unblocks the session. The proper fix on the Claude Code side is presumably either (a) reinstate the per-call cancel that v2.1.110 added when the transport-down event fires, or (b) add a client-side per-call deadline as a secondary guardrail.
4. The Streamable-HTTP cousin (#55992) is the right cross-reference, not a duplicate. Same family — "transport detected dead, in-flight calls leak" — but if SSE and HTTP code paths re-converged in 2.1.143 and the cancellation refactor only landed for HTTP, the SSE side regressing alone makes sense. Worth checking whether
useUnifiedTransportor equivalent flag flipped recently.Happy to share the orchestrator's reconnect-watchdog code if useful as a reference. Independent repro, different workload, same fingerprint.
arbitermano commentedon May 18, 2026
Confirming the fingerprint match. Same OS (Darwin 25.3.0), same 2.1.143, nginx upstream with
proxy_read_timeout 86400s— drop happened at the 17h12m mark, no graceful close on either side, so this aligns with your "abrupt RST/FIN, not graceful close" hypothesis explaining why the v2.1.110 regression suite likely missed it.Your split of the v2.1.110 fix into (a) transport-down detection and (b) per-call cancel propagation matches my debug log exactly: detection fires (
SSE connection dropped+Connection error), nothing else fires, the 30-second heartbeat keeps going. The cancel-propagation side is the load-bearing piece that looks regressed.Parent-side per-call deadline as a guardrail is the right secondary fix — agreed it's ugly. Would still want the transport-down → fail-pending-calls path restored as the primary.
+1 on #55992 being the cross-reference, not a duplicate. If the SSE/HTTP transport refactor diverged again post-v2.1.110, that's the diff to look at.
arbitermano commentedon Jun 2, 2026
Still reproduces on 2.1.160 (latest on npm as of 2026-06-02), same Darwin 25.3.0, same SSE upstream behind nginx with
proxy_read_timeout 86400s.I walked the changelog across every release from 2.1.143 → 2.1.160 — no entry addresses the load-bearing symptom here (transport-down detected, in-flight call never failed or cancelled). The nearest MCP/SSE touch is:
tools/list" — different surface: that's the discovery/tools/listreconnect path looping, not an in-flight tool call hanging on an abrupt drop. In our case there is no reconnect at all.Everything else in that range is paginated
tools/list(2.1.144/145/147/157), image results (2.1.145), or OAuth/proxy (2.1.152) — none touch the per-call cancel propagation that @kcarriedo and I narrowed this down to.So the v2.1.110 regression (transport-down → fail-pending-calls path not firing on SSE) is still live on 2.1.160. Flagging to keep this from going stale / being auto-closed. Happy to re-run the debug-log repro on any candidate build.
mikestarkenburg commentedon Jun 12, 2026
Data point that narrows this (tested 2026-06-12 on 2.1.156 and 2.1.175): with a healthy streamable-HTTP MCP server, large tool results alone never hang — 1KB / 64KB / 256KB text payloads all delivered cleanly via
claude -p(six runs across both versions, 11–18s each, payloads verified intact).So the failure mode here really does look like the orphaned in-flight tool call on transport failure (no client-side timeout), consistent with #38437 — not a payload-size threshold. A per-call client-side timeout for streamable-HTTP (or honoring
MCP_TOOL_TIMEOUTin-pmode) would convert the infinite hang into a recoverable tool error.mcp-docker-deployment: default to streamable-http, deprecate SSE (v1.…
fix(run_sse): synthesize errors for requests in flight when the SSE s…
github-actions commentedon Jul 24, 2026
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
feat(supabase): postgres-mcp en streamable-http, image construite dep…
github-actions commentedon Sep 18, 2026
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.