Skip to content

Commit 75b142b

Browse files
committed
refactor: use typed models and add test for session 404 response
- Replace raw dict with JSONRPCError and ErrorData types for consistency with the rest of the codebase - Use INVALID_REQUEST (-32600) error code instead of -32001, which is in the reserved JSON-RPC implementation range (see spec issue #509) - Remove pragma: no cover and add unit test for unknown session ID - Test verifies HTTP 404 status and proper JSON-RPC error format Github-Issue:#1727
1 parent 4aaf4ad commit 75b142b

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import contextlib
6-
import json
76
import logging
87
from collections.abc import AsyncIterator
98
from http import HTTPStatus
@@ -23,6 +22,7 @@
2322
StreamableHTTPServerTransport,
2423
)
2524
from mcp.server.transport_security import TransportSecuritySettings
25+
from mcp.types import INVALID_REQUEST, ErrorData, JSONRPCError
2626

2727
logger = logging.getLogger(__name__)
2828

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

278278
# Handle the HTTP request and return the response
279279
await http_transport.handle_request(scope, receive, send)
280-
else: # pragma: no cover
280+
else:
281281
# Unknown or expired session ID - return 404 per MCP spec
282-
# Match TypeScript SDK exactly: jsonrpc, error, id order
283-
error_body = json.dumps(
284-
{
285-
"jsonrpc": "2.0",
286-
"error": {
287-
"code": -32001,
288-
"message": "Session not found",
289-
},
290-
"id": None,
291-
}
282+
error_response = JSONRPCError(
283+
jsonrpc="2.0",
284+
id="server-error",
285+
error=ErrorData(
286+
code=INVALID_REQUEST,
287+
message="Session not found",
288+
),
292289
)
293290
response = Response(
294-
content=error_body,
291+
content=error_response.model_dump_json(by_alias=True, exclude_none=True),
295292
status_code=HTTPStatus.NOT_FOUND,
296293
media_type="application/json",
297294
)

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}
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)