modelcontextprotocol / modelcontextprotocol/python-sdk
streamable-http client: no size bound before JSONRPCMessage.model_validate_json — one large server message can OOM the client
还没有人认领这个 Issue。
- 主要语言
- Python
- 星标
- 24.3k
- 派生
- 4k
- 平均合并
- 1 天 1 小时
- 30 天内合并 PR
- 31
描述
Summary
StreamableHTTPTransport parses every inbound SSE event and JSON response body with JSONRPCMessage.model_validate_json / model_validate_json(content) and no size bound. Because pydantic validation of a large JSON document allocates several times the wire size in live Python objects, a single oversized message from a server can exhaust the client's memory. There is no hook to inspect or reject a message before it is parsed.
We hit this in production: a client connected to ~30 MCP servers, one of which answered tools/list with a 21.9 MB single SSE event. Measured with tracemalloc at the parse site:
#1 663.4 MB in 9,957,044 blocks
mcp/client/streamable_http.py:217
message = JSONRPCMessage.model_validate_json(sse.data)
#2 46.9 MB in 3 blocks
httpx_sse/_decoders.py:61 (whole event buffered as one string)
#3 46.9 MB in 2 blocks
httpx_sse/_decoders.py:120 (copy from slicing)
Roughly a 7× amplification of the wire size in live objects, transient but concurrent with other parses. A trivial request that triggered only catalogue loading peaked at 2.35 GB RSS from a 56 MB baseline; heavier concurrent work reached 4.2 GB. Removing that one server dropped the same request's peak to 456 MB. Versions: mcp 1.27.1, Python 3.14.
Why a client-side bound is needed
The client cannot know in advance that a server will return a huge payload, and a misbehaving or misconfigured server should not be able to OOM its client. Today the only outcome is process death with nothing identifying the responsible server — the failure surfaces as an unexplained kill rather than an actionable error.
What we did as a workaround, and why it was awkward
We wrapped StreamableHTTPTransport._handle_sse_event and _handle_json_response to measure sse.data / the response body and reject anything over a configured cap before parsing. Two things made this harder than expected, and both seem worth addressing upstream:
- Raising from the handler is not viable. Every call site wraps it in
except Exception: logger.debug(...)and then reconnects withLast-Event-ID, which replays the same oversized payload. The raise is swallowed and the pending request hangs. - Delivering a bare
Exceptionon the read stream does not fail the request either. InBaseSession._receive_loop,isinstance(message, Exception)routes to_handle_incoming, which for the default message handler is a no-op; only aJSONRPCResponse/JSONRPCErrormatching_response_streams[request_id]completessend_request, and that awaits withtimeout=None. To fail the request we had to synthesize aJSONRPCErrorand recover the request id from the raw payload with a regex, since parsing it is exactly what we were trying to avoid.
Suggested improvements
- An optional client-side maximum message size (constructor argument and/or environment variable) enforced before
model_validate_json, on both the SSE andapplication/jsonpaths. - When it trips, fail the corresponding pending request with a distinct error identifying the endpoint and the observed size, rather than letting the reconnect path replay the payload.
- Failing that, a documented hook to inspect a raw message before parsing would let clients implement this without patching private methods.
- Independently:
_handle_sse_eventreturningFalsefor a parse failure causes the caller to treat the stream as ended and reconnect withLast-Event-ID; for a deterministic failure (such as a payload that will always be too large, or malformed JSON) this retries something that cannot succeed.
Happy to open a PR if a maintainer indicates the preferred shape (constructor arg vs. env var vs. pre-parse hook).
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
从 mcp/client/streamable_http.py 中第 217 行附近的 SSE 解析位置开始,检查两条入站路径中的 _handle_sse_event 和 _handle_json_response。然后阅读 BaseSession._receive_loop 和 _handle_incoming,以了解待处理请求的失败处理。完成标准是:超大消息在 model_validate_json 之前被拒绝,匹配的请求因一个可采取行动的错误而失败,并且重新连接不会重放该 payload。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- python
- 领域
- api, networking
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 48/100