# HTTP Transport

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

Source: [https://askrjs.com/docs/mcp/http-transport](https://askrjs.com/docs/mcp/http-transport)

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

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

## Documentation navigation

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