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)
まだ誰も着手していません。
- 主要言語
- Python
- スター
- 24.3k
- フォーク
- 4k
- 平均マージ
- 1日 1時間
- マージ済み PR(30日)
- 31
説明
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.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
src/mcp/client/streamable_http.py で _handle_sse_response、_handle_reconnection、_send_session_terminated_error を読み始め、#3047 の 2.x における対応する解決方法と比較します。応答のない SSE 切断を再現し、保留中のリクエストが期待される McpError を速やかに受け取ることを確認してから、関連するクライアントテストを実行し、v1.x へのバックポートがリリース可能な状態であることを確認します。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- api
- issue の種類
- バグ
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 活発さ
- 活発
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 72/100