docs: rebuild the v2 documentation around a task-based page tree (#2397)
This commit is contained in:
parent
708d5455d9
commit
b153505428
1 changed files with 11884 additions and 4971 deletions
129
docs/serving/http.md
Normal file
129
docs/serving/http.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
shape: how-to
|
||||
---
|
||||
|
||||
# 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](./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 source="../../examples/guides/serving/http.examples.ts#createHandler"
|
||||
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](../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](../servers/notifications.md).
|
||||
|
||||
::: info Coming from v1?
|
||||
`createMcpHandler` replaces the per-request `StreamableHTTPServerTransport` + `connect()` wiring — run the codemod, then see the [upgrade guide](../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 source="../../examples/guides/serving/http.examples.ts#factoryContext"
|
||||
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](../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](./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`:
|
||||
|
||||
```ts source="../../examples/guides/serving/http.examples.ts#mountNode"
|
||||
createServer(toNodeHandler(handler)).listen(3000);
|
||||
```
|
||||
|
||||
`POST http://localhost:3000/mcp` now reaches the factory. The same wrapped handler mounts under [Express](./express.md), [Fastify](./fastify.md), and [Hono](./hono.md); [Serve on web-standard runtimes](./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](./express.md), [Hono](./hono.md), and [Fastify](./fastify.md) recipes start there. On a bare fetch runtime, put `hostHeaderValidationResponse` and `originValidationResponse` (from `@modelcontextprotocol/server`) in front of `handler.fetch` — [Serve on web-standard runtimes](./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](./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 source="../../examples/guides/serving/http.examples.ts#shapeResponse"
|
||||
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](./legacy-clients.md).
|
||||
:::
|
||||
|
||||
## Shut down
|
||||
|
||||
`handler.close()` aborts in-flight exchanges and closes their per-request instances; the handler holds nothing else.
|
||||
|
||||
```ts source="../../examples/guides/serving/http.examples.ts#shutDown"
|
||||
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.
|
||||
Loading…
Reference in a new issue