-
Notifications
You must be signed in to change notification settings - Fork 198
Session expiry breaks MCP connections instead of allowing client recovery #4425
Description
Bug description
When a client sends a request with an expired or unknown Mcp-Session-Id, ToolHive's proxies return plain-text HTTP errors instead of JSON-RPC error responses.
MCP clients like Claude Code have built-in session recovery: when a session expires, the client can detect the failure, clear its connection state, and retry with a fresh session. This detection relies on receiving HTTP 404 with a JSON-RPC error body containing "code":-32001. Because ToolHive returns plain text instead of JSON-RPC, the detection never fires.
This means that thv restart, container crashes, or any event that invalidates sessions leaves MCP connections broken until the user manually restarts their client. What should be a transparent recovery becomes a disruptive interruption.
Steps to reproduce
- Start any stdio-based MCP server via ToolHive (e.g.,
thv run --transport stdio npx://@modelcontextprotocol/server-memory) - Send a request with a bogus session ID:
curl -X POST http://127.0.0.1:<port>/mcp \
-H 'Content-Type: application/json' \
-H 'Mcp-Session-Id: bogus-id' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
Expected behavior
HTTP 404 with Content-Type: application/json and a JSON-RPC error body:
{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found"},"id":1}Actual behavior
HTTP 404 with Content-Type: text/plain:
session not found
The same problem exists in all three proxy types with slightly different error strings:
- Streamable HTTP proxy:
session not found(HTTP 404) - Transparent proxy:
unknown session(HTTP 400, also wrong status code) - SSE proxy:
Could not find session(HTTP 404)
Additional context
The MCP TypeScript SDK reference server returns -32001 for this case. This code falls within the JSON-RPC 2.0 implementation-defined server-errors range (-32000 to -32099). Claude Code's session recovery checks for this code in the response body.
Five call sites are affected:
- Streamable HTTP proxy:
handleDelete,resolveSessionForBatch,resolveSessionForRequest - Transparent proxy: session guard in
RoundTrip(also uses wrong status code 400 instead of 404) - SSE proxy:
handlePostRequest