Skip to content

Commit 8676c9f

Browse files
committed
Stateful Streamable-HTTP: send 404 Not Found on unknown Mcp-Session-Id (#1004)
1 parent 3e798bf commit 8676c9f

File tree

2 files changed

+56
-2
lines changed

2 files changed

+56
-2
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
273273
else:
274274
# Invalid session ID
275275
response = Response(
276-
"Bad Request: No valid session ID provided",
277-
status_code=HTTPStatus.BAD_REQUEST,
276+
"Not Found: Unknown session ID",
277+
status_code=HTTPStatus.NOT_FOUND,
278278
)
279279
await response(scope, receive, send)

tests/server/test_streamable_http_manager.py

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

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

@@ -202,6 +203,59 @@ async def mock_receive():
202203
assert not manager._server_instances, "No sessions should be tracked after the only session crashes"
203204

204205

206+
@pytest.mark.anyio
207+
async def test_stateful_session_returning_http_not_found_when_not_found(
208+
running_manager: tuple[StreamableHTTPSessionManager, Server],
209+
):
210+
"""Test that a request with a non-existent session ID returns HTTP 404 Not Found.
211+
212+
This is in accordance to the specification point 2.5.3 of:
213+
https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
214+
"""
215+
manager, app = running_manager
216+
217+
mock_mcp_run = AsyncMock(return_value=None)
218+
app.run = mock_mcp_run
219+
220+
sent_messages: list[Message] = []
221+
222+
async def mock_send(message: Message):
223+
sent_messages.append(message)
224+
225+
scope = {
226+
"type": "http",
227+
"method": "POST",
228+
"path": "/mcp",
229+
"headers": [
230+
(b"content-type", b"application/json"),
231+
# The point of this test -- non-existent session ID:
232+
(b"mcp-session-id", b"non-existent-session-id"),
233+
],
234+
}
235+
236+
async def mock_receive():
237+
return {"type": "http.request", "body": b"", "more_body": False}
238+
239+
# Send the request with mcp-session-id header set to a non-existent value.
240+
await manager.handle_request(scope, mock_receive, mock_send)
241+
242+
# Extract HTTP status and body from the messages.
243+
http_status = None
244+
body = None
245+
for msg in sent_messages:
246+
if msg["type"] == "http.response.start":
247+
http_status = msg["status"]
248+
break
249+
for msg in sent_messages:
250+
if msg["type"] == "http.response.body":
251+
body = msg["body"]
252+
break
253+
254+
assert http_status == HTTPStatus.NOT_FOUND, "Response status should be 404 Not Found"
255+
assert body is not None, "Response body should not be None"
256+
assert b"Not Found" in body, "Response body should indicate Not Found"
257+
258+
205259
@pytest.mark.anyio
206260
async def test_stateless_requests_memory_cleanup():
207261
"""Test that stateless requests actually clean up resources using real transports."""

0 commit comments

Comments
 (0)