Skip to content

Session expiry breaks MCP connections instead of allowing client recovery #4425

@gkatz2

Description

@gkatz2

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

  1. Start any stdio-based MCP server via ToolHive (e.g., thv run --transport stdio npx://@modelcontextprotocol/server-memory)
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiItems related to the APIbugSomething isn't workinggoPull requests that update go code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions