|
14 | 14 | import mcp_types as types |
15 | 15 | import pytest |
16 | 16 | from inline_snapshot import snapshot |
17 | | -from mcp_types import INVALID_REQUEST, CallToolResult, ErrorData, ListToolsResult, TextContent, Tool |
| 17 | +from mcp_types import ( |
| 18 | + INVALID_REQUEST, |
| 19 | + CallToolResult, |
| 20 | + ErrorData, |
| 21 | + ListToolsResult, |
| 22 | + LoggingMessageNotification, |
| 23 | + LoggingMessageNotificationParams, |
| 24 | + TextContent, |
| 25 | + Tool, |
| 26 | +) |
18 | 27 | from starlette.types import Receive, Scope, Send |
19 | 28 |
|
20 | 29 | from mcp import MCPError |
21 | 30 | from mcp.client.client import Client |
22 | 31 | from mcp.client.streamable_http import streamable_http_client |
23 | 32 | from mcp.server import Server, ServerRequestContext |
| 33 | +from mcp.server.mcpserver import Context, MCPServer |
24 | 34 | from tests.interaction._connect import BASE_URL, NO_DNS_REBINDING_PROTECTION, client_via_http, mounted_app |
25 | 35 | from tests.interaction._requirements import requirement |
26 | 36 | from tests.interaction.transports._bridge import StreamingASGITransport |
@@ -158,6 +168,67 @@ async def call(n: int) -> None: |
158 | 168 | assert len(tools_call_posts) == 3 |
159 | 169 |
|
160 | 170 |
|
| 171 | +# One byte past the 1 MiB that httpx2 >= 2.10 allows a single SSE event by default. |
| 172 | +_OVERSIZED_TEXT = "x" * (1024 * 1024 + 1) |
| 173 | + |
| 174 | + |
| 175 | +@requirement("client-transport:http:post-stream-large-event") |
| 176 | +async def test_a_post_stream_delivers_a_tool_result_larger_than_one_mebibyte() -> None: |
| 177 | + """A tool result bigger than httpx2's default per-event SSE cap arrives intact over the request's |
| 178 | + POST stream. SDK-defined: MCP sets no message size limit, so the transport lifts the cap (#3332).""" |
| 179 | + mcp = MCPServer("bulky") |
| 180 | + |
| 181 | + @mcp.tool() |
| 182 | + def bulk() -> str: |
| 183 | + """Return more than one SSE event may carry by default.""" |
| 184 | + return _OVERSIZED_TEXT |
| 185 | + |
| 186 | + async with mounted_app(mcp) as (http, _), client_via_http(http) as client: |
| 187 | + with anyio.fail_after(5): |
| 188 | + result = await client.call_tool("bulk", {}) |
| 189 | + |
| 190 | + assert result.content == [TextContent(text=_OVERSIZED_TEXT)] |
| 191 | + |
| 192 | + |
| 193 | +@requirement("client-transport:http:get-stream-large-event") |
| 194 | +async def test_the_standalone_get_stream_delivers_a_notification_larger_than_one_mebibyte() -> None: |
| 195 | + """A server-initiated notification bigger than httpx2's default per-event SSE cap arrives intact |
| 196 | + over the standalone GET stream, which the transport opens with the same lifted cap (#3332).""" |
| 197 | + mcp = MCPServer("bulky") |
| 198 | + |
| 199 | + @mcp.tool() |
| 200 | + async def shout(ctx: Context) -> str: |
| 201 | + """Emit one unrelated notification, which the server routes to the standalone stream.""" |
| 202 | + params = LoggingMessageNotificationParams(level="info", data=_OVERSIZED_TEXT) |
| 203 | + await ctx.session.send_notification(LoggingMessageNotification(params=params)) |
| 204 | + return "sent" |
| 205 | + |
| 206 | + get_stream_open = anyio.Event() |
| 207 | + |
| 208 | + async def on_response(response: httpx2.Response) -> None: |
| 209 | + if response.request.method == "GET": |
| 210 | + get_stream_open.set() |
| 211 | + |
| 212 | + received: list[object] = [] |
| 213 | + delivered = anyio.Event() |
| 214 | + |
| 215 | + async def collect(params: LoggingMessageNotificationParams) -> None: |
| 216 | + received.append(params.data) |
| 217 | + delivered.set() |
| 218 | + |
| 219 | + async with ( |
| 220 | + mounted_app(mcp, on_response=on_response) as (http, _), |
| 221 | + client_via_http(http, logging_callback=collect) as client, |
| 222 | + ): |
| 223 | + with anyio.fail_after(5): |
| 224 | + # The server drops standalone messages emitted before the GET stream is established. |
| 225 | + await get_stream_open.wait() |
| 226 | + await client.call_tool("shout", {}) |
| 227 | + await delivered.wait() |
| 228 | + |
| 229 | + assert received == [_OVERSIZED_TEXT] |
| 230 | + |
| 231 | + |
161 | 232 | @requirement("client-transport:http:sse-405-tolerated") |
162 | 233 | @requirement("client-transport:http:terminate-405-ok") |
163 | 234 | async def test_client_tolerates_405_on_get_and_delete() -> None: |
|
0 commit comments