Engineers debugging MCP session errors or designing a stateful MCP server.
Whether your MCP server needs sessions at all, and what authority a session identifier is allowed to carry.
The control gap
The Mcp-Session-Id header solves a real problem: a stateful server needs to associate a sequence of requests with one client conversation. It becomes a security problem the moment a server treats possession of the identifier as evidence of who is calling. It is sent on ordinary requests, it appears in logs and proxies, and it does not expire the way a token does.
What good looks like
Sessions correlate requests and nothing more. Every request is independently authenticated, every tool call is independently authorized, and losing a session identifier costs a reconnect rather than an incident.
- Generate the identifier from a cryptographically secure source with enough entropy that enumeration is impractical.
- Require the bearer token on every request that carries a session identifier, and verify it every time.
- Return 404 for an unknown or terminated session so the client re-initializes cleanly instead of retrying against nothing.
- Terminate sessions explicitly on DELETE, on token revocation, and on consent expiry, not only on idle timeout.
A production workflow
- The client sends initialize with no session header; the server issues one in the response.
- The client echoes the header on every later request alongside its bearer token.
- The server resolves the session for state, then authenticates and authorizes the request as if the session did not exist.
- The client sends DELETE when the conversation ends, and the server discards the state and invalidates the identifier.
Copy this
Most reported MCP session bugs are one of these three responses being misread. The header is easy; the failure semantics are where implementations diverge.
# Issue
POST /mcp/<slug> {"method":"initialize", ...}
<- 200 Mcp-Session-Id: 5f2c... (opaque, random, no meaning to the client)
# Use
POST /mcp/<slug>
Mcp-Session-Id: 5f2c...
Authorization: Bearer <token> # still required, still verified
# The three responses that matter
404 Not Found session unknown or terminated -> client must re-initialize
400 Bad Request session header missing on a server that requires one
401 Unauthorized token problem, NOT a session problem, do not re-initialize
# Terminate
DELETE /mcp/<slug> with Mcp-Session-Id -> 204
# Anti-pattern
if (sessions.has(req.headers["mcp-session-id"])) { allow(); } // authority from a handleA client that responds to 401 by re-initializing will loop, because a new session does not fix an expired or wrongly scoped token. Distinguishing 404 from 401 in the server is what lets the client recover correctly.
Evidence to require
- Session creation and termination events with the identity that owned each one.
- Requests carrying an unknown session identifier, which indicate either a stale client or probing.
- Whether authorization ran per call, visible as a decision record per tool invocation rather than per session.
- Sessions closed because the underlying token or consent was revoked.
Buyer checklist
- Could an attacker who learns a session identifier do anything without also holding a valid token?
- Does the server distinguish 404 for a dead session from 401 for a bad token?
- Are session identifiers redacted from logs and traces that leave the boundary?
- Does revoking access terminate open sessions, or only prevent new ones?
Practical answers
Common implementation questions
Does every MCP server need sessions?
No. A stateless server can omit the header entirely, which removes a whole class of failure. Add sessions when the server genuinely holds per-conversation state that cannot be reconstructed from the request.
How long should a session live?
No longer than the authority behind it. Tie termination to token expiry and consent revocation rather than to an idle timer, so a withdrawn permission takes effect on the next call.
Can the session identifier carry meaning, like a workspace name?
It should not. An opaque random value leaks nothing if it appears in a proxy log, and it removes any temptation to derive authorization from parsing it.