Skip to content

Commit 01397f0

Browse files
committed
fix: return HTTP 404 for unknown session IDs instead of 400
Per the MCP specification (2025-11-25), when a server receives a request with an unknown or expired session ID, it MUST return HTTP 404 Not Found instead of 400 Bad Request. This signals to the client that it should reinitialize the session. Github-Issue: #1727
1 parent ef96a31 commit 01397f0

File tree

1 file changed

+19
-8
lines changed

1 file changed

+19
-8
lines changed

src/mcp/server/streamable_http_manager.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ async def _handle_stateless_request(
172172
)
173173

174174
# Start server in a new task
175-
async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED):
175+
async def run_stateless_server(
176+
*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
177+
):
176178
async with http_transport.connect() as streams:
177179
read_stream, write_stream = streams
178180
task_status.started()
@@ -215,7 +217,10 @@ async def _handle_stateful_request(
215217
request_mcp_session_id = request.headers.get(MCP_SESSION_ID_HEADER)
216218

217219
# Existing session case
218-
if request_mcp_session_id is not None and request_mcp_session_id in self._server_instances: # pragma: no cover
220+
if (
221+
request_mcp_session_id is not None
222+
and request_mcp_session_id in self._server_instances
223+
): # pragma: no cover
219224
transport = self._server_instances[request_mcp_session_id]
220225
logger.debug("Session already exists, handling request directly")
221226
await transport.handle_request(scope, receive, send)
@@ -239,7 +244,9 @@ async def _handle_stateful_request(
239244
logger.info(f"Created new transport with session ID: {new_session_id}")
240245

241246
# Define the server runner
242-
async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None:
247+
async def run_server(
248+
*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
249+
) -> None:
243250
async with http_transport.connect() as streams:
244251
read_stream, write_stream = streams
245252
task_status.started()
@@ -259,15 +266,18 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
259266
# Only remove from instances if not terminated
260267
if ( # pragma: no branch
261268
http_transport.mcp_session_id
262-
and http_transport.mcp_session_id in self._server_instances
269+
and http_transport.mcp_session_id
270+
in self._server_instances
263271
and not http_transport.is_terminated
264272
):
265273
logger.info(
266274
"Cleaning up crashed session "
267275
f"{http_transport.mcp_session_id} from "
268276
"active instances."
269277
)
270-
del self._server_instances[http_transport.mcp_session_id]
278+
del self._server_instances[
279+
http_transport.mcp_session_id
280+
]
271281

272282
# Assert task group is not None for type checking
273283
assert self._task_group is not None
@@ -277,9 +287,10 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
277287
# Handle the HTTP request and return the response
278288
await http_transport.handle_request(scope, receive, send)
279289
else: # pragma: no cover
280-
# Invalid session ID
290+
# Unknown or expired session ID - return 404 per MCP spec
291+
# This tells the client to reinitialize
281292
response = Response(
282-
"Bad Request: No valid session ID provided",
283-
status_code=HTTPStatus.BAD_REQUEST,
293+
"Session not found",
294+
status_code=HTTPStatus.NOT_FOUND,
284295
)
285296
await response(scope, receive, send)

0 commit comments

Comments
 (0)