Skip to content

Commit 78a9504

Browse files
fix: return HTTP 404 for unknown session IDs instead of 400 (#1808)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
1 parent a9cc822 commit 78a9504

File tree

2 files changed

+67
-4
lines changed

2 files changed

+67
-4
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
StreamableHTTPServerTransport,
2323
)
2424
from mcp.server.transport_security import TransportSecuritySettings
25+
from mcp.types import INVALID_REQUEST, ErrorData, JSONRPCError
2526

2627
logger = logging.getLogger(__name__)
2728

@@ -276,10 +277,21 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
276277

277278
# Handle the HTTP request and return the response
278279
await http_transport.handle_request(scope, receive, send)
279-
else: # pragma: no cover
280-
# Invalid session ID
280+
else:
281+
# Unknown or expired session ID - return 404 per MCP spec
282+
# TODO: Align error code once spec clarifies
283+
# See: https://github.com/modelcontextprotocol/python-sdk/issues/1821
284+
error_response = JSONRPCError(
285+
jsonrpc="2.0",
286+
id="server-error",
287+
error=ErrorData(
288+
code=INVALID_REQUEST,
289+
message="Session not found",
290+
),
291+
)
281292
response = Response(
282-
"Bad Request: No valid session ID provided",
283-
status_code=HTTPStatus.BAD_REQUEST,
293+
content=error_response.model_dump_json(by_alias=True, exclude_none=True),
294+
status_code=HTTPStatus.NOT_FOUND,
295+
media_type="application/json",
284296
)
285297
await response(scope, receive, send)

tests/server/test_streamable_http_manager.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for StreamableHTTPSessionManager."""
22

3+
import json
34
from typing import Any
45
from unittest.mock import AsyncMock, patch
56

@@ -11,6 +12,7 @@
1112
from mcp.server.lowlevel import Server
1213
from mcp.server.streamable_http import MCP_SESSION_ID_HEADER, StreamableHTTPServerTransport
1314
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
15+
from mcp.types import INVALID_REQUEST
1416

1517

1618
@pytest.mark.anyio
@@ -262,3 +264,52 @@ async def mock_receive():
262264

263265
# Verify internal state is cleaned up
264266
assert len(transport._request_streams) == 0, "Transport should have no active request streams"
267+
268+
269+
@pytest.mark.anyio
270+
async def test_unknown_session_id_returns_404():
271+
"""Test that requests with unknown session IDs return HTTP 404 per MCP spec."""
272+
app = Server("test-unknown-session")
273+
manager = StreamableHTTPSessionManager(app=app)
274+
275+
async with manager.run():
276+
sent_messages: list[Message] = []
277+
response_body = b""
278+
279+
async def mock_send(message: Message):
280+
nonlocal response_body
281+
sent_messages.append(message)
282+
if message["type"] == "http.response.body":
283+
response_body += message.get("body", b"")
284+
285+
# Request with a non-existent session ID
286+
scope = {
287+
"type": "http",
288+
"method": "POST",
289+
"path": "/mcp",
290+
"headers": [
291+
(b"content-type", b"application/json"),
292+
(b"accept", b"application/json, text/event-stream"),
293+
(b"mcp-session-id", b"non-existent-session-id"),
294+
],
295+
}
296+
297+
async def mock_receive():
298+
return {"type": "http.request", "body": b"{}", "more_body": False} # pragma: no cover
299+
300+
await manager.handle_request(scope, mock_receive, mock_send)
301+
302+
# Find the response start message
303+
response_start = next(
304+
(msg for msg in sent_messages if msg["type"] == "http.response.start"),
305+
None,
306+
)
307+
assert response_start is not None, "Should have sent a response"
308+
assert response_start["status"] == 404, "Should return HTTP 404 for unknown session ID"
309+
310+
# Verify JSON-RPC error format
311+
error_data = json.loads(response_body)
312+
assert error_data["jsonrpc"] == "2.0"
313+
assert error_data["id"] == "server-error"
314+
assert error_data["error"]["code"] == INVALID_REQUEST
315+
assert error_data["error"]["message"] == "Session not found"

0 commit comments

Comments
 (0)