streamable-http client: no size bound before JSONRPCMessage.model_validate_json — one large server message can OOM the client
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- python
- Lĩnh vực
- api, networking
Hướng nghiên cứu
Bắt đầu trong mcp/client/streamable_http.py tại vị trí phân tích SSE quanh dòng 217 và kiểm tra _handle_sse_event cùng _handle_json_response cho cả hai đường dẫn inbound. Sau đó đọc BaseSession._receive_loop và _handle_incoming để hiểu cách xử lý lỗi của các request đang chờ. Hoàn thành khi các message quá lớn bị từ chối trước model_validate_json, request tương ứng thất bại với một lỗi có thể xử lý được, và việc reconnect không phát lại payload.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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).
- Ngôn ngữ chính
- Python
- Star
- 24.3k
- Fork
- 4k
- Merge trung bình
- 1 ngày 19 phút
- Pull request đã merge (30 ngày)
- 29
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của modelcontextprotocol/python-sdk
-
Streamable HTTP client logs a WARNING for valid 202 Accepted on session termination (DELETE) Đang mởv1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
modelcontextprotocol/python-sdk#3546 · 5 bình luận ·
-
v1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
modelcontextprotocol/python-sdk#3545 · 1 bình luận ·
-
v1 v2
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 91/100
modelcontextprotocol/python-sdk#3508 · 2 bình luận ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 64/100
modelcontextprotocol/python-sdk#3504 ·
-
v1 v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
modelcontextprotocol/python-sdk#3492 · 1 bình luận ·
Tất cả issue của modelcontextprotocol/python-sdk
Issue tương tự
-
from:qa priority:P2 reliability tech-debt
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
spec-kitty/spec-kitty#4874 ·
-
fix: inaccuracy ⚠️
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
uabrc/uabrc.github.io#1255 · 1 bình luận ·
-
kind:bug needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
-
docs
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
ethereum-optimism/factory#64 ·