Askr documentation
Server & APIs

HTTP Transport

HTTP Transport 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) }],
  })
);

HTTP requests

`registerMcpRoutes(router, path, mcp, options)` mounts POST, GET, and DELETE handlers for `path` on an existing `Router`. POST requires `content-type: application/json`, an `accept` header that satisfies **both** `application/json` and `text/event-stream` (not either/or — a client that only sends `Accept: application/json` gets rejected too), and a body under `options.maxRequestBytes` (1 MiB by default) — violations return `415`, `406`, and `413` respectively before the body is even parsed. Origin and host checks run first via `options.allowedOrigins` / `options.allowedHosts`, and requests with an unrecognized `mcp-protocol-version` header are rejected with `400` before reaching the MCP server.

Responses

A successful POST that produces a result returns `200` with the JSON-RPC response body; a notification or request that yields no result (like `notifications/initialized`) returns a bare `202` with no body. When `options.stateful` is true, the first `initialize` call gets a fresh `mcp-session-id` header (a `crypto.randomUUID()`), which the client must echo on every subsequent request; a missing or unknown session ID on a stateful deployment produces `400` or `404`. Malformed JSON in the request body returns a JSON-RPC `-32700 Parse error` at HTTP `400`, and JSON-RPC batch arrays are rejected outright with `-32600`.

Protected resource metadata

Pass `resource` (and optionally `authorizationServers`) in the options and the adapter automatically serves `GET /.well-known/oauth-protected-resource`, backed by `protectedResourceMetadata(resource, authorizationServers)`. The returned object is frozen and includes `resource`, `authorization_servers`, and a fixed `bearer_methods_supported: ["header"]`, matching the OAuth protected-resource metadata clients use for discovery. You can also call `protectedResourceMetadata` directly if you need to serve that document from a route the adapter doesn't own.

Deployment

Statefulness is opt-in: pass `stateful: true` to get session IDs, SSE push, and the DELETE-to-terminate flow; omit it and every POST is handled independently with `methodNotAllowed` returned for GET and DELETE. By default sessions live in an in-memory store that's bounded, not unlimited — a session expires after `sessionTtlMs` (30 minutes by default) and the store caps out at `maxSessions` (1,000 by default), returning `503` once that capacity is reached — and it only works for a single process either way, so supply your own `sessionStore` (matching the `create`/`has`/`delete` shape) if you're running more than one instance behind a load balancer. Set `allowedOrigins` and `allowedHosts` for any endpoint reachable from a browser to guard against DNS-rebinding-style requests, and tune `heartbeatInterval` (default 30 seconds) if intermediaries close idle connections faster than that.