modelcontextprotocol / modelcontextprotocol/python-sdk
1.x: streamable_http client leaves a request unresolved when the SSE response stream ends without a response (fixed on 2.x by #3047 — backport request for v1.x)
Dieses Issue hat noch niemand übernommen.
- Vorherrschende Sprache
- Python
- Sterne
- 24.3k
- Forks
- 4k
- Ø Merge
- 1 T. 1 Std.
- Gemergte PRs (30 T.)
- 31
Beschreibung
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 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 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
- #3047 — the 2.x fix (
_resolve_abandoned_request), part of the subscriptions/listen driver PR. - #1401 — ClientSession error handling (broader; this is one concrete instance on 1.x).
- Downstream consumer report: IBM/mcp-context-forge#6592 (a gateway on
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.x if maintainers want it.
Beitragsleitfaden
Erste Schritte
- Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
- Forke das Repository und arbeite in einem Branch.
- Öffne einen Pull Request, der die Issue-Nummer nennt.
Rechercherichtung
Beginne in src/mcp/client/streamable_http.py mit dem Lesen von _handle_sse_response, _handle_reconnection und _send_session_terminated_error; vergleiche die entsprechende Lösung in 2.x aus #3047. Reproduziere die SSE-Trennung ohne Antwort und überprüfe, dass ausstehende Anfragen umgehend den erwarteten McpError erhalten. Führe anschließend die relevanten Client-Tests aus und bestätige, dass der v1.x-Backport zur Veröffentlichung bereit ist.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- python
- Bereich
- api
- Issue-Typ
- Bug
- Schwierigkeit
- 3/5
- Geschätzter Aufwand
- 1-2 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Klar beschrieben
- Anfängerfreundlichkeit
- 72/100