# Sessions

> Sessions at the Askr server boundary, with validated input and explicit failure states.

Source: [https://askrjs.com/docs/mcp/sessions](https://askrjs.com/docs/mcp/sessions)

Status: stable. Packages: @askrjs/askr.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

Create one MCP server, register typed primitives on it, and let the selected transport supply auth, cancellation, sessions, and progress.

```tsx
import { schema } from '@askrjs/schema';
import { createMcpServer } from '@askrjs/server/mcp';

export const mcp = createMcpServer({
  name: 'project-tools',
  version: '1.0.0',
}).tool(
  'lookup-project',
  { input: schema.object({ id: schema.uuid() }) },
  async (context, { id }) => ({
    content: [{ type: 'text', text: await lookup(id, context.signal) }],
  })
);
```

## Session identity

In stateful HTTP mode, a session is identified purely by the `mcp-session-id` header: the server mints one with `crypto.randomUUID()` the moment it sees an `initialize` call with no existing session ID, and the client is expected to send that same header on every subsequent request. Stdio sessions work differently — `connectMcpStdio` generates a single random session ID per process connection and uses it for every message on that stream, since there's only ever one "client" per stdio pipe.

## State ownership

Two separate stores back a session in stateful HTTP mode: the `McpSessionStore` (`create`/`has`/`delete`, backed by a TTL- and capacity-bounded in-memory `Map` by default — see `sessionTtlMs`/`maxSessions`) tracks which session IDs are currently valid, while the MCP server itself keeps a parallel map of initialized session state — negotiated `client` info, `clientCapabilities`, and `protocolRevision` — keyed by that same ID. The two are meant to stay in sync: a request is only dispatched once `sessions.has(id)` is true at the HTTP layer and the server-side map has an entry for that ID, and `terminateSession` clears the server-side entry and any SSE listeners for that ID.

## Expiry

The default in-memory session store does expire sessions on its own: `sessionTtlMs` (30 minutes by default) ages an ID out automatically, and `maxSessions` (1,000 by default) bounds how many can be live at once, returning `503` on a new session once that cap is hit. Once a session is gone — aged out, capacity-evicted, or explicitly deleted via `DELETE path` — the adapter treats it the same way: `404 MCP session not found` on the next request that references it. A custom `sessionStore` can implement its own eviction policy the same way, by having `has(id)` return `false` once you consider a session gone.

## Horizontal scaling

The default in-memory session store and the server's own initialized-session map are both process-local, so a stateful deployment behind multiple instances needs sticky routing on `mcp-session-id`, a shared `sessionStore` implementation, or both. Because SSE channels (the `Map` of open `send` functions) are also held in the process that accepted the GET request, notifications for a given session can only be pushed by the instance holding that connection — routing by session ID isn't optional if you scale out and want push to keep working, not just request/response.

## Documentation navigation

[Previous](https://askrjs.com/docs/mcp/sse-transport/index.md) | [Next](https://askrjs.com/docs/mcp/stdio-transport/index.md)
