docs: rebuild the v2 documentation around a task-based page tree (#2397)

This commit is contained in:
Felix Weinberger 2026-06-30 21:18:42 +01:00 • committed by GitHub
commit b153505428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 11884 additions and 4971 deletions

View file

@ -0,0 +1,84 @@
// docs: typecheck-only
/**
* Companion example for `docs/serving/authorization.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The Express middleware needs a listening
* HTTP server and a real authorization server to exercise, so this file only
* typechecks:
*
* pnpm --filter @modelcontextprotocol/examples typecheck
*
* @module
*/
//#region requireBearerAuth_basic
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import {
createMcpExpressApp,
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
requireBearerAuth
} from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import type { AuthInfo, OAuthMetadata } from '@modelcontextprotocol/server';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
const mcpServerUrl = new URL('https://api.example.com/mcp');
const verifier: OAuthTokenVerifier = { verifyAccessToken };
const auth = requireBearerAuth({
verifier,
requiredScopes: ['mcp'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });
const node = toNodeHandler(createMcpHandler(buildServer));
app.all('/mcp', auth, (req, res) => void node(req, res, req.body));
//#endregion requireBearerAuth_basic
//#region tokenVerifier_basic
async function verifyAccessToken(token: string): Promise<AuthInfo> {
const payload = await verifyJwt(token);
return { token, clientId: payload.sub, scopes: payload.scopes, expiresAt: payload.exp };
}
//#endregion tokenVerifier_basic
// Stand-in for your JWT library or RFC 7662 introspection call.
declare function verifyJwt(token: string): Promise<{ sub: string; scopes: string[]; exp: number }>;
// Your authorization server's RFC 8414 metadata document — fetch it from the AS
// at startup or embed it.
const oauthMetadata: OAuthMetadata = {
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
response_types_supported: ['code']
};
//#region metadataRouter_basic
app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl }));
//#endregion metadataRouter_basic
// The per-request factory from the Express recipe; the lead block mounts it behind `auth`.
function buildServer(): McpServer {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
//#region authInfo_handler
server.registerTool('whoami', { description: 'Report the authenticated caller' }, async ctx => {
const caller = ctx.http?.authInfo;
return { content: [{ type: 'text', text: `${caller?.clientId} [${caller?.scopes.join(' ')}]` }] };
});
//#endregion authInfo_handler
//#region perToolScopes_handler
server.registerTool('purge-notes', { description: 'Delete every note' }, async ctx => {
if (!ctx.http?.authInfo?.scopes.includes('notes:write')) {
return { content: [{ type: 'text', text: 'insufficient_scope: purge-notes requires notes:write' }], isError: true };
}
return { content: [{ type: 'text', text: 'All notes deleted' }] };
});
//#endregion perToolScopes_handler
return server;
}

View file

