Skip to content

Commit bd9885f

Browse files
Add regression test for stateless request memory cleanup (#1140)
1 parent 95b44fb commit bd9885f

File tree

1 file changed

+64
-1
lines changed

1 file changed

+64
-1
lines changed

tests/server/test_streamable_http_manager.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Tests for StreamableHTTPSessionManager."""
22

3-
from unittest.mock import AsyncMock
3+
from unittest.mock import AsyncMock, patch
44

55
import anyio
66
import pytest
77

8+
from mcp.server import streamable_http_manager
89
from mcp.server.lowlevel import Server
910
from mcp.server.streamable_http import MCP_SESSION_ID_HEADER
1011
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
@@ -197,3 +198,65 @@ async def mock_receive():
197198
"Session ID should be removed from _server_instances after an exception"
198199
)
199200
assert not manager._server_instances, "No sessions should be tracked after the only session crashes"
201+
202+
203+
@pytest.mark.anyio
204+
async def test_stateless_requests_memory_cleanup():
205+
"""Test that stateless requests actually clean up resources using real transports."""
206+
app = Server("test-stateless-real-cleanup")
207+
manager = StreamableHTTPSessionManager(app=app, stateless=True)
208+
209+
# Track created transport instances
210+
created_transports = []
211+
212+
# Patch StreamableHTTPServerTransport constructor to track instances
213+
214+
original_constructor = streamable_http_manager.StreamableHTTPServerTransport
215+
216+
def track_transport(*args, **kwargs):
217+
transport = original_constructor(*args, **kwargs)
218+
created_transports.append(transport)
219+
return transport
220+
221+
with patch.object(streamable_http_manager, "StreamableHTTPServerTransport", side_effect=track_transport):
222+
async with manager.run():
223+
# Mock app.run to complete immediately
224+
app.run = AsyncMock(return_value=None)
225+
226+
# Send a simple request
227+
sent_messages = []
228+
229+
async def mock_send(message):
230+
sent_messages.append(message)
231+
232+
scope = {
233+
"type": "http",
234+
"method": "POST",
235+
"path": "/mcp",
236+
"headers": [
237+
(b"content-type", b"application/json"),
238+
(b"accept", b"application/json, text/event-stream"),
239+
],
240+
}
241+
242+
# Empty body to trigger early return
243+
async def mock_receive():
244+
return {
245+
"type": "http.request",
246+
"body": b"",
247+
"more_body": False,
248+
}
249+
250+
# Send a request
251+
await manager.handle_request(scope, mock_receive, mock_send)
252+
253+
# Verify transport was created
254+
assert len(created_transports) == 1, "Should have created one transport"
255+
256+
transport = created_transports[0]
257+
258+
# The key assertion - transport should be terminated
259+
assert transport._terminated, "Transport should be terminated after stateless request"
260+
261+
# Verify internal state is cleaned up
262+
assert len(transport._request_streams) == 0, "Transport should have no active request streams"

0 commit comments

Comments
 (0)