You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP client doesn't not initialize new session when getting 404 session not found #1676
When a client receives HTTP 404 in response to a request containing an MCP-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached.
This is not the case with the python client. It just sends a session terminated error in streamable_http.py
Bug: When the server returns HTTP 404 (session not found), the client raises McpError: Session terminated instead of automatically re-initializing with a new session.
When a client receives HTTP 404 in response to a request containing an MCP-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached.
Current behavior: Calls _send_session_terminated_error() which propagates as McpError
Expected behavior: Clear self.session_id and send a new InitializeRequest
Server (server.py)
"""Reproduction server for issue #1676:MCP client doesn't initialize new session when getting 404 session not foundThis server simulates a session being terminated/expired between requests.Call GET /invalidate to mark all sessions as terminated, then subsequentMCP requests with the old session ID will receive 404 Not Found.Spec reference (https://modelcontextprotocol.io/specification/draft/basic/transports):> When a client receives HTTP 404 in response to a request containing an> MCP-Session-Id, it MUST start a new session by sending a new InitializeRequest> without a session ID attached.Run with: uv run python scratch_server_1676.py"""importcontextlibimportloggingfromcollections.abcimportAsyncIteratorfromtypingimportAnyfromstarlette.applicationsimportStarlettefromstarlette.requestsimportRequestfromstarlette.responsesimportJSONResponsefromstarlette.routingimportMount, Routeimportmcp.typesastypesfrommcp.server.lowlevelimportServerfrommcp.server.streamable_http_managerimportStreamableHTTPSessionManagerlogging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger=logging.getLogger(__name__)
# Create the MCP servermcp_server=Server("session-termination-demo")
# Store reference to session manager for termination_session_manager: StreamableHTTPSessionManager|None=None@mcp_server.list_tools()asyncdeflist_tools() ->list[types.Tool]:
return [
types.Tool(
name="get_greeting",
description="Returns a simple greeting",
inputSchema={"type": "object", "properties": {}},
),
types.Tool(
name="get_time",
description="Returns the current server time",
inputSchema={"type": "object", "properties": {}},
),
]
@mcp_server.call_tool()asyncdefcall_tool(name: str, arguments: dict[str, Any]) ->list[types.ContentBlock]:
ifname=="get_greeting":
return [types.TextContent(type="text", text="Hello from the server!")]
elifname=="get_time":
importdatetimenow=datetime.datetime.now().isoformat()
return [types.TextContent(type="text", text=f"Current time: {now}")]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
asyncdefinvalidate_sessions(request: Request) ->JSONResponse:
""" Endpoint to invalidate all sessions. Simulates server restart, session timeout, or session revocation. After calling this, requests with the old session ID will get 404. """if_session_managerisNone:
returnJSONResponse({"error": "Session manager not initialized"}, status_code=500)
invalidated= []
forsession_id, transportinlist(_session_manager._server_instances.items()):
logger.info(f"Marking session as terminated: {session_id}")
# Mark as terminated - transport.handle_request will return 404transport._terminated=Trueinvalidated.append(session_id)
returnJSONResponse({
"message": f"Invalidated {len(invalidated)} sessions",
"sessions": invalidated,
})
asyncdeflist_sessions(request: Request) ->JSONResponse:
"""Endpoint to list all active sessions."""if_session_managerisNone:
returnJSONResponse({"error": "Session manager not initialized"}, status_code=500)
sessions= []
forsession_id, transportin_session_manager._server_instances.items():
sessions.append({
"session_id": session_id,
"terminated": transport.is_terminated,
})
returnJSONResponse({"sessions": sessions})
# Create session managersession_manager=StreamableHTTPSessionManager(
app=mcp_server,
json_response=True, # Use JSON for simpler debugging
)
_session_manager=session_manager@contextlib.asynccontextmanagerasyncdeflifespan(app: Starlette) ->AsyncIterator[None]:
asyncwithsession_manager.run():
logger.info("="*60)
logger.info("Server started!")
logger.info("MCP endpoint: http://localhost:8000/mcp")
logger.info("List sessions: GET http://localhost:8000/sessions")
logger.info("Invalidate all: GET http://localhost:8000/invalidate")
logger.info("="*60)
yieldlogger.info("Server shutting down...")
# Create the Starlette appapp=Starlette(
debug=True,
routes=[
Mount("/mcp", app=session_manager.handle_request),
Route("/invalidate", invalidate_sessions),
Route("/sessions", list_sessions),
],
lifespan=lifespan,
)
if__name__=="__main__":
importuvicornuvicorn.run(app, host="127.0.0.1", port=8000)
Client (client.py)
"""Reproduction client for issue #1676:MCP client doesn't initialize new session when getting 404 session not foundThis client demonstrates the bug where the MCP client fails to re-initializea new session when the server returns 404 for an expired/invalid session.Steps to reproduce:1. Start the server: uv run python scratch_server_1676.py2. Run this client: uv run python scratch_client_1676.pyExpected behavior (per spec):- After receiving 404, client should automatically re-initialize with a new sessionActual behavior (bug):- Client raises McpError with "Session terminated" instead of re-initializingSpec reference (https://modelcontextprotocol.io/specification/draft/basic/transports):> When a client receives HTTP 404 in response to a request containing an> MCP-Session-Id, it MUST start a new session by sending a new InitializeRequest> without a session ID attached.Bug location in code:- src/mcp/client/streamable_http.py:309-315- When 404 is received, _send_session_terminated_error() is called instead of clearing the session ID and sending a new InitializeRequest"""importasyncioimportloggingimporthttpxfrommcpimportClientSessionfrommcp.client.streamable_httpimportstreamablehttp_client# Set to DEBUG to see all HTTP traffic, INFO for cleaner outputlogging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger=logging.getLogger(__name__)
asyncdefinvalidate_server_sessions():
"""Call the server's invalidate endpoint to simulate session expiry."""asyncwithhttpx.AsyncClient() asclient:
response=awaitclient.get("http://localhost:8000/invalidate")
returnresponse.json()
asyncdefmain():
print("="*70)
print("Issue #1676 Reproduction: Client doesn't re-init on 404")
print("="*70)
try:
asyncwithstreamablehttp_client("http://localhost:8000/mcp") as (
read_stream,
write_stream,
get_session_id,
):
asyncwithClientSession(read_stream, write_stream) assession:
# Step 1: Initialize and make a successful requestprint("\n[Step 1] Initializing session...")
awaitsession.initialize()
session_id=get_session_id()
print(f" Session ID: {session_id}")
# Step 2: Call a tool to verify everything worksprint("\n[Step 2] Calling get_greeting tool (should succeed)...")
result=awaitsession.call_tool("get_greeting", {})
print(f" Result: {result.content[0].text}")
# Step 3: Invalidate the session on the server side# This simulates a server restart or session timeoutprint("\n[Step 3] Invalidating session on server (simulating expiry)...")
invalidate_result=awaitinvalidate_server_sessions()
print(f" {invalidate_result['message']}")
# Step 4: Try to make another request - this should trigger re-initprint("\n[Step 4] Calling get_time tool after session invalidation...")
print(" Expected: Client should re-initialize and succeed")
print(" Actual (bug): Client raises 'Session terminated' error")
print()
try:
result=awaitsession.call_tool("get_time", {})
print(f" SUCCESS: {result.content[0].text}")
print("\n BUG IS FIXED! Client properly re-initialized the session.")
exceptExceptionase:
print(f" ERROR: {type(e).__name__}: {e}")
print("\n BUG CONFIRMED!")
print(" Per the spec, on 404 the client MUST send a new InitializeRequest")
print(" without a session ID attached, but instead it just errors out.")
print()
print(" Relevant code: src/mcp/client/streamable_http.py:309-315")
print(" Current behavior: Calls _send_session_terminated_error()")
print(" Expected behavior: Clear session_id and send new InitializeRequest")
exceptExceptionase:
logger.exception(f"Unexpected error: {e}")
raiseprint("\n"+"="*70)
if__name__=="__main__":
asyncio.run(main())
To reproduce
# Terminal 1: Start server
uv run python server.py
# Terminal 2: Run client
uv run python client.py
I've investigated the CI test failures and they appear to be unrelated to the changes in this PR.
Root cause: The test suite fails during collection when tests/client/test_config.py is imported. This file imports from mcp.cli.claude, which triggers an import chain that reaches mcp/cli/cli.py. That module calls sys.exit(1) if typer is not installed:
confirmed — the client sends a Session terminated error on 404 instead of re-initializing per the spec.
the bug is in src/mcp/client/streamable_http.py:260-263 — on 404, _send_session_terminated_error() is called which propagates as MCPError, but the spec requires the client to clear the session ID and send a new InitializeRequest.
note: the TypeScript SDK also doesn't implement this yet (it throws a generic SdkError on 404), so this would make the Python SDK ahead on spec compliance.
I'd like to take this on with a focused v2 fix. I reproduced the current behavior on main: after an established Streamable HTTP session receives a bare 404, the pending request gets MCPError(-32600, "Session terminated") and the client never re-initializes.\n\nProposed scope:\n- preserve pre-session 404 -> METHOD_NOT_FOUND behavior\n- turn a post-session 404 into an SDK-internal, request-correlated session-expired signal\n- serialize one re-initialization, retry the original request exactly once, and fail normally if the retry also 404s\n- add deterministic in-process tests for the exact session/header/method sequence and the repeated-404 bound\n\nI will not reuse reserved -32002; the old #1818 approach is stale against the current dispatcher/session architecture.\n\nAI assistance disclosure: I am using Claude for codebase research and test scaffolding; I understand and will own the design, implementation, and review responses.
Activity
maxisbey commentedon Dec 2, 2025
Claude's reproduction:
Issue Confirmed
Bug: When the server returns HTTP 404 (session not found), the client raises
McpError: Session terminatedinstead of automatically re-initializing with a new session.Spec requirement (from https://modelcontextprotocol.io/specification/draft/basic/transports):
Bug location:
src/mcp/client/streamable_http.py:309-315Current behavior: Calls
_send_session_terminated_error()which propagates asMcpErrorExpected behavior: Clear
self.session_idand send a newInitializeRequestServer (
server.py)Client (
client.py)To reproduce
jayhemnani9910 commentedon Dec 31, 2025
CI Failures Investigation
I've investigated the CI test failures and they appear to be unrelated to the changes in this PR.
Root cause: The test suite fails during collection when
tests/client/test_config.pyis imported. This file imports frommcp.cli.claude, which triggers an import chain that reachesmcp/cli/cli.py. That module callssys.exit(1)iftyperis not installed:felixweinberger commentedon Feb 5, 2026
confirmed — the client sends a
Session terminatederror on 404 instead of re-initializing per the spec.the bug is in
src/mcp/client/streamable_http.py:260-263— on 404,_send_session_terminated_error()is called which propagates asMCPError, but the spec requires the client to clear the session ID and send a newInitializeRequest.note: the TypeScript SDK also doesn't implement this yet (it throws a generic
SdkErroron 404), so this would make the Python SDK ahead on spec compliance.PR #1818 is open with a proposed fix.
Add MCP session recovery for stale session IDs, bump to fork.2
nankingjing commentedon Aug 16, 2026
I'd like to take this on with a focused v2 fix. I reproduced the current behavior on main: after an established Streamable HTTP session receives a bare 404, the pending request gets
MCPError(-32600, "Session terminated")and the client never re-initializes.\n\nProposed scope:\n- preserve pre-session 404 ->METHOD_NOT_FOUNDbehavior\n- turn a post-session 404 into an SDK-internal, request-correlated session-expired signal\n- serialize one re-initialization, retry the original request exactly once, and fail normally if the retry also 404s\n- add deterministic in-process tests for the exact session/header/method sequence and the repeated-404 bound\n\nI will not reuse reserved-32002; the old #1818 approach is stale against the current dispatcher/session architecture.\n\nAI assistance disclosure: I am using Claude for codebase research and test scaffolding; I understand and will own the design, implementation, and review responses.