Skip to content

Commit ee0b868

Browse files
committed
fix(sse): add configurable max_sse_event_size to sse_client and SseServerParameters (fixes #3803)
1 parent 77001c9 commit ee0b868

4 files changed

Lines changed: 65 additions & 3 deletions

File tree

‎src/mcp/client/session_group.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class SseServerParameters(BaseModel):
4343
# Timeout for SSE read operations (in seconds).
4444
sse_read_timeout: float = 300.0
4545

46+
# Maximum bytes in one server-sent event. None disables the limit.
47+
max_sse_event_size: int | None = Field(default=DEFAULT_MAX_SSE_EVENT_SIZE, gt=0)
48+
4649

4750
class StreamableHttpParameters(BaseModel):
4851
"""Parameters for initializing a streamable_http_client."""
@@ -321,6 +324,7 @@ async def _establish_session(
321324
headers=server_params.headers,
322325
timeout=server_params.timeout,
323326
sse_read_timeout=server_params.sse_read_timeout,
327+
max_sse_event_size=server_params.max_sse_event_size,
324328
)
325329
read, write = await session_stack.enter_async_context(client)
326330
else:

‎src/mcp/client/sse.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
logger = logging.getLogger(__name__)
2424

25+
DEFAULT_MAX_SSE_EVENT_SIZE = 1024 * 1024
26+
2527

2628
def remove_request_params(url: str) -> str:
2729
return urljoin(url, urlparse(url).path)
@@ -41,6 +43,7 @@ async def sse_client(
4143
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
4244
auth: httpx2.Auth | None = None,
4345
on_session_created: Callable[[str], None] | None = None,
46+
max_sse_event_size: int | None = DEFAULT_MAX_SSE_EVENT_SIZE,
4447
):
4548
"""Client transport for SSE.
4649
@@ -62,12 +65,15 @@ async def sse_client(
6265
they make.
6366
auth: Optional httpx2 authentication handler.
6467
on_session_created: Optional callback invoked with the session ID when received.
68+
max_sse_event_size: Maximum bytes buffered for one SSE event. None disables the limit.
6569
"""
70+
if max_sse_event_size is not None and max_sse_event_size <= 0:
71+
raise ValueError("max_sse_event_size must be positive or None")
6672
logger.debug(f"Connecting to SSE endpoint: {remove_request_params(url)}")
6773
async with httpx_client_factory(
6874
headers=headers, auth=auth, timeout=httpx2.Timeout(timeout, read=sse_read_timeout)
6975
) as client:
70-
async with sse_within_origin(client, url) as event_source:
76+
async with sse_within_origin(client, url, max_event_size=max_sse_event_size) as event_source:
7177
event_source.response.raise_for_status()
7278
logger.debug("SSE connection established")
7379

‎tests/client/test_session_group.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,10 @@ async def test_client_session_group_disconnect_non_existent_server():
306306
"mcp.client.session_group.mcp.stdio_client",
307307
),
308308
(
309-
SseServerParameters(url="http://test.com/sse", timeout=10.0),
309+
SseServerParameters(url="http://test.com/sse", timeout=10.0, max_sse_event_size=32 * 1024 * 1024),
310310
"sse",
311311
"mcp.client.session_group.sse_client",
312-
), # url, headers, timeout, sse_read_timeout
312+
), # url, headers, timeout, sse_read_timeout, max_sse_event_size
313313
(
314314
StreamableHttpParameters(
315315
url="http://test.com/stream", terminate_on_close=False, max_sse_event_size=32 * 1024 * 1024
@@ -374,6 +374,7 @@ async def test_client_session_group_establish_session_parameterized(
374374
headers=server_params_instance.headers,
375375
timeout=server_params_instance.timeout,
376376
sse_read_timeout=server_params_instance.sse_read_timeout,
377+
max_sse_event_size=server_params_instance.max_sse_event_size,
377378
)
378379
elif client_type_name == "streamablehttp": # pragma: no branch
379380
assert isinstance(server_params_instance, StreamableHttpParameters)

‎tests/shared/test_sse.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,54 @@ async def test_sse_session_cleanup_on_disconnect() -> None:
523523
headers={"Content-Type": "application/json"},
524524
)
525525
assert response.status_code == 404
526+
527+
528+
@pytest.mark.anyio
529+
async def test_sse_client_none_disables_the_sse_event_limit() -> None:
530+
"""Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/3803
531+
532+
Passing max_sse_event_size=None to sse_client disables the 1 MiB default SSE
533+
event size limit so tool results over 1 MiB can be received over SSE.
534+
"""
535+
payload = "x" * (2 * 1024 * 1024)
536+
response = JSONRPCResponse(
537+
jsonrpc="2.0",
538+
id=1,
539+
result={"text": payload},
540+
)
541+
response_json = response.model_dump_json(by_alias=True, exclude_none=True)
542+
event_stream = (
543+
"event: endpoint\ndata: /messages/?session_id=abc123\n\n"
544+
f"event: message\ndata: {response_json}\n\n"
545+
)
546+
547+
def serve(request: httpx2.Request) -> httpx2.Response:
548+
return httpx2.Response(200, headers={"content-type": "text/event-stream"}, text=event_stream)
549+
550+
def factory(
551+
headers: dict[str, str] | None = None,
552+
timeout: httpx2.Timeout | None = None,
553+
auth: httpx2.Auth | None = None,
554+
) -> httpx2.AsyncClient:
555+
return httpx2.AsyncClient(transport=httpx2.MockTransport(serve))
556+
557+
with anyio.fail_after(5):
558+
async with sse_client("http://test/sse", httpx_client_factory=factory, max_sse_event_size=None) as (
559+
read_stream,
560+
_,
561+
):
562+
msg = await read_stream.receive()
563+
564+
assert isinstance(msg, SessionMessage)
565+
assert isinstance(msg.message, types.JSONRPCResponse)
566+
assert msg.message.result == {"text": payload}
567+
568+
569+
@pytest.mark.anyio
570+
@pytest.mark.parametrize("invalid_size", [0, -1])
571+
async def test_sse_client_rejects_non_positive_max_sse_event_size(invalid_size: int) -> None:
572+
"""sse_client rejects zero or negative max_sse_event_size values."""
573+
with pytest.raises(ValueError, match="max_sse_event_size must be positive or None"):
574+
async with sse_client("http://test/sse", max_sse_event_size=invalid_size):
575+
pytest.fail("should not connect") # pragma: no cover
576+

0 commit comments

Comments
 (0)