[BUG] MCP streamable-HTTP: an immediate 404 "Session not found" is surfaced as -32001 Request timed out, the session is never re-initialized, and the call is silently lost
TL;DR
When a streamable-HTTP MCP session is retired for idleness, the server answers
the next request with HTTP 404 Session not found in ~48 ms — the response
the MCP spec requires for a terminated session. The client turns that immediate,
protocol-correct refusal into MCP error -32001: Request timed out, does not
re-initialize the session, and the tool call never reaches the server. Issuing
the identical call again succeeds instantly, because that second attempt
establishes a fresh session.
The practical damage is silent data loss: a write that is reported as
"timed out" was in fact never executed, and nothing in the error says so.
This looks like the same missing behaviour as #78911 ("the client never
re-initializes a suspect transport"), but on a different and much less ambiguous
path. In #78911 the server goes silent and the client waits out the idle
timeout — there, waiting is at least explicable. Here the server replies
correctly and immediately, and the client still reports a timeout. There is no
proxy, no cloud connector, and no server-side slowness to hide behind.
Environment
- Claude Code 2.1.220, macOS 26.5.2 (Darwin 25.5.0)
- MCP server: a local, self-hosted streamable-HTTP server on loopback
(https://127.0.0.1:<port>/mcp), configured as "type": "http" — the
client's native HTTP transport, not mcp-remote, no proxy of any kind
- Server-side session idle timeout: 5 minutes (the
rmcp2.2.0 default)
Reproduction
- Call any tool on the server — it completes in tens of milliseconds.
- Do non-MCP work for longer than the server's session idle timeout (here
5 minutes; a compile or a CI wait does it).
- Call any tool again.
Expected: the client notices the session is gone and re-initializes,
transparently. The call succeeds.
Actual: MCP error -32001: Request timed out. Call the same tool again
immediately and it succeeds.
Evidence that the server is not the slow party
Driving the same server directly over HTTP with curl, with timestamps:
| step | result |
|---|---|
| initialize | session id returned in 0.051 s |
| tools/call on the live session | 0.027 s, HTTP 200 |
| idle 360 s (> the 300 s server timeout) | — |
| tools/call on the same session id | HTTP 404 Not Found: Session not found in 0.048 s |
| same again | 404 in 0.028 s |
So the server answers the retired session in ~48 ms with the correct 404. The
client, given that same 404, reports a timeout instead.
Why it is not a slow-backend problem
Ruling out the usual suspects explicitly, because they were my own first two
hypotheses and both were wrong:
- It reproduces on a pure read that performs no model call at all, so it is
not the write path, not an embedding, and not any inference backend.
- The server process was up 18 h with no restart, and its CPU was idle at the
moment of the failure.
- The failure tracks time since the last MCP call, not the cost of the call.
Short gaps never fail; gaps past the server's idle timeout fail every time.
Impact
-32001 Request timed out is indistinguishable, to the caller, from a slow call
that may or may not have landed. For a write it is worse than a plain error:
the operation never reached the server, but the caller cannot tell that from the
message. In one working session this cost four silently lost writes; each was
detected only by reading the stored state back and finding it unchanged.
A prompt 404-driven re-initialize would make all of these succeed on the first
attempt.
Suggested behaviour
On 404 from a streamable-HTTP MCP endpoint carrying a session id, treat the
session as terminated: re-initialize and retry the call once, rather than
surfacing a timeout. That is the recovery the client already performs when a
process is restarted — it is just not triggered by the 404.
Failing that, surfacing the actual status (session terminated) instead ofRequest timed out would at least let callers distinguish "never ran" from
"may have run".
What the server side did about it
Only a mitigation, and only because the root cause is not fixable from there:
the server's session idle timeout was raised from 5 to 60 minutes and made
configurable, so an agent's normal pauses — compiles, CI waits — no longer
retire the session. That reduces how often the client meets the 404; it does not
change what the client does when it does meet one.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