diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index 50bcd5e79..cca58395e 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -225,7 +225,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE read_stream, write_stream = streams task_status.started() try: - # Use a cancel scope for idle timeout — when the + # Use a cancel scope for idle timeout \u2014 when the # deadline passes the scope cancels app.run() and # execution continues after the ``with`` block. # Incoming requests push the deadline forward. @@ -272,6 +272,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE # Unknown or expired session ID - return 404 per MCP spec # TODO: Align error code once spec clarifies # See: https://github.com/modelcontextprotocol/python-sdk/issues/1821 + logger.warning("Rejected request with unknown or expired session ID: %s", request_mcp_session_id) error_response = JSONRPCError( jsonrpc="2.0", id=None, diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index 54a898cc5..68a845df4 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -1,6 +1,7 @@ """Tests for StreamableHTTPSessionManager.""" import json +import logging from typing import Any from unittest.mock import AsyncMock, patch @@ -269,7 +270,7 @@ async def mock_receive(): @pytest.mark.anyio -async def test_unknown_session_id_returns_404(): +async def test_unknown_session_id_returns_404(caplog: pytest.LogCaptureFixture): """Test that requests with unknown session IDs return HTTP 404 per MCP spec.""" app = Server("test-unknown-session") manager = StreamableHTTPSessionManager(app=app) @@ -299,7 +300,8 @@ async def mock_send(message: Message): async def mock_receive(): return {"type": "http.request", "body": b"{}", "more_body": False} # pragma: no cover - await manager.handle_request(scope, mock_receive, mock_send) + with caplog.at_level(logging.WARNING, logger="mcp.server.streamable_http_manager"): + await manager.handle_request(scope, mock_receive, mock_send) # Find the response start message response_start = next( @@ -316,6 +318,11 @@ async def mock_receive(): assert error_data["error"]["code"] == INVALID_REQUEST assert error_data["error"]["message"] == "Session not found" + # Verify warning was logged with the session ID + assert any("non-existent-session-id" in record.message for record in caplog.records), ( + "Should log a warning with the unknown session ID" + ) + @pytest.mark.anyio async def test_e2e_streamable_http_server_cleanup():