Repository navigation
streamable_http_client: one concurrent request HTTPStatusError tears down sibling requests #2604
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
When using streamable_http_client with concurrent JSON-RPC requests over the same Streamable HTTP session, an HTTP error from one request can escape the transport task group and disrupt unrelated sibling requests that are still waiting for their responses.
This happens because post_writer() starts each JSON-RPC request as a task: tg.start_soon(handle_request_async) and _handle_post_request() calls: response.raise_for_status()
If one concurrent request receives a non-2xx HTTP response, e.g. 400 Bad Request, the resulting httpx.HTTPStatusError escapes from that per-request task into the shared task group. The task group then exits with:
ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
At the same time, sibling requests that were still waiting in ClientSession.send_request() can be cancelled/disrupted. In our production traces, the sibling request stack showed:
File ".../mcp/shared/session.py", line 292, in send_request
response_or_error = await response_stream_reader.receive()
File ".../anyio/streams/memory.py", line 117, in receive
return self.receive_nowait()
File ".../anyio/streams/memory.py", line 112, in receive_nowait
raise WouldBlock
anyio.WouldBlock
The WouldBlock is not the root cause by itself; it appears because the sibling request was waiting for a response when the transport/session was torn down by the other request's HTTP error.
Expected Behavior
A non-2xx HTTP response for one request should be scoped to that request only.
For example, if request A receives HTTP 400, request A should receive a request-level exception or MCP error, but request B should continue waiting for its own response and should be able to complete successfully.
Actual Behavior
A single request's HTTPStatusError escapes the shared transport task group and can poison the whole Streamable HTTP session, cancelling or disrupting sibling requests.
Why this matters
Parallel tool calls are a common and useful pattern for MCP clients. In our agent, we intentionally allow parallel MCP tool calls to reduce latency. If one tool call gets a server-side HTTP 400 while another valid tool call is still in flight, the valid sibling call can fail too.
Related Issues
This seems related to but more specific than:
Those discuss non-2xx propagation and stream cleanup races. This issue is specifically about concurrent Streamable HTTP requests where one request's HTTP error escapes the per-request boundary and tears down sibling in-flight requests.
Example Code
Python & MCP Python SDK
Python 3.12
MCP Python SDK 1.26.0
I also inspected 1.27.1, which is currently the latest PyPI release, and the same relevant logic appears to still be present.
Activity
jshaofa-ui commentedon May 15, 2026
jshaofa-ui commentedon May 15, 2026
norika1207-lab commentedon Jun 2, 2026
maxisbey commentedon Jun 11, 2026
This is fixed on main by #2047, non-2xx responses get turned into a request-scoped JSON-RPC error instead of raising into the task group, so a 400 on one request no longer kills its siblings. Verified your concurrent scenario against current main and the sibling request completes fine.
https://github.com/modelcontextprotocol/python-sdk/blob/616476fc4f4a35ee3b1611548e80ccc0858d142e/src/mcp/client/streamable_http.py#L277-L282
We're not backporting it to v1.x. Any fix here changes what you get on a 4xx/5xx (from
ExceptionGroupwrappinghttpx.HTTPStatusErrorto anMcpError), and there's downstream code on 1.x that relies on the current shape for retry/auth handling. Not a change we want to make in a point release, so it lands with v2.Closing as fixed on main.
AI Disclaimer
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…
fix(mcp): keep short-TTL HTTP sessions alive with configurable ping k…