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)

Open
#3,441 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

v1
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/mcp/client/streamable_http.py, reading _handle_sse_response, _handle_reconnection, and _send_session_terminated_error; compare the corresponding resolution in 2.x from #3047. Reproduce the no-response SSE disconnect and verify that pending requests receive the expected McpError promptly, then run the relevant client tests and confirm the v1.x backport is ready for release.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.