# Serve over HTTP

To host one MCP endpoint that many clients connect to, serve your factory over **Streamable HTTP**. A host that launches the server as a local child process speaks [stdio](https://ts.sdk.modelcontextprotocol.io/v2/serving/stdio.md) instead.

## Create a handler

`createMcpHandler` takes a **factory** — a function that builds and returns a fresh `McpServer` — and returns the handler that serves it.

```ts
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const handler = createMcpHandler(() => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    server.registerTool(
        'add-note',
        {
            description: 'Save a note',
            inputSchema: z.object({ text: z.string() })
        },
        async ({ text }) => ({ content: [{ type: 'text', text: `Saved: ${text}` }] })
    );
    return server;
});
```

`handler.fetch` is a web-standard `(Request) => Promise<Response>` — nothing is listening yet. The tool calls on this page come from a real `Client` driving the handler's `fetch` in process; [Test a server](https://ts.sdk.modelcontextprotocol.io/v2/testing.md) shows that wiring.

Calling `add-note` through it returns the tool result:

```
[ { type: 'text', text: 'Saved: ship the release notes' } ]
```

The handler also carries `close` for shutdown and the `notify`/`bus` pair that publishes change events to subscribed clients — see [Notifications](https://ts.sdk.modelcontextprotocol.io/v2/servers/notifications.md).

::: info Coming from v1?
`createMcpHandler` replaces the per-request `StreamableHTTPServerTransport` + `connect()` wiring — run the codemod, then see the [upgrade guide](https://ts.sdk.modelcontextprotocol.io/v2/migration/upgrade-to-v2.md).
:::

## Understand the per-request factory

The factory runs once per HTTP request: a fresh instance serves every request, and the handler holds nothing between requests. Register tools, resources, and prompts inside the factory, never on a shared instance outside it.

The factory receives the **request context** — `era`, `authInfo`, and the inbound `Request` as `requestInfo`. Destructure `authInfo` to build the instance around one caller; [Pass authentication through](#pass-authentication-through) shows where the value comes from.

```ts
const perCaller = createMcpHandler(({ authInfo }) => {
    const server = new McpServer({ name: 'notes', version: '1.0.0' });
    server.registerTool('whoami', { description: 'Name the authenticated caller' }, async () => ({
        content: [{ type: 'text', text: authInfo?.clientId ?? 'anonymous' }]
    }));
    return server;
});
```

Every request now gets an instance built for its own caller. Keep the factory cheap and side-effect-free: create connection pools and caches once at module scope and close over them.

`era` names the protocol revision the request speaks — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).

Because no state lives on the instance, the endpoint is stateless and scales horizontally as-is; sessions, resumability, and multi-node fan-out are their own page, [Sessions, state, and scaling](https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.md).

## Mount it on your runtime

On a web-standard runtime — Cloudflare Workers, Deno, Bun — `export default handler` is the entire mount. Node frameworks wrap the handler once with `toNodeHandler` from `@modelcontextprotocol/node`; on plain `node:http`, bind loopback explicitly and compose the `localhostHostValidation` / `localhostOriginValidation` guards (also from `@modelcontextprotocol/node`) in front of it, matching the framework factories' defaults:

```ts
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createServer((req, res) => {
    if (!validateHost(req, res) || !validateOrigin(req, res)) return;
    void nodeHandler(req, res);
}).listen(3000, '127.0.0.1');
```

`POST http://127.0.0.1:3000/mcp` now reaches the factory; the guards answer anything else with `403` before the handler sees it — [the next section](#validate-host-and-origin-in-front-of-it) explains why they belong in front. The same wrapped handler mounts under [Express](https://ts.sdk.modelcontextprotocol.io/v2/serving/express.md), [Fastify](https://ts.sdk.modelcontextprotocol.io/v2/serving/fastify.md), and [Hono](https://ts.sdk.modelcontextprotocol.io/v2/serving/hono.md); [Serve on web-standard runtimes](https://ts.sdk.modelcontextprotocol.io/v2/serving/web-standard.md) covers the `export default` side.

## Validate Host and Origin in front of it

The handler trusts its caller: it validates no `Host` header, no `Origin` header, and no token. Mount those checks in front of it — on a localhost bind, the `Host` check is what stops **DNS rebinding**, a malicious page resolving its own domain to `127.0.0.1` so the browser treats your local server as same-origin.

Under a framework you never wire either check by hand: `createMcpExpressApp`, `createMcpHonoApp`, and `createMcpFastifyApp` all arm both by default on localhost binds — the [Express](https://ts.sdk.modelcontextprotocol.io/v2/serving/express.md), [Hono](https://ts.sdk.modelcontextprotocol.io/v2/serving/hono.md), and [Fastify](https://ts.sdk.modelcontextprotocol.io/v2/serving/fastify.md) recipes start there. On plain `node:http`, compose `localhostHostValidation` and `localhostOriginValidation` (from `@modelcontextprotocol/node`) in front of the wrapped handler, as [the mount above](#mount-it-on-your-runtime) does. On a bare fetch runtime, put `hostHeaderValidationResponse` and `originValidationResponse` (from `@modelcontextprotocol/server`) in front of `handler.fetch` — [Serve on web-standard runtimes](https://ts.sdk.modelcontextprotocol.io/v2/serving/web-standard.md#protect-against-dns-rebinding) builds that wrapper.

## Pass authentication through

`authInfo` is pass-through: the handler never reads it from headers and never verifies a token. Verify the bearer token in front of the handler and hand it the result as `fetch`'s second argument, `handler.fetch(request, { authInfo })`; the factory reads it back as `authInfo`, and tool handlers as `ctx.http.authInfo`.

Under a Node framework the verifying middleware runs first and `toNodeHandler` forwards what it sets — each recipe shows its own mount, and [Require authorization](https://ts.sdk.modelcontextprotocol.io/v2/serving/authorization.md) builds the verifier with `requireBearerAuth`.

With an `AuthInfo` whose `clientId` is `alice`, `whoami` from [the factory above](#understand-the-per-request-factory) answers:

```
[ { type: 'text', text: 'alice' } ]
```

## Shape the response stream

The handler answers a request with a single JSON body and upgrades to an SSE stream only when a tool handler emits a notification — progress, logging — before its result. `responseMode` pins one shape instead.

```ts
const jsonOnly = createMcpHandler(factory, { responseMode: 'json' });
```

`'json'` never streams: the SDK drops mid-call notifications and delivers only the terminal result. `'sse'` always streams. `subscriptions/listen` streams stay on SSE whichever you pick.

::: info
The handler serves 2025-era clients statelessly from the same factory by default. The `legacy` option — and where the SSE transport went — is on [Support legacy clients](https://ts.sdk.modelcontextprotocol.io/v2/serving/legacy-clients.md).
:::

## Shut down

`handler.close()` aborts in-flight exchanges and closes their per-request instances; the handler holds nothing else.

```ts
process.on('SIGINT', async () => {
    await handler.close();
    process.exit(0);
});
```

`close()` resolves once every in-flight instance has closed; `fetch` then throws on any further request.

## Recap

- `createMcpHandler(factory)` returns `{ fetch, close, notify, bus }`; `fetch` is a web-standard `(Request) => Promise<Response>`.
- The factory builds one fresh instance per request and receives `era`, `authInfo`, and `requestInfo`.
- `export default handler` mounts it on web-standard runtimes; `toNodeHandler(handler)` mounts it once under Node frameworks.
- The handler validates no `Host` or `Origin` header and verifies no token — mount both checks in front of it; the framework app factories arm the header checks for you.
- `authInfo` flows from `fetch(request, { authInfo })` into the factory and tool handlers; each framework recipe shows its own mount.
- `responseMode` pins the response shape; `'json'` drops mid-call notifications.
