Streamable HTTP: ASGI application returns before the full SSE body is sent
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 50/100
Rechercherichtung
Beginne mit dem EventSourceResponse des POST-Handlers und seiner gemeinsam genutzten Task-Gruppe und reproduziere dann den Fehler mit der bereitgestellten one_attempt-Schleife unter der angegebenen CPU-Beschränkung. Verfolge die Interaktionen von Stream, Disconnect und h11 receive; als erledigt gilt, wenn wiederholte initialize POSTs den vollständigen SSE-Body über Serverneustarts hinweg ohne den von ASGI zurückgegebenen Fehler liefern.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
- I confirm that I'm using the newest release of my line (mcp 2.2.0, and mcp 1.30.0 for the 1.x comparison)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line: 2.x (current stable) — also reproducible on the 1.x maintenance line, see below.
Description
On mcp 1.x and 2.x, a streamable-HTTP POST response can stop before the server sends the full SSE body. uvicorn writes ASGI callable returned without completing response and closes the connection. The client receives httpx.RemoteProtocolError: peer closed connection without sending complete message body. The failure occurs on the first initialize POST of a session. Session setup fails.
We observed this behavior in GitHub Actions and in controlled reproduction environments.
The failure rate grows with two factors: CPU contention, and repeated uvicorn server start/stop cycles in one process.
Verified results, each with a fresh server per attempt:
| Experiment | mcp 2.2.0 | mcp 1.30.0 |
|---|---|---|
| One server per process, moderate CPU load | 24 of 25 runs failed | 0 of 25 runs failed |
| 20 servers in one process, 1 CPU limit | 17 of 20 failed | 17 of 20 failed |
| One server, 200 sequential POSTs | 0 failed | 0 failed |
In the 20-server test, the first two or three servers pass. All later servers fail. One reused server never fails. This pattern points at state that accumulates across server restarts on one event loop.
Expected behavior
The server must always send the last body chunk of the response. The client must always receive the complete stream for a request that has an answer.
Suspected cause (inference)
This section is an inference from code reading.
- The POST handler sends the response through an
EventSourceResponseobject. - The response runs several helper tasks in one shared task group.
- The first task that ends cancels all other tasks in the group.
- The stream task sets
active = Falsebefore it sends the last body chunk. - If the last send operation waits, the disconnect task can end first and cancel the send.
- The h11
receive()function in uvicorn returns immediately after it consumes the request body. This lets the disconnect task run in a loop.
Example Code
"""Repro: streamable-HTTP SSE response terminates mid-body."""
import anyio, httpx, uvicorn
from fastmcp import FastMCP
mcp = FastMCP("repro")
@mcp.tool()
def echo(message: str) -> str:
return message
INIT = {
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "repro", "version": "0"}},
}
HEADERS = {"Accept": "application/json, text/event-stream"}
async def one_attempt() -> str | None:
config = uvicorn.Config(mcp.http_app(transport="streamable-http"),
host="127.0.0.1", port=0, log_level="error")
server = uvicorn.Server(config)
async with anyio.create_task_group() as tg:
tg.start_soon(server.serve)
while not server.started:
await anyio.sleep(0.05)
port = server.servers[0].sockets[0].getsockname()[1]
try:
async with httpx.AsyncClient() as client:
r = await client.post(f"http://127.0.0.1:{port}/mcp", json=INIT, headers=HEADERS)
r.raise_for_status()
except (httpx.HTTPError, RuntimeError) as error:
return f"{type(error).__name__}: {error}"
finally:
server.should_exit = True
return None
async def main() -> None:
failures = 0
for attempt in range(20):
if (error := await one_attempt()) is not None:
failures += 1
print(f"attempt {attempt}: {error}")
print(f"RESULT: {failures}/20 initialize POSTs failed")
anyio.run(main)
Run it in a CPU-limited container:
- Start a container:
docker run --rm --cpus=1 -it python:3.13-slim bash. - Install the dependencies:
pip install fastmcp==4.0.3 uvicorn==0.52.4 httpx anyio. - Run the script.
Expected result: 20 of 20 attempts pass. Actual result: approximately 17 of 20 attempts fail from the third or fourth server onwards.
Python & MCP Python SDK
Python 3.13; mcp 2.2.0 (newest 2.x) and mcp 1.30.0 (newest 1.x); fastmcp 4.0.3 / 3.4.7; uvicorn 0.52.4 (h11); starlette 1.6.0; anyio 4.14.2; httpx 0.28.1; Linux (Ubuntu 24.04, Debian slim; x86_64 and arm64). The closest existing issues (#2150, #3441) describe different defects.
- Vorherrschende Sprache
- Python
- Sterne
- 24.3k
- Forks
- 4k
- Ø Merge
- 1 T. 11 Std.
- Gemergte PRs (30 T.)
- 30
Beitragsleitfaden
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus modelcontextprotocol/python-sdk
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
modelcontextprotocol/python-sdk#3566 ·
-
v1 v2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
modelcontextprotocol/python-sdk#3546 · 5 Kommentare ·
-
v1 v2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
modelcontextprotocol/python-sdk#3545 · 1 Kommentar ·
-
v1 v2
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 91/100
modelcontextprotocol/python-sdk#3508 · 2 Kommentare ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 64/100
modelcontextprotocol/python-sdk#3504 ·
Alle Issues in modelcontextprotocol/python-sdk
Ähnliche Issues
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
xinnan-tech/xiaozhi-fde-talk#263 ·
-
rules
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 90/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
huggingface/Repo2RLEnv#163 · 1 Kommentar ·
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 95/100
huggingface/sentence-transformers#4074 ·
-
comp/dashboard invalid P3
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
NousResearch/hermes-agent#121143 ·