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)

Offen
#3,441 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Anfängerfreundlichkeit
72/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
python
Bereich
api

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.

Beschreibung

v1
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…)
  1. _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.
  2. _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
  • #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<2 that renders this hang as Tool 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.

Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 1 Std.
Gemergte PRs (30 T.)
31

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus modelcontextprotocol/python-sdk

Alle Issues in modelcontextprotocol/python-sdk

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.