Repository navigation
Conversation
The Streamable HTTP transport requires a server to answer requests that carry a terminated or expired session ID with 404, so the client knows to re-initialize. POST /mcp instead minted a new session (with enableSSE) or served the request statelessly under the dead ID (without SSE), and GET /mcp opened a stream on a brand-new session. With sessionTtlMs expiry this meant idle clients silently lost their negotiated state. The session-owner preHandler now also rejects unknown IDs, on POST, GET and DELETE alike. POST gets a JSON-RPC error body (-32001 "Session not found"), which is also what a session owned by another user now answers, so the two cases stay indistinguishable. `initialize` carrying an unknown (or foreign) ID is accepted and gets a fresh session, since that is how a client recovers. Requests without the header behave as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Streamable HTTP transport (2025-03-26 and later) says that once a server terminates a session it MUST answer requests carrying that
Mcp-Session-Idwith404, and the client then re-initializes. This plugin never did:POST /mcpwithenableSSE: an unknown or expired id silently got a brand-new session, and the request was served under it.POST /mcpwithout SSE: the request was served statelessly under the dead id.GET /mcp: an unknown id (header or?mcp-session-id=) opened a stream on a new session.Since #242 sessions expire after
sessionTtlMs, so idle clients were losing their negotiated state without ever being told.Fix
The session-owner preHandler (now
enforceSession) also rejects ids the store does not know, onPOST,GETandDELETE:POSTanswers404with a JSON-RPC error{ "jsonrpc": "2.0", "id": null, "error": { "code": -32001, "message": "Session not found" } }(-32001 is the code the official SDKs use). No session is created and the message is not processed.GETandDELETEkeep the existing{ "error": "Session not found" }body.DELETEalready answered 404, so it does not change.initializecarrying a stale id is accepted and gets a fresh session. Clients should sendinitializewithout the header, but accepting it is how a client that kept its old id recovers instead of getting stuck.initializewith someone else's id now gives the caller a fresh session of their own rather than a 404. Otherwiseinitializewould answer differently for "unknown" and "someone else's live session", which would tell an attacker that a leaked id is still live.GETthis lookup now happens beforereply.hijack().Without SSE: the plugin does issue and store session ids without SSE (on
initialize, since #242/#253), so returning 404 is the spec-compliant answer in both modes. Stateless clients that never sendMcp-Session-Idare unaffected: requests without the header are still served statelessly (no SSE) or get a new session (SSE), as before.Expiry (
sessionTtlMs) works with both stores, becauseMemorySessionStore.getand the Redis key TTL both make an expired session read asnull. Tests cover memory with and without SSE, expiry,DELETE-then-reuse, and Redis.Breaking change
This is a behaviour change, and I'd treat it as potentially breaking (semver-minor at least, or major if you want to be strict):
Mcp-Session-Idand relied on being served anyway now get404and must re-initialize. Spec-compliant clients (including the official SDKs) already handle this.404. Such setups need sticky sessions or theredisoption, which the README now says.POST404 body for a session owned by another user changes from{ "error": "Session not found" }to the JSON-RPC error above.Existing tests updated:
test/tool-context-access.test.tscalled a tool with a made-upmcp-session-id. It now initializes first.test/session-owner-binding.test.tsexpects the JSON-RPC 404 body and coversinitializewith a foreign id.Overlap
PR #170 (2026-07-28 revision) moves this POST session logic into its legacy-POST path and still references
enforceSessionOwnerin its preHandler arrays. Whichever of the two merges second needs a mechanical rebase: rename the hook, and keep the "unknown id → 404 / ignore the id on initialize" check in the legacy POST path.Fixes #304
🤖 Generated with Claude Code
https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM