Skip to content

Lift httpx2's default SSE event size cap on every client SSE reader - #3338

Draft
maxisbey wants to merge 1 commit into
mainfrom
fix/sse-max-event-size
Draft

Lift httpx2's default SSE event size cap on every client SSE reader#3338
maxisbey wants to merge 1 commit into
mainfrom
fix/sse-max-event-size

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Pass an explicit max_event_size=None to every SSE reader the client opens, so httpx2 ≥ 2.10's new 1 MiB per-event default no longer rejects large MCP messages. Fixes #3332.

Motivation and Context

httpx2 2.10 (pydantic/httpx2#1071) started capping a single server-sent event at 1 MiB by default, on client.sse() and on a bare EventSource(response) alike, raising SSEError past it. The SDK constructs both without an explicit size at five sites (POST response stream, standalone GET stream, resumption, reconnection, and the legacy SSE transport), so on a fresh install any JSON-RPC message over ~1 MiB delivered over SSE fails. The caller sees MCPError -32000 "SSE stream ended without a response"; the actual cause is only logged at DEBUG. With an event store the client also replays the same oversized event on each reconnect attempt before giving up. Server-initiated messages over the limit are dropped silently, and a second one kills the GET stream for the session.

Scope: on the 2026-07-28 path a plain result comes back as application/json and is unaffected; SSE (and the cap) kicks in once the handler emits a notification or runs past the deferral window. On 2025-xx servers every response is SSE unless json_response=True. The application/json path has no bound, and neither does MCP, so this restores the pre-2.10 behaviour rather than introducing a different cap. A deliberate, consistently-applied client-side message size limit is a separate design (#3330 asks for exactly that, from the other direction) and is not attempted here.

The floor moves to httpx2>=2.10.0, the first version that accepts the argument. uv.lock still pins 2.5.0 in this draft; it needs to move to ≥ 2.10 before this is ready, both so CI exercises the fix and so pyright sees the parameter.

How Has This Been Tested?

Three regression tests drive the public API end to end over the in-process ASGI bridge: a tool result past 1 MiB over the POST stream, a server-initiated notification past 1 MiB over the standalone GET stream, and a resource read past 1 MiB over the legacy sse_client. All three reproduce the issue's failure against httpx2 2.10's decoder without the fix and pass with it. Also driven by hand through mcp.Client with 2 MiB and 16 MiB (multi-byte) results in legacy and auto modes, with and without an event store (no replays), and with oversized request-scoped and standalone notifications.

Note: the httpx2 ≥ 2.10 runs here were done with 2.10.0's _sse.py/sse() applied over an older install, since the locked version is still 2.5.0; a real run against 2.12 is pending the lock bump.

Breaking Changes

None for callers. Dependency floor raised from httpx2>=2.5.0 to httpx2>=2.10.0.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Deliberately not exposing max_event_size as a public knob: it is an httpx2-flavoured, SSE-only setting, and bounding only one of the two response encodings is a behavioural hazard rather than a memory bound. If the SDK grows an inbound message size limit it should cover the SSE and JSON paths together, treat the oversize case as non-retryable, and surface a request-scoped error; that's the follow-up to #3330 and the remaining half of #3332. Surfacing the underlying exception text in the "stream ended" error also belongs there.

Cross-SDK for reference: TypeScript and C# are unbounded; Go had a 1 MiB cap and removed it after user reports (go-sdk#726); Rust and Kotlin ship configurable 16 MiB per-event caps.

AI Disclaimer

httpx2 2.10 caps a single server-sent event at 1 MiB by default, on both
`client.sse()` and a bare `EventSource(response)`, and raises `SSEError`
past it. A JSON-RPC message is one event and MCP sets no message size
limit, so any tool result or server notification over ~1 MiB delivered
over SSE failed with `SSE stream ended without a response` (the cause
was logged at DEBUG only), and with an event store the client also
replayed the same oversized event on each reconnect attempt.

Pass `max_event_size=None` at all five sites - the POST response stream,
the standalone GET stream, resumption, reconnection, and the legacy SSE
transport - which restores the pre-2.10 behaviour and matches the
unbounded `application/json` response path. The floor moves to
`httpx2>=2.10.0`, the first version that accepts the argument.

Fixes #3332
@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3338.mcp-python-docs.pages.dev
Deployment https://766a9cd9.mcp-python-docs.pages.dev
Commit dd16fe0
Triggered by @maxisbey
Updated 2026-08-19 16:52:53 UTC

@maxisbey
maxisbey marked this pull request as ready for review August 19, 2026 16:55
@maxisbey
maxisbey marked this pull request as draft August 19, 2026 17:01
Comment thread pyproject.toml
"anyio>=4.10; python_version >= '3.14'",
"anyio>=4.9; python_version < '3.14'",
"httpx2>=2.5.0",
"httpx2>=2.10.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 httpx2 floor raised to >=2.10.0 but uv.lock is not regenerated and still pins httpx2 2.5.0, which has no max_event_size parameter

Extended reasoning...

The diff does not touch uv.lock (it still locks httpx2-2.5.0 with specifier '>=2.5.0', see uv.lock lines 676-688 and the '>=2.5.0' specifier entries). Any environment built from the lock (uv run --frozen, uv sync --locked, CI, ./scripts/test) installs httpx2 2.5.0, whose client.sse() and EventSource.__init__ do not accept max_event_size. Every SSE connection the client opens — legacy sse_client (src/mcp/client/sse.py:58) and all four sites in src/mcp/client/streamable_http.py (lines 213, 256, 426, 504) — raises TypeError: unexpected keyword argument 'max_event_size', so all streamable-HTTP/SSE client tests fail; additionally pyright fails on the unknown parameter and the pre-commit uv.lock consistency check rejects the mismatched pyproject/lock. The PR is broken as-is until uv lock --upgrade-package httpx2 is run and committed.

Verification: normal — The diff raises the floor in pyproject.toml line 133 ("httpx2>=2.5.0" -> "httpx2>=2.10.0") but does not touch uv.lock: the changed-file list is 7 files with no uv.lock, and /home/claude/python-sdk/uv.lock still locks httpx2 at 2.5.0 (lines 676-677: name = "httpx2" / version = "2.5.0", wheel httpx2-2.5.0-py3-none-any.whl at line 688) with every specifier entry still ">=2.5.0" (li

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea this pushw as a mistake

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't mean to make a pr just yet

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pyproject.toml">

<violation number="1" location="pyproject.toml:133">
P2: This dependency-floor change must be accompanied by a regenerated `uv.lock`; otherwise the repository's frozen CI sync sees stale `httpx2>=2.5.0` metadata and the old 2.5.0 artifact. Regenerate and commit the lockfile so CI installs at least 2.10.0.</violation>
</file>

<file name="tests/interaction/transports/test_client_transport_http.py">

<violation number="1" location="tests/interaction/transports/test_client_transport_http.py:176">
P3: These two new tests contradict the file's stated scope. Its docstring says every test records the HTTP requests the SDK client issues and asserts on what the transport sends (headers, methods, ordering), never on what the protocol layer returns — but both new tests assert on Client-visible protocol results (result.content and the logging callback) and record no wire traffic. Either move them to a behavioural transport-test file or update the docstring so the file's stated purpose stays accurate.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread pyproject.toml
"anyio>=4.10; python_version >= '3.14'",
"anyio>=4.9; python_version < '3.14'",
"httpx2>=2.5.0",
"httpx2>=2.10.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This dependency-floor change must be accompanied by a regenerated uv.lock; otherwise the repository's frozen CI sync sees stale httpx2>=2.5.0 metadata and the old 2.5.0 artifact. Regenerate and commit the lockfile so CI installs at least 2.10.0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 133:

<comment>This dependency-floor change must be accompanied by a regenerated `uv.lock`; otherwise the repository's frozen CI sync sees stale `httpx2>=2.5.0` metadata and the old 2.5.0 artifact. Regenerate and commit the lockfile so CI installs at least 2.10.0.</comment>

<file context>
@@ -130,7 +130,7 @@ dependencies = [
     "anyio>=4.10; python_version >= '3.14'",
     "anyio>=4.9; python_version < '3.14'",
-    "httpx2>=2.5.0",
+    "httpx2>=2.10.0",
     "mcp-types=={{ version }}",
     "pydantic>=2.12.0",
</file context>



@requirement("client-transport:http:post-stream-large-event")
async def test_a_post_stream_delivers_a_tool_result_larger_than_one_mebibyte() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: These two new tests contradict the file's stated scope. Its docstring says every test records the HTTP requests the SDK client issues and asserts on what the transport sends (headers, methods, ordering), never on what the protocol layer returns — but both new tests assert on Client-visible protocol results (result.content and the logging callback) and record no wire traffic. Either move them to a behavioural transport-test file or update the docstring so the file's stated purpose stays accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/interaction/transports/test_client_transport_http.py, line 176:

<comment>These two new tests contradict the file's stated scope. Its docstring says every test records the HTTP requests the SDK client issues and asserts on what the transport sends (headers, methods, ordering), never on what the protocol layer returns — but both new tests assert on Client-visible protocol results (result.content and the logging callback) and record no wire traffic. Either move them to a behavioural transport-test file or update the docstring so the file's stated purpose stays accurate.</comment>

<file context>
@@ -158,6 +168,67 @@ async def call(n: int) -> None:
+
+
+@requirement("client-transport:http:post-stream-large-event")
+async def test_a_post_stream_delivers_a_tool_result_larger_than_one_mebibyte() -> None:
+    """A tool result bigger than httpx2's default per-event SSE cap arrives intact over the request's
+    POST stream. SDK-defined: MCP sets no message size limit, so the transport lifts the cap (#3332)."""
</file context>

@Zhangs-11

Copy link
Copy Markdown

I independently validated dd16fe06 against HTTPX2 2.10.0.

The two affected upstream test files pass: 40 tests covering Streamable HTTP POST/GET and legacy SSE.

I also exercised two additional production-shaped cases on both asyncio and trio:

  • a 16 MiB tool result concurrent with a sibling call, followed by another call on the same session; all results arrived intact and the large tool executed once;
  • a forced event-store reconnect carrying a 16 MiB result; replay completed successfully and the tool handler still executed once.

I found no additional correctness issue in the five updated SSE reader paths. This independently confirms the fix for the scenario reported in #3332.

This validation was prepared with AI assistance and reviewed by me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v2] Expose the SSE max_event_size setting in Streamable HTTP clients

2 participants