@ -0,0 +1,92 @@
/**
* Runnable, type-checked companion for `docs/serving/express.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The
* `createMcpExpressApp_listen` region lives in a wrapper function that is never
* invoked, so running this file never binds a port; the harness below the
* regions produces the response the page's "Run it and verify" section quotes
* verbatim and exits non-zero if it drifts.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/express.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
//#region createMcpExpressApp_mount
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
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: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: `Saved: ${text}` }]
}));
return server;
});
const app = createMcpExpressApp();
const node = toNodeHandler(handler);
app.all('/mcp', (req, res) => void node(req, res, req.body));
//#endregion createMcpExpressApp_mount
//#region createMcpExpressApp_allowedHosts
const publicApp = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });
//#endregion createMcpExpressApp_allowedHosts
// `verifier` stands in for your deployment's token verification (JWT
// validation, RFC 7662 introspection, a call to your IdP). The page points at
// docs/serving/authorization.md for the real thing.
const verifier: OAuthTokenVerifier = {
async verifyAccessToken(token) {
return { token, clientId: 'docs-harness', scopes: ['mcp'], expiresAt: Date.now() / 1000 + 3600 };
}
};
//#region requireBearerAuth_mount
import { requireBearerAuth } from '@modelcontextprotocol/express';
const auth = requireBearerAuth({ verifier });
publicApp.all('/mcp', auth, (req, res) => void node(req, res, req.body));
//#endregion requireBearerAuth_mount
// "Run it and verify" — the listen line. Never invoked: docs companions must
// terminate on their own and never bind a port.
function createMcpExpressApp_listen(): void {
//#region createMcpExpressApp_listen
app.listen(3000);
//#endregion createMcpExpressApp_listen
}
// ---------------------------------------------------------------------------
// Harness (not shown on the page). The page quotes the response to its curl
// command verbatim; this produces it. The Express route is
// `(req, res) => void node(req, res, req.body)` with `node = toNodeHandler(handler)`,
// and `toNodeHandler` copies `handler.fetch(request)`'s status, headers, and
// body onto `res` unchanged — so `handler.fetch` given the curl command's exact
// request yields the bytes curl prints. (Driving the Express stack itself needs
// a listening socket, which a docs companion never opens.)
// ---------------------------------------------------------------------------
const response = await handler.fetch(
new Request('http://127.0.0.1:3000/mcp', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
})
);
const text = await response.text();
console.log(text);
const quotedOnPage =
'event: message\n' +
'data: {"result":{"tools":[{"name":"add-note","description":"Append a note","inputSchema":{"type":"object",' +
'"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"text":{"type":"string"}},"required":["text"]}}]},"jsonrpc":"2.0","id":1}';
if (response.status !== 200 || text.trimEnd() !== quotedOnPage) {
throw new Error(`express.md "Run it and verify" output drifted from the SDK: ${JSON.stringify(text)}`);
}

View file

@ -0,0 +1,89 @@
/**
* Runnable, type-checked companion for `docs/serving/fastify.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The
* `createMcpFastifyApp_listen` region lives in a wrapper function that is never
* invoked, so running this file never binds a port; the harness below the
* regions drives the real Fastify app in process with `app.inject()`, produces
* the response the page's "Run it and verify" section quotes verbatim, and
* exits non-zero if it drifts.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/fastify.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import type { AuthInfo } from '@modelcontextprotocol/server';
//#region createMcpFastifyApp_mount
import { createMcpFastifyApp } from '@modelcontextprotocol/fastify';
import { toNodeHandler } from '@modelcontextprotocol/node';
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: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: `Saved: ${text}` }]
}));
return server;
});
const app = createMcpFastifyApp();
const node = toNodeHandler(handler);
app.all('/mcp', (request, reply) => node(request.raw, reply.raw, request.body));
//#endregion createMcpFastifyApp_mount
//#region createMcpFastifyApp_allowedHosts
const publicApp = createMcpFastifyApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });
//#endregion createMcpFastifyApp_allowedHosts
// `verifyToken` stands in for your deployment's token verification (JWT
// validation, RFC 7662 introspection, a call to your IdP). The page points at
// docs/serving/authorization.md for the real thing.
async function verifyToken(authorization: string | undefined): Promise<AuthInfo> {
const token = authorization?.replace(/^Bearer /, '') ?? '';
return { token, clientId: 'docs-harness', scopes: ['mcp'], expiresAt: Date.now() / 1000 + 3600 };
}
//#region toNodeHandler_authInfo
publicApp.all('/mcp', async (request, reply) => {
const auth = await verifyToken(request.headers.authorization);
return node(Object.assign(request.raw, { auth }), reply.raw, request.body);
});
//#endregion toNodeHandler_authInfo
// "Run it and verify" — the listen line. Never invoked: docs companions must
// terminate on their own and never bind a port.
async function createMcpFastifyApp_listen(): Promise<void> {
//#region createMcpFastifyApp_listen
await app.listen({ port: 3000 });
//#endregion createMcpFastifyApp_listen
}
// ---------------------------------------------------------------------------
// Harness (not shown on the page). `app.inject()` runs the real Fastify app —
// the Host/Origin validation hooks, the JSON body parser, and the `/mcp` route
// — entirely in process, no socket. The page quotes its payload verbatim.
// ---------------------------------------------------------------------------
const injected = await app.inject({
method: 'POST',
url: '/mcp',
headers: { host: '127.0.0.1:3000', 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
payload: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
});
console.log(injected.payload);
const quotedOnPage =
'event: message\n' +
'data: {"result":{"tools":[{"name":"add-note","description":"Append a note","inputSchema":{"type":"object",' +
'"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"text":{"type":"string"}},"required":["text"]}}]},"jsonrpc":"2.0","id":1}';
if (injected.statusCode !== 200 || injected.payload.trimEnd() !== quotedOnPage) {
throw new Error(`fastify.md "Run it and verify" output drifted from the SDK: ${JSON.stringify(injected.payload)}`);
}
await app.close();
await publicApp.close();

View file

@ -0,0 +1,77 @@
/**
* Runnable, type-checked companion for `docs/serving/hono.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The harness
* below the regions drives the real Hono app in process with `app.request()`,
* produces the response the page's "Run it and verify" section quotes verbatim,
* and exits non-zero if it drifts. No port is ever bound.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/hono.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import type { AuthInfo } from '@modelcontextprotocol/server';
//#region createMcpHonoApp_mount
import { createMcpHonoApp } from '@modelcontextprotocol/hono';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import type { Context } from 'hono';
import * as z from 'zod/v4';
const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool('add-note', { description: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: `Saved: ${text}` }]
}));
return server;
});
const app = createMcpHonoApp();
app.all('/mcp', (c: Context) => handler.fetch(c.req.raw, { parsedBody: c.get('parsedBody') }));
export default app;
//#endregion createMcpHonoApp_mount
//#region createMcpHonoApp_allowedHosts
const publicApp = createMcpHonoApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });
//#endregion createMcpHonoApp_allowedHosts
// `verifyToken` stands in for your deployment's token verification (JWT
// validation, RFC 7662 introspection, a call to your IdP). The page points at
// docs/serving/authorization.md for the real thing.
async function verifyToken(request: Request): Promise<AuthInfo> {
const token = request.headers.get('authorization')?.replace(/^Bearer /, '') ?? '';
return { token, clientId: 'docs-harness', scopes: ['mcp'], expiresAt: Date.now() / 1000 + 3600 };
}
//#region McpHttpHandler_fetch_authInfo
publicApp.all('/mcp', async (c: Context) => {
const authInfo = await verifyToken(c.req.raw);
return handler.fetch(c.req.raw, { authInfo, parsedBody: c.get('parsedBody') });
});
//#endregion McpHttpHandler_fetch_authInfo
// ---------------------------------------------------------------------------
// Harness (not shown on the page). `app.request()` runs the real Hono app —
// the JSON body-parsing middleware, the Host/Origin validation, and the `/mcp`
// route — entirely in process. The page quotes its body verbatim.
// ---------------------------------------------------------------------------
const response = await app.request('/mcp', {
method: 'POST',
headers: { host: '127.0.0.1:8787', 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
});
const text = await response.text();
console.log(text);
const quotedOnPage =
'event: message\n' +
'data: {"result":{"tools":[{"name":"add-note","description":"Append a note","inputSchema":{"type":"object",' +
'"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"text":{"type":"string"}},"required":["text"]}}]},"jsonrpc":"2.0","id":1}';
if (response.status !== 200 || text.trimEnd() !== quotedOnPage) {
throw new Error(`hono.md "Run it and verify" output drifted from the SDK: ${JSON.stringify(text)}`);
}

View file

@ -0,0 +1,139 @@
/**
* Companion example for `docs/serving/http.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
* regions drives `handler.fetch` in process — no port, no socket — and
* produces the output the page quotes verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/http.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import { createServer } from 'node:http';
import { toNodeHandler } from '@modelcontextprotocol/node';
import type { McpServerFactory } from '@modelcontextprotocol/server';
// ---------------------------------------------------------------------------
// "Create a handler"
// ---------------------------------------------------------------------------
//#region 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;
});
//#endregion createHandler
// ---------------------------------------------------------------------------
// "Understand the per-request factory" — the factory reads the request context.
// ---------------------------------------------------------------------------
//#region 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;
});
//#endregion factoryContext
// ---------------------------------------------------------------------------
// "Mount it on your runtime" — never invoked: binding a port is the reader's
// deployment step, not this program's. The region typechecks against the real
// module-scope `handler` above.
// ---------------------------------------------------------------------------
/** Example: mounting the handler on plain `node:http`. */
function mountNode(): void {
//#region mountNode
createServer(toNodeHandler(handler)).listen(3000);
//#endregion mountNode
}
void mountNode;
// ---------------------------------------------------------------------------
// "Validate Host and Origin in front of it" / "Pass authentication through" —
// no regions: http.md states the contract and links to the serving recipes
// (web-standard, express, hono, fastify) that build each mount. The harness
// below produces the `alice` result the auth section quotes.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// "Shape the response stream" — never invoked: `responseMode: 'json'` warns at
// construction by design, and the page quotes nothing from it.
// ---------------------------------------------------------------------------
/** Example: pin the response shape to plain JSON. */
function shapeResponse(factory: McpServerFactory) {
//#region shapeResponse
const jsonOnly = createMcpHandler(factory, { responseMode: 'json' });
//#endregion shapeResponse
return jsonOnly;
}
void shapeResponse;
// ---------------------------------------------------------------------------
// "Shut down" — never invoked: this program exits on its own.
// ---------------------------------------------------------------------------
/** Example: tear down in-flight modern exchanges on SIGINT. */
function shutDown(): void {
//#region shutDown
process.on('SIGINT', async () => {
await handler.close();
process.exit(0);
});
//#endregion shutDown
}
void shutDown;
// ---------------------------------------------------------------------------
// Harness (not shown on the page). A real `Client` drives each handler's
// `fetch` in process — the URL is never dialed (docs/testing.md owns that
// wiring). It produces the two tool results the page quotes verbatim.
// ---------------------------------------------------------------------------
const { Client, StreamableHTTPClientTransport } = await import('@modelcontextprotocol/client');
// "Create a handler" — the add-note result the page quotes.
const client = new Client({ name: 'http-docs-harness', version: '1.0.0' });
await client.connect(
new StreamableHTTPClientTransport(new URL('http://localhost/mcp'), {
fetch: (url, init) => handler.fetch(new Request(url, init))
})
);
const saved = await client.callTool({ name: 'add-note', arguments: { text: 'ship the release notes' } });
console.log(saved.content);
await client.close();
// "Pass authentication through" — the caller hands `fetch` a verified
// `AuthInfo`; the factory above reads it back as `ctx.authInfo`.
const authInfo = { token: 'verified-elsewhere', clientId: 'alice', scopes: ['notes'] };
const alice = new Client({ name: 'http-docs-harness', version: '1.0.0' });
await alice.connect(
new StreamableHTTPClientTransport(new URL('http://localhost/mcp'), {
fetch: (url, init) => perCaller.fetch(new Request(url, init), { authInfo })
})
);
const who = await alice.callTool({ name: 'whoami' });
console.log(who.content);
await alice.close();
await handler.close();
await perCaller.close();

