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)

Abierto
#3,441 1 comentario 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

v1
Lenguaje dominante
Python
Estrellas
24.3k
Forks
4k
Merge medio
1 d 1 h
PR fusionados (30 d)
31

Descripción

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.

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza en src/mcp/client/streamable_http.py leyendo _handle_sse_response, _handle_reconnection y _send_session_terminated_error; compara la resolución correspondiente en 2.x de #3047. Reproduce la desconexión SSE sin respuesta y verifica que las solicitudes pendientes reciban rápidamente el McpError esperado. Después, ejecuta las pruebas relevantes del cliente y confirma que el backport a v1.x está listo para su publicación.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
python
Área
api
Tipo de issue
Error
Dificultad
3/5
Tiempo estimado
1-2 días
Estado de actividad
Activo
Claridad
Bien especificado
Aptitud para principiantes
72/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.