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)
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 24.3k
- Fork
- 4k
- Merge trung bình
- 1 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 31
Mô tả
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.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu trong src/mcp/client/streamable_http.py bằng cách đọc _handle_sse_response, _handle_reconnection và _send_session_terminated_error; so sánh cách giải quyết tương ứng trong 2.x từ #3047. Tái hiện việc ngắt kết nối SSE không có phản hồi và xác minh rằng các yêu cầu đang chờ nhận được McpError dự kiến một cách kịp thời, sau đó chạy các bài kiểm thử client liên quan và xác nhận rằng bản backport cho v1.x đã sẵn sàng để phát hành.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- api
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 72/100