Askr documentation
Server & APIs

Sessions

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

Example

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

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.