Research guide · MCP · Streamable HTTP · 14 September 2026
MCP RemoteProtocolError: fix incomplete Streamable HTTP SSE responses
MCP SSE failures now show up in at least two high-intent shapes: a request-scoped Streamable HTTP response that ends before its final body arrives, and a long-lived subscriptions/listen stream that acknowledges successfully but dies when a later notification is written. Diagnose the HTTP/SSE lifecycle before changing OAuth, tool schemas or application logic.
Published 14 September 2026 · updated 14 September 2026 · sources checked 14 September 2026
Python client error
httpx.RemoteProtocolError: peer closed connection without sending complete message body
Python server error
ASGI callable returned without completing response
Go long-lived symptom
subscriptions/listen acknowledges, then a later write ends with unexpected EOF
Deployment clue
Failure timing lines up with http.Server.WriteTimeout or an intermediary idle timeout
Why the error is specific
The HTTP connection is closing before MCP finishes the response
The MCP Streamable HTTP contract allows an ordinary request POST to return either one application/json object or a request-scoped text/event-stream SSE response. For normal request-scoped SSE, the stream should reach the final JSON-RPC response before that request is complete.
Python SDK issue #3494, opened 11 September 2026, reports both sides of one failure family: uvicorn says the ASGI callable returned without completing the response, and httpx says the peer closed the connection before the complete message body arrived. The reproduction fails on the initial initialize POST and becomes more frequent with CPU contention or repeated server creation and shutdown on one event loop.
That pattern distinguishes a transport/body-completion problem from an MCP JSON-RPC error. If the server has already selected an SSE response and the connection then dies mid-body, changing OAuth scopes, tool names or output schemas is unlikely to address the first failure boundary.
Fresh Go deployment failure
A subscriptions/listen stream can look healthy until its first real notification fails
Go SDK issue #1262, filed 13 September 2026, describes a different but adjacent failure on current Streamable HTTP deployments. In its minimal reproduction, a subscriptions/listen response is acknowledged immediately while http.Server.WriteTimeout is set to two seconds. Four seconds later, the server produces a real notification and the stream ends with unexpected EOF. With WriteTimeout left at zero in that reproduction, the same notification is delivered and the stream stays open.
The important diagnostic lesson is timing: a long-lived stream can be quiet long enough for an application-server or intermediary deadline to expire even though both sides still appear connected. The failure may only become visible when the next write happens. Compare time-to-failure with your HTTP server deadline, reverse-proxy read timeout and any platform idle timeout before blaming the MCP message.
The 2026-07-28 MCP Streamable HTTP specification explicitly treats subscriptions/listen as a long-lived SSE response, recommends X-Accel-Buffering: no when initiating SSE, and encourages periodic SSE comment lines to keep quiet streams from being closed by intermediaries or client idle timeouts. A comment keepalive can address intermediary idleness, but it cannot by itself repair an application-server write deadline that has already expired; those are separate layers to inspect.
Nine-step diagnostic
Prove which layer kills the stream before applying a workaround
- 1. Capture both ends of the failure. Preserve the client exception and the server/runtime log from the same request or subscription. A client-side EOF alone does not prove which layer closed the connection.
- 2. Record the response content type. Determine whether the failing request selected
text/event-streamorapplication/json. This guide is primarily about the SSE path. - 3. Identify the exact MCP method. Separate an
initializeortools/callresponse that dies before its final result from a long-livedsubscriptions/listenstream that dies after acknowledgement. - 4. Compare failure time with server deadlines. On Go, inspect
http.Server.WriteTimeout; on other stacks, identify the equivalent response/write deadline. If failure appears when the next write occurs after that boundary, the message payload may be innocent. - 5. Audit intermediary idle behavior. Check reverse-proxy read timeouts, buffering and platform connection limits. For long-lived SSE, verify whether
X-Accel-Buffering: noand transport-level comment keepalives are actually reaching the wire. - 6. Compare fresh-process and reused-process behavior. The Python report found one reused server stable while repeated start/stop cycles accumulated failures. If a fresh process behaves differently, lifecycle state is a stronger lead than application payload content.
- 7. Test under reduced CPU contention. A load-sensitive change in failure rate points toward scheduling/lifecycle timing. Do not interpret a low-load pass as proof of correctness; use it to narrow the race.
- 8. Use JSON-only responses only where semantics allow. If an ordinary request needs no streaming and your SDK exposes a supported JSON response mode, this can remove request-scoped SSE while you validate the defect. It is not a replacement for
subscriptions/listen. - 9. Bound retries and keep evidence. Retry only with a clear cap and idempotent request semantics. Record SDK/runtime/proxy versions and the observed timeout boundary so a later fix can be verified rather than guessed.
Do not confuse adjacent failures
Four MCP symptoms that need different fixes
| Symptom | First place to investigate |
|---|---|
| Incomplete request-scoped SSE body / RemoteProtocolError | ASGI response lifecycle, event-stream completion, server process lifecycle and CPU pressure. |
subscriptions/listen acknowledged, then first notification ends with unexpected EOF | Application-server write deadline, reverse-proxy idle timeout/buffering, and SSE keepalive behavior. |
Long-lived subscriptions/listen pins a serverless runtime | Advertised change capabilities, listen-stream behavior and stateless/serverless architecture. |
| HTTP 401/403 or protected-resource metadata 404 | OAuth resource metadata, audience/resource binding and mounted routing. |
Primary evidence
- MCP Python SDK issue #3494 — open report and reproduction for incomplete Streamable HTTP SSE bodies on mcp 1.x and 2.x.
- MCP Go SDK issue #1262 — open report reproducing a long-lived
subscriptions/listenstream failing afterhttp.Server.WriteTimeoutexpires. - MCP 2026-07-28 Streamable HTTP specification — request-scoped POST responses may be JSON or SSE, while
subscriptions/listenis long-lived; the spec also covers buffering and SSE comment keepalives. - MCP 2025-11-25 transport specification — legacy-era Streamable HTTP behavior for POST/SSE responses and disconnection handling.
Continue the implementation path
Migrate MCP Streamable HTTP safely
Separate request-scoped SSE from subscriptions/listen and legacy session assumptions.
Debug MCP OAuth separately
Rule out 401/403 and protected-resource metadata failures before blaming the stream.
Verify an MCP server before connecting
Check transport, trust and discovery signals before an agent connects.
Use zFinia's machine API
Move from protocol research into machine-readable verification and intelligence endpoints.