Sessions
Opt-in Mcp-Session-Id lifecycle, legacy only.
Note
Protocol-level sessions were removed in 2026-07-28. Modern servers are stateless; carry cross-call state in explicit handles passed as ordinary tool arguments.
Sessions are off by default. Enable them and initialize starts issuing session ids that clients must return on every subsequent request:
defineMcpHandler({
name: "my-server",
version: "1.0.0",
// Simple — uses crypto.randomUUID()
session: true,
// Or configured
session: {
generateId: (event) => `session-${Date.now()}`,
maxSessions: 1000,
},
tools: [/* ... */],
});#Lifecycle
initialize creates a session and returns it in the mcp-session-id response header.POST, GET, and DELETE must carry that header.DELETE removes it from the store.| Situation | Response |
|---|---|
Missing mcp-session-id | 400 |
| Unknown session id | 404 |
| Session created by a different credential | 404 |
maxSessions exceeded | 503 |
Session ids are format-checked before they are looked up or stored — bounded length, alphanumerics plus - and _. A custom generateId must stay inside that alphabet.
With auth on, a session is bound to the credential that created it (event.context.mcp.principal: the validator's subject, else a digest of the token). A request that presents the id with a different credential — or with none, or with one when the session was created without auth — is answered exactly like an unknown session, so the response never confirms that the session exists. A subject-less token that rotates is a new principal, and its session must be re-initialized.
#The Store Is In-Memory
The built-in store is a bounded Map in the handler's closure. That has consequences worth being explicit about:
- It does not survive a restart. Clients get
404and must re-initialize. - It is per-process. Behind a load balancer without sticky sessions, requests will land on a process that has never seen the id.
maxSessionsis unset by default. The store is a client-growable, server-lifetime collection: without a cap, anything that can reachinitializecan grow it. SetmaxSessionson any endpoint that is not fully trusted — exceeding it returns503rather than consuming more memory.
For anything multi-process, prefer keeping real state in your own store keyed by the session id, so losing the id costs a handshake rather than data.
#Reading the Session
defineTool({
name: "remember",
handler: (event) => {
const sessionId = event.context.mcp?.sessionId;
return { content: [{ type: "text", text: `session: ${sessionId ?? "none"}` }] };
},
});Important
A session id is a bearer credential for the session's state: whoever holds it continues that session on a server without auth. It is not an authentication mechanism — pair sessions with transport auth, which also binds each session to the credential that created it, rather than treating the id as proof of identity.