|
2 | 2 |
|
3 | 3 | import json |
4 | 4 | import logging |
5 | | -from typing import Any |
| 5 | +from typing import Any, cast |
6 | 6 | from unittest.mock import AsyncMock, patch |
7 | 7 |
|
8 | 8 | import anyio |
9 | 9 | import httpx |
10 | 10 | import pytest |
11 | | -from mcp_types import INVALID_REQUEST, ListToolsResult, PaginatedRequestParams |
| 11 | +from mcp_types import INVALID_REQUEST, JSONRPCRequest, ListToolsResult, PaginatedRequestParams |
12 | 12 | from starlette.types import Message, Scope |
13 | 13 |
|
14 | 14 | from mcp import Client |
|
18 | 18 | from mcp.server.auth.provider import AccessToken |
19 | 19 | from mcp.server.streamable_http import MCP_SESSION_ID_HEADER, StreamableHTTPServerTransport |
20 | 20 | from mcp.server.streamable_http_manager import StreamableHTTPSessionManager |
| 21 | +from mcp.shared.message import SessionMessage |
21 | 22 |
|
22 | 23 |
|
23 | 24 | @pytest.mark.anyio |
@@ -101,6 +102,49 @@ async def running_manager(): |
101 | 102 | yield manager, app |
102 | 103 |
|
103 | 104 |
|
| 105 | +@pytest.mark.anyio |
| 106 | +async def test_streamable_http_post_sse_cleans_up_streams_when_response_returns(monkeypatch: pytest.MonkeyPatch): |
| 107 | + transport = StreamableHTTPServerTransport(mcp_session_id=None) |
| 108 | + sent_messages: list[Message] = [] |
| 109 | + body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}).encode() |
| 110 | + |
| 111 | + class DisconnectingEventSourceResponse: |
| 112 | + def __init__(self, *args: Any, **kwargs: Any) -> None: |
| 113 | + pass |
| 114 | + |
| 115 | + async def __call__(self, scope: Scope, receive: Any, send: Any) -> None: |
| 116 | + await send({"type": "http.response.start", "status": 200, "headers": []}) |
| 117 | + |
| 118 | + async def send(message: Message) -> None: |
| 119 | + sent_messages.append(message) |
| 120 | + |
| 121 | + async def receive() -> Message: |
| 122 | + return {"type": "http.request", "body": body, "more_body": False} |
| 123 | + |
| 124 | + scope: Scope = { |
| 125 | + "type": "http", |
| 126 | + "method": "POST", |
| 127 | + "path": "/mcp", |
| 128 | + "headers": [ |
| 129 | + (b"accept", b"application/json, text/event-stream"), |
| 130 | + (b"content-type", b"application/json"), |
| 131 | + ], |
| 132 | + } |
| 133 | + |
| 134 | + monkeypatch.setattr("mcp.server.streamable_http.EventSourceResponse", DisconnectingEventSourceResponse) |
| 135 | + |
| 136 | + async with transport.connect() as (read_stream, _write_stream): |
| 137 | + async with anyio.create_task_group() as tg: |
| 138 | + tg.start_soon(transport.handle_request, scope, receive, send) |
| 139 | + session_message = cast(SessionMessage, await read_stream.receive()) |
| 140 | + assert isinstance(session_message.message, JSONRPCRequest) |
| 141 | + assert session_message.message.method == "tools/list" |
| 142 | + |
| 143 | + assert transport._request_streams == {} |
| 144 | + assert transport._sse_stream_writers == {} |
| 145 | + assert any(message["type"] == "http.response.start" for message in sent_messages) |
| 146 | + |
| 147 | + |
104 | 148 | @pytest.mark.anyio |
105 | 149 | async def test_stateful_session_cleanup_on_graceful_exit(running_manager: tuple[StreamableHTTPSessionManager, Server]): |
106 | 150 | manager, _app = running_manager |
|
0 commit comments