Streamable HTTP client hangs on a null-id JSON-RPC error in a 200 JSON response
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
src/mcp/client/streamable_http.py の _handle_json_response から始め、その処理を _handle_sse_event および _handle_post_request と比較します。tests/client/test_notification_response.py にある既存の non-2xx null-id テストの隣に回帰テストを追加します。完了条件は、id null の 200 JSON-RPC エラーによって、リクエストが待機したままになるのではなく MCPError が発生することです。
索引モデルが issue の本文から書いたものです。
説明
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- 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)
Description
On current main (2118f14f), when a server answers a request POST with a 200 application/json body holding a JSON-RPC error with "id": null, the streamable HTTP client never resolves the call. session.list_tools() (or any request) waits forever, or until a read timeout if one is set.
JSON-RPC 2.0 allows id: null on an error when the server could not determine the request id. Gateways and non-SDK servers can send one with status 200.
_handle_json_response in src/mcp/client/streamable_http.py forwards the parsed message unchanged. The dispatcher then drops it as a response to an unknown id (jsonrpc_dispatcher.py, "unknown/late request id").
The other two response paths in the same transport already handle this:
- SSE (
_handle_sse_event): replaces a response's id with the original request id. - Non-2xx JSON body (
_handle_post_request): rebuilds the error under the request's id. The comment there says: "The server may have setid: null(request rejected before its id was parsed); use this request's id so correlation works."
So the same error body resolves the call over SSE or with a 4xx status, but hangs with a 200.
Expected: the call raises MCPError with the server's error, the same as the SSE and non-2xx paths.
Proposed fix: in _handle_json_response, when the parsed message is a JSONRPCError, set its id to the POST's request id. A 2xx JSON body answers exactly that POST, so nothing else can be waiting on it. That is a 4-line change plus a regression test next to the existing non-2xx null-id test in tests/client/test_notification_response.py.
I'd like to fix this and have a branch ready: https://github.com/hyeonsang010716/python-sdk/tree/fix/streamable-http-json-null-id
I found and prepared this with AI assistance. I've reviewed the change, checked the repro below fails on main and passes with the fix, and can explain it.
Example Code
import json
import anyio
import httpx2
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route
from mcp import ClientSession, MCPError
from mcp.client.streamable_http import streamable_http_client
async def handle_mcp(request: Request) -> Response:
data = json.loads(await request.body())
if data.get("method") == "initialize":
result = {"protocolVersion": "2025-06-18", "capabilities": {}, "serverInfo": {"name": "s", "version": "1"}}
return JSONResponse({"jsonrpc": "2.0", "id": data["id"], "result": result})
if "id" not in data:
return Response(status_code=202)
# e.g. a gateway that fails before it has parsed the request id
return JSONResponse({"jsonrpc": "2.0", "id": None, "error": {"code": -32603, "message": "upstream failed"}})
async def main() -> None:
app = Starlette(routes=[Route("/mcp", handle_mcp, methods=["POST"])])
async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app)) as http:
async with streamable_http_client("http://localhost/mcp", http_client=http) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
try:
with anyio.fail_after(3):
await session.list_tools()
except MCPError as exc:
print("raised MCPError:", exc.error.message)
except TimeoutError:
print("list_tools() never returned")
anyio.run(main)
Output on main:
list_tools() never returned
Output with the fix:
raised MCPError: upstream failed
Python & MCP Python SDK
Python 3.14.7
mcp main @ 2118f14f (also present in v2.3.0)
- 主要言語
- Python
- スター
- 24.5k
- フォーク
- 4k
- 平均マージ
- 1日 4時間
- マージ済み PR(30日)
- 33
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/python-sdk のほかの issue
-
bug v1 v2
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
modelcontextprotocol/python-sdk#3670 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
documentation v2
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
modelcontextprotocol/python-sdk#3662 ·
メンテナーはふだん 1 日以内に返信
-
Audio(data=b"") raises "Either path or data can be provided", while Image(data=b"") works対応中かも @KaiyiQuan が 2 日前に担当しました。 オープンbug v1 v2
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
modelcontextprotocol/python-sdk#3656 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
modelcontextprotocol/python-sdk#3655 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 1/5 1時間未満 初心者へのやさしさ 86/100
modelcontextprotocol/python-sdk#3654 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/python-sdk の issue をすべて見る
似ている issue
-
enhancement good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
python-version
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 1 日以内に返信
-
bug javascript P2-medium python release:v3.1
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
adrirubio/claude-deck#546 ·
メンテナーはふだん 1 日以内に返信
-
area: desktop area: website priority: P2 type: feature
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
appandflow/stim#3411 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信