Summary
On the 1.x line (v1.29.0, v1.29.1, and the v1.x branch as of 92120b4b), the Streamable HTTP client leaves a pending request unresolved forever when the text/event-stream response for that request ends without a response event. The caller learns of the disconnect only when its own deadline fires, so a dead server is indistinguishable from a slow one — and it is reported as a timeout, not a disconnect.
This is already fixed on 2.x: #3047 (merged 2026-07-07, shipped in v2.0.0) added StreamableHTTPTransport._resolve_abandoned_request(...) and calls it at exactly the two sites below with "SSE stream ended without a response" and "SSE stream ended and reconnection attempts were exhausted". The v1.x branch does not carry it (grep -c _resolve_abandoned_request src/mcp/client/streamable_http.py → 0 at 92120b4b), and neither did the v1.29.1 release (2026-08-24).
Ask: backport the _resolve_abandoned_request resolution from #3047 onto v1.x and cut a 1.29.x/1.30 release. Consumers pinned to mcp<2 (e.g. IBM/mcp-context-forge, mcp>=1.28.1,<2 at v1.0.8/v1.0.9/main) cannot pick the fix up from 2.x.
Mechanism (src/mcp/client/streamable_http.py @ v1.29.0 == v1.29.1, sha256 8eb01c3d…)
_handle_sse_response (:397-435): a completed response returns inside the try. When the server dies mid-stream, EventSource.aiter_sse() raises httpx.RemoteProtocolError("peer closed connection without sending complete message body (incomplete chunked read)"), which except Exception as e: # pragma: no cover (:429-430) logs at DEBUG (SSE stream ended: …). The tail (:432-435) reconnects only if last_event_id is not None; a server that sends no event ids (FastMCP's default, no event store) makes the coroutine return without writing anything to ctx.read_stream_writer. ClientSession.send_request therefore waits forever on its response stream.
_handle_reconnection (:446-448) gives up after MAX_RECONNECTION_ATTEMPTS with a bare return — the same unresolved-request hang for servers that do send ids.
The SDK already has the channel to resolve this: _send_session_terminated_error (:510-522) writes a JSONRPCError for a request id to read_stream_writer, and _receive_loop routes it to the pending request, where send_request raises McpError.
Reproduction (measured 2026-09-04, mcp 1.29.0, httpx 0.28.1, anyio 4.14.2, Python 3.12.13)
Server: a FastMCP 3.x Streamable HTTP server with a tool slow(seconds) that sleeps. Client: streamablehttp_client + ClientSession, initialize(), then
with anyio.fail_after(5):
await session.call_tool("slow", {"seconds": 4})
and SIGKILL the server ≈1.0 s after the call starts (response headers arrive at ~0.002 s; no event before the kill).
|
Leaf exception |
elapsed |
| v1.29.0 as shipped |
builtins.TimeoutError (str='') — the caller's own fail_after |
5.008 s |
v1.29.0 + _resolve_abandoned_request backported in memory (same two sites, same messages, code=CONNECTION_CLOSED) |
mcp.shared.exceptions.McpError: SSE stream ended without a response (httpx.RemoteProtocolError: peer closed connection without sending complete message body (incomplete chunked read)) |
1.06–1.14 s |
The SDK's own debug log confirms the swallow: SSE stream ended: peer closed connection without sending complete message body (incomplete chunked read) at +1.11 s, then silence until the caller's deadline.
Related
We are carrying a build-time backport of the two-site resolution in our ContextForge image until a 1.x release ships it; happy to open the backport PR against v1.x if maintainers want it.
Summary
On the 1.x line (
v1.29.0,v1.29.1, and thev1.xbranch as of92120b4b), the Streamable HTTP client leaves a pending request unresolved forever when thetext/event-streamresponse for that request ends without a response event. The caller learns of the disconnect only when its own deadline fires, so a dead server is indistinguishable from a slow one — and it is reported as a timeout, not a disconnect.This is already fixed on 2.x: #3047 (merged 2026-07-07, shipped in v2.0.0) added
StreamableHTTPTransport._resolve_abandoned_request(...)and calls it at exactly the two sites below with"SSE stream ended without a response"and"SSE stream ended and reconnection attempts were exhausted". Thev1.xbranch does not carry it (grep -c _resolve_abandoned_request src/mcp/client/streamable_http.py→ 0 at92120b4b), and neither did the v1.29.1 release (2026-08-24).Ask: backport the
_resolve_abandoned_requestresolution from #3047 ontov1.xand cut a 1.29.x/1.30 release. Consumers pinned tomcp<2(e.g. IBM/mcp-context-forge,mcp>=1.28.1,<2at v1.0.8/v1.0.9/main) cannot pick the fix up from 2.x.Mechanism (
src/mcp/client/streamable_http.py@ v1.29.0 == v1.29.1, sha2568eb01c3d…)_handle_sse_response(:397-435): a completed response returns inside thetry. When the server dies mid-stream,EventSource.aiter_sse()raiseshttpx.RemoteProtocolError("peer closed connection without sending complete message body (incomplete chunked read)"), whichexcept Exception as e: # pragma: no cover(:429-430) logs at DEBUG (SSE stream ended: …). The tail (:432-435) reconnects onlyif last_event_id is not None; a server that sends no event ids (FastMCP's default, no event store) makes the coroutine return without writing anything toctx.read_stream_writer.ClientSession.send_requesttherefore waits forever on its response stream._handle_reconnection(:446-448) gives up afterMAX_RECONNECTION_ATTEMPTSwith a barereturn— the same unresolved-request hang for servers that do send ids.The SDK already has the channel to resolve this:
_send_session_terminated_error(:510-522) writes aJSONRPCErrorfor a request id toread_stream_writer, and_receive_looproutes it to the pending request, wheresend_requestraisesMcpError.Reproduction (measured 2026-09-04, mcp 1.29.0, httpx 0.28.1, anyio 4.14.2, Python 3.12.13)
Server: a FastMCP 3.x Streamable HTTP server with a tool
slow(seconds)that sleeps. Client:streamablehttp_client+ClientSession,initialize(), thenand SIGKILL the server ≈1.0 s after the call starts (response headers arrive at ~0.002 s; no event before the kill).
elapsedbuiltins.TimeoutError(str='') — the caller's ownfail_after_resolve_abandoned_requestbackported in memory (same two sites, same messages,code=CONNECTION_CLOSED)mcp.shared.exceptions.McpError:SSE stream ended without a response (httpx.RemoteProtocolError: peer closed connection without sending complete message body (incomplete chunked read))The SDK's own debug log confirms the swallow:
SSE stream ended: peer closed connection without sending complete message body (incomplete chunked read)at +1.11 s, then silence until the caller's deadline.Related
_resolve_abandoned_request), part of the subscriptions/listen driver PR.mcp<2that renders this hang asTool invocation timed out after {tool_timeout}s).We are carrying a build-time backport of the two-site resolution in our ContextForge image until a 1.x release ships it; happy to open the backport PR against
v1.xif maintainers want it.