View file

@ -0,0 +1,107 @@
/**
* Companion example for `docs/serving/legacy-clients.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The file also runs: the harness below the
* regions drives `handler.fetch` in process — no port, no socket — and
* produces the output the page quotes verbatim.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/legacy-clients.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import { serveStdio } from '@modelcontextprotocol/server/stdio';
// ---------------------------------------------------------------------------
// "Choose a legacy posture"
// ---------------------------------------------------------------------------
//#region createMcpHandler_legacyReject
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
const buildServer = () => new McpServer({ name: 'notes', version: '1.0.0' });
const strict = createMcpHandler(buildServer, { legacy: 'reject' });
//#endregion createMcpHandler_legacyReject
// ---------------------------------------------------------------------------
// "Choose the same posture on stdio" — never invoked: `serveStdio` over real
// stdio would hold this program open on stdin. The posture is the point.
// ---------------------------------------------------------------------------
/** Example: the same posture on the stdio entry. */
function rejectOnStdio(): void {
//#region serveStdio_legacyReject
serveStdio(buildServer, { legacy: 'reject' });
//#endregion serveStdio_legacyReject
}
void rejectOnStdio;
// ---------------------------------------------------------------------------
// "Keep a sessionful 2025 deployment running"
// ---------------------------------------------------------------------------
//#region isLegacyRequest_route
import { isLegacyRequest, legacyStatelessFallback } from '@modelcontextprotocol/server';
const legacy = legacyStatelessFallback(buildServer);
async function serve(request: Request): Promise<Response> {
if (await isLegacyRequest(request)) {
return legacy(request);
}
return strict.fetch(request);
}
//#endregion isLegacyRequest_route
// ---------------------------------------------------------------------------
// Harness (not shown on the page). A 2025-era client opens with a claim-less
// `initialize` POST; build that request twice and send it to the strict
// handler, then through the `isLegacyRequest` branch. The page quotes both
// outputs verbatim; the self-checks at the bottom exit non-zero if either
// claim stops being observable.
// ---------------------------------------------------------------------------
const legacyInitialize = () =>
new Request('http://localhost/mcp', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'legacy-host', version: '1.0.0' } }
})
});
// "Choose a legacy posture" — the strict rejection the page quotes.
const rejected = await strict.fetch(legacyInitialize());
const rejection = (await rejected.json()) as { error: { code: number; data: { supported: string[]; requested: string } } };
console.log(rejected.status);
console.log(JSON.stringify(rejection, null, 2));
// "Keep a sessionful 2025 deployment running" — the same request through the
// branch reaches the legacy leg and completes the 2025 handshake over SSE.
const served = await serve(legacyInitialize());
const sse = await served.text();
const dataLine = sse.split('\n').find(line => line.startsWith('data: '));
const initialized = JSON.parse(dataLine?.slice('data: '.length) ?? '{}') as {
result: { protocolVersion: string; serverInfo: { name: string; version: string } };
};
console.log(served.status);
console.log(initialized.result);
// Self-verification — the page's claims must stay observable.
if (rejected.status !== 400 || rejection.error.code !== -32_022) {
throw new Error(`expected the 400 / -32022 strict rejection, got ${rejected.status} ${JSON.stringify(rejection)}`);
}
if (rejection.error.data.supported[0] !== '2026-07-28' || rejection.error.data.requested !== '2025-06-18') {
throw new Error(`expected the supported/requested revisions in the error data, got ${JSON.stringify(rejection.error.data)}`);
}
if (served.status !== 200 || initialized.result.protocolVersion !== '2025-06-18') {
throw new Error(`expected the legacy leg to complete the 2025 handshake, got ${served.status} ${JSON.stringify(initialized)}`);
}
await strict.close();

