
# 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:

```ts
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

1. `initialize` creates a session and returns it in the `mcp-session-id` response header.
2. Every later `POST`, `GET`, and `DELETE` must carry that header.
3. `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](/security/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-`initialize`d.

## 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 `404` and 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.
- **`maxSessions` is unset by default.** The store is a client-growable, server-lifetime collection: without a cap, anything that can reach `initialize` can grow it. Set `maxSessions` on any endpoint that is not fully trusted — exceeding it returns `503` rather 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

```ts
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](/security/auth), which also binds each session to the credential that created it, rather than treating the id as proof of identity.