View file

@ -0,0 +1,95 @@
// docs: typecheck-only
/**
* Companion example for `docs/serving/sessions-state-scaling.md`.
*
* Every `ts` fence on that page is synced from a `//#region` in this file
* (`pnpm sync:snippets --check`). The regions are fragments of an HTTP
* deployment — transport options, an Express route, a handler option — and
* none of them may bind a port, so the file is typecheck-only.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
*
* @module
*/
// "Sessions" lead block — sessionIdGenerator turns sessions on for the
// hand-wired 2025-era Streamable HTTP transport.
//#region sessions_stateful
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { randomUUID } from 'node:crypto';
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});
//#endregion sessions_stateful
void transport;
// Imports for the function-wrapped regions below, kept out of the page's lead block.
import type { EventStore, McpServer, ServerEventBus } from '@modelcontextprotocol/server';
import { createMcpHandler, isInitializeRequest } from '@modelcontextprotocol/server';
import type { Express, Request, Response } from 'express';
/**
* "Sessions" follow-up — one transport per session, routed by `Mcp-Session-Id`
* (mined from `examples/legacy-routing/server.ts`).
*/
function sessions_routing(app: Express, buildServer: () => McpServer) {
//#region sessions_routing
const sessions = new Map<string, NodeStreamableHTTPServerTransport>();
const route = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId && sessions.has(sessionId)) {
await sessions.get(sessionId)!.handleRequest(req, res, req.body);
return;
}
if (!sessionId && isInitializeRequest(req.body)) {
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: id => {
sessions.set(id, transport);
}
});
transport.onclose = () => {
if (transport.sessionId) sessions.delete(transport.sessionId);
};
await buildServer().connect(transport);
await transport.handleRequest(req, res, req.body);
return;
}
if (sessionId) {
// Unknown session id: the client should start a new session.
res.status(404).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Session not found' }, id: null });
return;
}
// No session header on a non-initialize request: the request is malformed.
res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: Session ID required' }, id: null });
};
app.post('/mcp', route);
app.get('/mcp', route);
app.delete('/mcp', route);
//#endregion sessions_routing
}
void sessions_routing;
/** "Resumability" — an EventStore implementation next to sessionIdGenerator. */
function resumability_eventStore(databaseEventStore: EventStore) {
//#region resumability_eventStore
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore: databaseEventStore
});
//#endregion resumability_eventStore
return transport;
}
void resumability_eventStore;
/** "Multi-node" — every node hands the same pub/sub-backed bus to createMcpHandler. */
function multiNode_bus(buildServer: () => McpServer, redisBus: ServerEventBus) {
//#region multiNode_bus
const handler = createMcpHandler(buildServer, { bus: redisBus });
//#endregion multiNode_bus
return handler;
}
void multiNode_bus;

View file

@ -0,0 +1,116 @@
/**
* Runnable, type-checked companion for `docs/serving/stdio.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's `ts` fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). The regions
* are one linear stdio server program. The file runs in two modes:
*
* - `node --import tsx stdio.examples.ts --serve` — be that stdio server, plus
* the one deliberate `console.log` the page's gotcha section describes, and
* stay alive on stdin like any stdio server.
* - `node --import tsx stdio.examples.ts` (default) — the harness: spawn this
* file with `--serve`, send it an `initialize` request, and print every line
* the child wrote to each of its streams. The page quotes that output
* verbatim; the harness exits non-zero if the corruption it demonstrates
* ever stops being observable.
*
* @module
*/
/* eslint-disable no-console */
//#region serveStdio_basic
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
const handle = serveStdio(() => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
// server.registerTool(...) — one factory builds the instance that serves the connection
return server;
});
//#endregion serveStdio_basic
//#region serveStdio_logStderr
console.error('notes server is listening on stdio');
//#endregion serveStdio_logStderr
//#region serveStdio_shutdown
process.on('SIGINT', () => {
void handle.close();
});
//#endregion serveStdio_shutdown
// ---------------------------------------------------------------------------
// Harness (not shown on the page).
//
// `--serve` mode: the regions above already made this process a real stdio
// server. Inject the page's gotcha — one `console.log` on the protocol channel
// — and stay alive on stdin (the spawning harness kills the child when done).
//
// Default mode: spawn this file with `--serve`, write a JSON-RPC `initialize`
// request to the child's stdin, then print every line the child wrote to each
// stream. "Log to stderr, never stdout" quotes this output verbatim.
// ---------------------------------------------------------------------------
if (process.argv.includes('--serve')) {
// The bug the page demonstrates: one log line written to stdout.
console.log('debug: starting the notes server');
} else {
const { spawn } = await import('node:child_process');
const { dirname } = await import('node:path');
const { fileURLToPath } = await import('node:url');
const selfPath = fileURLToPath(import.meta.url);
const child = spawn(process.execPath, ['--no-warnings', '--import', 'tsx', selfPath, '--serve'], {
cwd: dirname(selfPath)
});
let childStdout = '';
let childStderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => {
childStdout += chunk;
});
child.stderr.on('data', (chunk: string) => {
childStderr += chunk;
});
// What an MCP host writes first: the `initialize` request, one JSON line on stdin.
child.stdin.write(
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'host', version: '1.0.0' } }
})}\n`
);
// Wait for the `initialize` response to reach the child's stdout, then stop the child.
const deadline = Date.now() + 30_000;
while (!childStdout.includes('"id":1') && Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, 25));
}
child.kill();
await new Promise(resolve => child.once('exit', resolve));
const stdoutLines = childStdout.trimEnd().split('\n');
const stderrLines = childStderr.trimEnd().split('\n');
for (const line of stdoutLines) console.log(`[stdout] ${line}`);
for (const line of stderrLines) console.log(`[stderr] ${line}`);
// Self-verification — the page's claims must stay observable, or this exits non-zero.
if (stdoutLines[0] !== 'debug: starting the notes server') {
throw new Error(`expected the stray console.log first on stdout, got ${JSON.stringify(stdoutLines)}`);
}
if (!stdoutLines[1]?.includes('"jsonrpc":"2.0"')) {
throw new Error(`expected the initialize response next on stdout, got ${JSON.stringify(stdoutLines)}`);
}
if (!stderrLines.includes('notes server is listening on stdio')) {
throw new Error(`expected the console.error banner on stderr, got ${JSON.stringify(stderrLines)}`);
}
await handle.close();
// The regions above also started a real stdio server on this process; end it here.
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
}

View file

@ -0,0 +1,101 @@
/**
* Runnable, type-checked companion for `docs/serving/web-standard.md`.
*
* Each `//#region` block is synced byte-for-byte into that page's code fences by
* `pnpm sync:snippets` (`pnpm sync:snippets --check` reports drift). On a
* web-standard runtime the default export's `fetch` IS the request path, so the
* harness below the regions calls it directly with the page's curl request,
* prints the response the page quotes verbatim, and exits non-zero if it (or
* the `guarded` / `secured` wrappers' behavior) drifts. No port is ever bound.
*
* pnpm --filter @modelcontextprotocol/examples typecheck
* npx tsx guides/serving/webStandard.examples.ts # from examples/
*
* @module
*/
/* eslint-disable no-console */
import type { AuthInfo } from '@modelcontextprotocol/server';
//#region createMcpHandler_exportDefault
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: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: `Saved: ${text}` }]
}));
return server;
});
export default handler;
//#endregion createMcpHandler_exportDefault
//#region hostHeaderValidationResponse_guard
import { hostHeaderValidationResponse, originValidationResponse } from '@modelcontextprotocol/server';
const guarded = {
async fetch(request: Request): Promise<Response> {
const rejected =
hostHeaderValidationResponse(request, ['api.example.com']) ?? originValidationResponse(request, ['app.example.com']);
return rejected ?? handler.fetch(request);
}
};
//#endregion hostHeaderValidationResponse_guard
// `verifyToken` stands in for your deployment's token verification (JWT
// validation, RFC 7662 introspection, a call to your IdP). The page points at
// docs/serving/authorization.md for the real thing.
async function verifyToken(request: Request): Promise<AuthInfo> {
const token = request.headers.get('authorization')?.replace(/^Bearer /, '') ?? '';
return { token, clientId: 'docs-harness', scopes: ['mcp'], expiresAt: Date.now() / 1000 + 3600 };
}
//#region McpHttpHandler_fetch_authInfo
const secured = {
async fetch(request: Request): Promise<Response> {
const authInfo = await verifyToken(request);
return handler.fetch(request, { authInfo });
}
};
//#endregion McpHttpHandler_fetch_authInfo
// ---------------------------------------------------------------------------
// Harness (not shown on the page). `handler.fetch` is exactly what a
// web-standard runtime calls on the default export, so calling it with the
// page's curl request IS the deployment path. The page quotes the body
// verbatim. The two wrapper exports the page describes are exercised too:
// `guarded` must answer 403 for a Host outside its allowlist, and `secured`
// must still serve the request.
// ---------------------------------------------------------------------------
const curlRequest = (): Request =>
new Request('http://127.0.0.1:8787/mcp', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
});
const response = await handler.fetch(curlRequest());
const text = await response.text();
console.log(text);
const quotedOnPage =
'event: message\n' +
'data: {"result":{"tools":[{"name":"add-note","description":"Append a note","inputSchema":{"type":"object",' +
'"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"text":{"type":"string"}},"required":["text"]}}]},"jsonrpc":"2.0","id":1}';
if (response.status !== 200 || text.trimEnd() !== quotedOnPage) {
throw new Error(`web-standard.md "Run it and verify" output drifted from the SDK: ${JSON.stringify(text)}`);
}
// "Protect against DNS rebinding": a Host outside the allowlist never reaches `fetch`.
const guardedResponse = await guarded.fetch(curlRequest());
if (guardedResponse.status !== 403) {
throw new Error(`web-standard.md guard claim failed: expected 403, got ${guardedResponse.status}`);
}
// "Forward auth and the parsed body": the secured wrapper still serves the request.
const securedResponse = await secured.fetch(curlRequest());
if (securedResponse.status !== 200) {
throw new Error(`web-standard.md auth claim failed: expected 200, got ${securedResponse.status}`);
}