diff --git a/.changeset/examples-protected-wiring.md b/.changeset/examples-protected-wiring.md new file mode 100644 index 0000000000..dce27b8666 --- /dev/null +++ b/.changeset/examples-protected-wiring.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/node': patch +--- + +Document composing the host and origin validation guards in front of `toNodeHandler` for hand-wired `node:http` servers, matching the protected wiring the examples and serving guide now demonstrate. diff --git a/docs/serving/http.md b/docs/serving/http.md index 228712d2f9..e95552656a 100644 --- a/docs/serving/http.md +++ b/docs/serving/http.md @@ -66,19 +66,25 @@ Because no state lives on the instance, the endpoint is stateless and scales hor ## 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`: +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 source="../../examples/guides/serving/http.examples.ts#mountNode" -createServer(toNodeHandler(handler)).listen(3000); +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://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. +`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](./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. +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 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](./web-standard.md#protect-against-dns-rebinding) builds that wrapper. ## Pass authentication through diff --git a/examples/CONTRIBUTING.md b/examples/CONTRIBUTING.md index ccd1443423..aabbb833c8 100644 --- a/examples/CONTRIBUTING.md +++ b/examples/CONTRIBUTING.md @@ -26,10 +26,9 @@ it (HTTP-only auth, sessionful transports, framework adapters, etc.). ### `server.ts` ```ts -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -46,12 +45,24 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost Host/Origin validation by default. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } ``` +The HTTP leg binds loopback explicitly and mounts the handler behind +`createMcpHonoApp()`, which applies host/origin validation by default — +matching the framework factories' defaults. A story whose point is the raw +`node:http` wiring (e.g. `gateway/`, `elicitation/`) keeps +`createServer` + `toNodeHandler` but must then bind +`.listen(port, '127.0.0.1')` and compose the `localhostHostValidation()` / +`localhostOriginValidation()` guards from `@modelcontextprotocol/node` in +front of the handler. Either way, no example teaches an unguarded HTTP mount. + ### `client.ts` ```ts diff --git a/examples/README.md b/examples/README.md index a35c8bf494..fc57f2de7d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,8 @@ pnpm --filter @mcp-examples/ client -- --http http://127.0.0.1:3000/mcp Add `-- --legacy` to the client command for the 2025-era handshake. +Every HTTP leg serves the handler behind host/origin validation — via `createMcpHonoApp()` (or another framework app factory, which arm the checks by default on localhost binds), or, for raw `node:http` stories, via the `localhostHostValidation()` / `localhostOriginValidation()` guards from `@modelcontextprotocol/node` composed in front of `toNodeHandler` on an explicit loopback bind. New stories follow the canonical skeleton — see [CONTRIBUTING.md](./CONTRIBUTING.md). + The one exception to the generic commands is the reference pair: [`cli-client/`](./cli-client/README.md) and [`todos-server/`](./todos-server/README.md) have their own entry points (`pnpm --filter @mcp-examples/cli-client start`, `pnpm --filter @mcp-examples/todos-server start:http`) — see their READMEs. ## Start here diff --git a/examples/caching/package.json b/examples/caching/package.json index 173bbe05eb..2a3b89d8a3 100644 --- a/examples/caching/package.json +++ b/examples/caching/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*" }, "devDependencies": { diff --git a/examples/caching/server.ts b/examples/caching/server.ts index fe93e66e2c..7e762a6ba3 100644 --- a/examples/caching/server.ts +++ b/examples/caching/server.ts @@ -13,10 +13,9 @@ * The fields are emitted ONLY toward 2026-era clients — a 2025-era response * is byte-for-byte unchanged. One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -96,7 +95,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/custom-methods/package.json b/examples/custom-methods/package.json index 3d5761985b..950607681c 100644 --- a/examples/custom-methods/package.json +++ b/examples/custom-methods/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/custom-methods/server.ts b/examples/custom-methods/server.ts index b21e8e4963..91f1438fdd 100644 --- a/examples/custom-methods/server.ts +++ b/examples/custom-methods/server.ts @@ -5,10 +5,9 @@ * One binary, either transport — selected by `--http --port ` (defaults to * stdio). See `examples/CONTRIBUTING.md` for the canonical shape. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import { z } from 'zod/v4'; @@ -36,7 +35,10 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/custom-version/package.json b/examples/custom-version/package.json index d4f51326d9..c072eb7a34 100644 --- a/examples/custom-version/package.json +++ b/examples/custom-version/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*" }, "devDependencies": { diff --git a/examples/custom-version/server.ts b/examples/custom-version/server.ts index ae31799dea..fff4dc99d6 100644 --- a/examples/custom-version/server.ts +++ b/examples/custom-version/server.ts @@ -3,10 +3,9 @@ * The first version in the list is the fallback when the client requests an * unsupported one. One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -33,7 +32,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/dual-era/package.json b/examples/dual-era/package.json index a9b0fe8737..d188f6d4df 100644 --- a/examples/dual-era/package.json +++ b/examples/dual-era/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/dual-era/server.ts b/examples/dual-era/server.ts index 9313acb436..7af3260d46 100644 --- a/examples/dual-era/server.ts +++ b/examples/dual-era/server.ts @@ -13,10 +13,9 @@ * (`createMcpHandler(buildServer)` on its default posture — modern served per * request, 2025-era traffic served stateless from the same factory). */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { CallToolResult, McpRequestContext } from '@modelcontextprotocol/server'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -49,7 +48,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/elicitation/server.ts b/examples/elicitation/server.ts index 37808e9d21..9157a07a01 100644 --- a/examples/elicitation/server.ts +++ b/examples/elicitation/server.ts @@ -23,7 +23,13 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { createServer } from 'node:http'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest } from '@modelcontextprotocol/node'; +import { + localhostHostValidation, + localhostOriginValidation, + NodeStreamableHTTPServerTransport, + toNodeHandler, + toWebRequest +} from '@modelcontextprotocol/node'; import type { CallToolResult, ElicitRequestFormParams, @@ -274,7 +280,15 @@ if (transport === 'stdio') { } }; + // Host/Origin guards for the hand-wired `node:http` server — plain + // `createServer` has no middleware chain, so compose the boolean-returning + // guards from `@modelcontextprotocol/node` in front of both arms, + // matching the framework factories' localhost defaults. + const validateHost = localhostHostValidation(); + const validateOrigin = localhostOriginValidation(); + createServer((req, res) => { + if (!validateHost(req, res) || !validateOrigin(req, res)) return; void (async () => { // `toWebRequest` reads the Node body into a web-standard `Request`, // so the body now lives in `request`, not `req`. Ask the predicate @@ -289,7 +303,7 @@ if (transport === 'stdio') { console.error('[server] request error:', error instanceof Error ? error.message : error); if (!res.headersSent) res.writeHead(500).end(); }); - }).listen(port, () => { + }).listen(port, '127.0.0.1', () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/extension-capabilities/package.json b/examples/extension-capabilities/package.json index 1acb70d384..78c6c469c9 100644 --- a/examples/extension-capabilities/package.json +++ b/examples/extension-capabilities/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*" }, "devDependencies": { diff --git a/examples/extension-capabilities/server.ts b/examples/extension-capabilities/server.ts index 9b6ac21ace..13c2bb5efc 100644 --- a/examples/extension-capabilities/server.ts +++ b/examples/extension-capabilities/server.ts @@ -6,10 +6,9 @@ * One binary, either transport — selected by `--http --port ` (defaults to * stdio). See `examples/CONTRIBUTING.md` for the canonical shape. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -31,7 +30,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default; + // bind loopback explicitly to match. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/gateway/server.ts b/examples/gateway/server.ts index ec0d657d27..08fc368032 100644 --- a/examples/gateway/server.ts +++ b/examples/gateway/server.ts @@ -9,7 +9,7 @@ import { createServer } from 'node:http'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { localhostHostValidation, localhostOriginValidation, toNodeHandler } from '@modelcontextprotocol/node'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; @@ -44,6 +44,16 @@ function buildServer(): McpServer { const { port } = parseExampleArgs(); const handler = createMcpHandler(buildServer); -createServer(toNodeHandler(handler)).listen(port, () => { +const nodeHandler = toNodeHandler(handler); +// Bind loopback explicitly and apply host/origin validation in front of the +// handler, matching the framework factories' defaults. The guards answer +// rejected requests themselves and never reach `createMcpHandler`, so the +// per-request factory count the client asserts on is unchanged. +const validateHost = localhostHostValidation(); +const validateOrigin = localhostOriginValidation(); +createServer((req, res) => { + if (!validateHost(req, res) || !validateOrigin(req, res)) return; + void nodeHandler(req, res); +}).listen(port, '127.0.0.1', () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); diff --git a/examples/guides/serving/http.examples.ts b/examples/guides/serving/http.examples.ts index 77979ec3d3..1a8c9666de 100644 --- a/examples/guides/serving/http.examples.ts +++ b/examples/guides/serving/http.examples.ts @@ -14,7 +14,7 @@ /* eslint-disable no-console */ import { createServer } from 'node:http'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { localhostHostValidation, localhostOriginValidation, toNodeHandler } from '@modelcontextprotocol/node'; import type { McpServerFactory } from '@modelcontextprotocol/server'; // --------------------------------------------------------------------------- @@ -62,7 +62,13 @@ const perCaller = createMcpHandler(({ authInfo }) => { /** Example: mounting the handler on plain `node:http`. */ function mountNode(): void { //#region mountNode - createServer(toNodeHandler(handler)).listen(3000); + 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'); //#endregion mountNode } void mountNode; diff --git a/examples/json-response/package.json b/examples/json-response/package.json index 4ec6e1c857..2c81cbb969 100644 --- a/examples/json-response/package.json +++ b/examples/json-response/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/json-response/server.ts b/examples/json-response/server.ts index d954db09d1..90abb4792a 100644 --- a/examples/json-response/server.ts +++ b/examples/json-response/server.ts @@ -7,10 +7,9 @@ * HTTP-only — `responseMode` shapes the HTTP response body; there is no stdio * equivalent and a stdio leg would not exercise the option. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; @@ -29,6 +28,10 @@ const { port } = parseExampleArgs(); // `responseMode: 'json'` is the point of this story — applies to the modern // (2026-07-28) per-request HTTP path. const handler = createMcpHandler(buildServer, { responseMode: 'json' }); -createServer(toNodeHandler(handler)).listen(port, () => { +// `createMcpHonoApp()` binds the endpoint behind localhost host/origin +// validation by default, matching the framework factories' defaults. +const app = createMcpHonoApp(); +app.all('/mcp', c => handler.fetch(c.req.raw)); +serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); diff --git a/examples/mrtr/package.json b/examples/mrtr/package.json index eca6285a84..35c336590c 100644 --- a/examples/mrtr/package.json +++ b/examples/mrtr/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/mrtr/server.ts b/examples/mrtr/server.ts index 66fe7bb82d..1f89fe236d 100644 --- a/examples/mrtr/server.ts +++ b/examples/mrtr/server.ts @@ -22,10 +22,9 @@ * One binary, either transport — selected by `--http --port ` (defaults to * stdio). See `examples/CONTRIBUTING.md` for the canonical shape. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { CallToolResult, InputRequiredResult } from '@modelcontextprotocol/server'; import { acceptedContent, createMcpHandler, createRequestStateCodec, inputRequired, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -114,7 +113,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/oauth/simpleOAuthClient.ts b/examples/oauth/simpleOAuthClient.ts index 8bea598f1f..cab70d5d2a 100644 --- a/examples/oauth/simpleOAuthClient.ts +++ b/examples/oauth/simpleOAuthClient.ts @@ -13,7 +13,7 @@ import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider'; // Configuration const DEFAULT_SERVER_URL = 'http://127.0.0.1:3000/mcp'; const CALLBACK_PORT = 8090; // Use different port than auth server (3001) -const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`; +const CALLBACK_URL = `http://127.0.0.1:${CALLBACK_PORT}/callback`; /** Minimal HTML escaper for any user/query-derived value interpolated into an HTML response. */ function escHtml(s: string): string { @@ -128,8 +128,10 @@ class InteractiveOAuthClient { } }); - server.listen(CALLBACK_PORT, () => { - console.log(`OAuth callback server started on http://localhost:${CALLBACK_PORT}`); + // Bind loopback explicitly — the callback target only ever needs to be + // reachable from the local browser, matching the framework factories' defaults. + server.listen(CALLBACK_PORT, '127.0.0.1', () => { + console.log(`OAuth callback server started on ${CALLBACK_URL}`); }); }); } diff --git a/examples/parallel-calls/package.json b/examples/parallel-calls/package.json index 7bf6831f81..be186df74d 100644 --- a/examples/parallel-calls/package.json +++ b/examples/parallel-calls/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/parallel-calls/server.ts b/examples/parallel-calls/server.ts index c2b390b32b..f02e578929 100644 --- a/examples/parallel-calls/server.ts +++ b/examples/parallel-calls/server.ts @@ -4,10 +4,9 @@ * calls (both transports), asserting in-flight notifications are attributed * back to the right caller. One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; @@ -43,7 +42,10 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/prompts/package.json b/examples/prompts/package.json index 4f46b52436..2f46943c16 100644 --- a/examples/prompts/package.json +++ b/examples/prompts/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/prompts/server.ts b/examples/prompts/server.ts index 8732d3d748..60cdab6172 100644 --- a/examples/prompts/server.ts +++ b/examples/prompts/server.ts @@ -5,10 +5,9 @@ * `completable(...)` so the client's `complete()` call returns suggestions. * One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { completable, createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; @@ -48,7 +47,10 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default; bind loopback explicitly to match. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/resources/package.json b/examples/resources/package.json index 5fb5df3b83..a7ac5f38f6 100644 --- a/examples/resources/package.json +++ b/examples/resources/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*" }, "devDependencies": { diff --git a/examples/resources/server.ts b/examples/resources/server.ts index 2ac10338d6..89c1234ada 100644 --- a/examples/resources/server.ts +++ b/examples/resources/server.ts @@ -9,10 +9,9 @@ * on the handler's notifier for clients on other requests. One binary, either * transport — selected from argv below. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { McpRequestContext } from '@modelcontextprotocol/server'; import { createMcpHandler, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -91,7 +90,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(reqCtx => buildServer(reqCtx, uri => handler.notify.resourceUpdated(uri))); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/sampling/package.json b/examples/sampling/package.json index f4981410af..7d0b10477e 100644 --- a/examples/sampling/package.json +++ b/examples/sampling/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/sampling/server.ts b/examples/sampling/server.ts index cf45c3a356..d8a5da5f98 100644 --- a/examples/sampling/server.ts +++ b/examples/sampling/server.ts @@ -15,10 +15,9 @@ * One binary, either transport. Logs go to stderr only — stdio's stdout is * the JSON-RPC stream. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { CallToolResult, InputRequiredResult, McpRequestContext } from '@modelcontextprotocol/server'; import { createMcpHandler, inputRequired, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -71,7 +70,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/schema-validators/package.json b/examples/schema-validators/package.json index cc94d3b645..0ec03d44f1 100644 --- a/examples/schema-validators/package.json +++ b/examples/schema-validators/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "@valibot/to-json-schema": "catalog:devTools", "arktype": "catalog:devTools", diff --git a/examples/schema-validators/server.ts b/examples/schema-validators/server.ts index 95408951ce..326a945004 100644 --- a/examples/schema-validators/server.ts +++ b/examples/schema-validators/server.ts @@ -5,10 +5,9 @@ * Valibot needs the `@valibot/to-json-schema` wrapper to expose JSON Schema * conversion. One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import { toStandardJsonSchema } from '@valibot/to-json-schema'; @@ -81,7 +80,10 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index 51606aa795..49b507e4b8 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -316,9 +316,10 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { } }); - // Start the auth server + // Start the auth server, bound to loopback explicitly — this demo server is + // meant to be reached only from the same machine. const authPort = Number.parseInt(authServerUrl.port, 10); - authApp.listen(authPort, (error?: Error) => { + authApp.listen(authPort, '127.0.0.1', (error?: Error) => { if (error) { console.error('Failed to start auth server:', error); // eslint-disable-next-line unicorn/no-process-exit diff --git a/examples/stateless-legacy/package.json b/examples/stateless-legacy/package.json index 8052f36367..190b8460aa 100644 --- a/examples/stateless-legacy/package.json +++ b/examples/stateless-legacy/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/stateless-legacy/server.ts b/examples/stateless-legacy/server.ts index a51ee5ab48..e141be0147 100644 --- a/examples/stateless-legacy/server.ts +++ b/examples/stateless-legacy/server.ts @@ -11,10 +11,9 @@ * hosting concern; a stdio leg would bypass it. See `dual-era/` for the stdio * analogue. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; @@ -31,6 +30,10 @@ function buildServer(): McpServer { const { port } = parseExampleArgs(); const handler = createMcpHandler(buildServer); -createServer(toNodeHandler(handler)).listen(port, () => { +// `createMcpHonoApp()` binds the endpoint behind localhost host/origin +// validation by default, matching the framework factories' defaults. +const app = createMcpHonoApp(); +app.all('/mcp', c => handler.fetch(c.req.raw)); +serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); diff --git a/examples/stickynotes/package.json b/examples/stickynotes/package.json index 2e5ce67e09..61a98e8b29 100644 --- a/examples/stickynotes/package.json +++ b/examples/stickynotes/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/stickynotes/server.ts b/examples/stickynotes/server.ts index c3baf45b48..441ff2915f 100644 --- a/examples/stickynotes/server.ts +++ b/examples/stickynotes/server.ts @@ -17,10 +17,9 @@ * * One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { RegisteredResource } from '@modelcontextprotocol/server'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -121,7 +120,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/streaming/package.json b/examples/streaming/package.json index 381e19a7e6..61dceffbff 100644 --- a/examples/streaming/package.json +++ b/examples/streaming/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/streaming/server.ts b/examples/streaming/server.ts index 0e45e320a7..219c6ad57f 100644 --- a/examples/streaming/server.ts +++ b/examples/streaming/server.ts @@ -6,10 +6,9 @@ * (when the server has the `logging` capability), and stops promptly when the * client cancels (`ctx.mcpReq.signal.aborted`). One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import * as z from 'zod/v4'; @@ -65,7 +64,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default; + // bind loopback explicitly to match. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, hostname: '127.0.0.1', port }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/subscriptions/package.json b/examples/subscriptions/package.json index 880618ea28..cd30b2ee16 100644 --- a/examples/subscriptions/package.json +++ b/examples/subscriptions/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/subscriptions/server.ts b/examples/subscriptions/server.ts index 4681de1693..3fd1693fe6 100644 --- a/examples/subscriptions/server.ts +++ b/examples/subscriptions/server.ts @@ -16,10 +16,9 @@ * so the client decides when to mutate (no timer race with the runner). The * canonical-shape transport branch below assigns `publish` per entry. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { RegisteredTool, ServerEventBus, ServerNotifier } from '@modelcontextprotocol/server'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -83,7 +82,11 @@ if (transport === 'stdio') { const notify: ServerNotifier = handler.notify; void bus; // (the typed publish facade `notify` wraps `bus.publish`) publish = () => notify.toolsChanged(); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` arms localhost host/origin validation by default; + // bind loopback explicitly to match. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/todos-server/package.json b/examples/todos-server/package.json index a7798a2abf..f55c1f16dd 100644 --- a/examples/todos-server/package.json +++ b/examples/todos-server/package.json @@ -7,8 +7,9 @@ "start:http": "tsx server.ts --http" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/todos-server/server.ts b/examples/todos-server/server.ts index 72ea371608..1c3271219d 100644 --- a/examples/todos-server/server.ts +++ b/examples/todos-server/server.ts @@ -3,10 +3,9 @@ * todos.ts). Same dual-transport skeleton as every other example: stdio by default * (cli-client spawns it as a child process), Streamable HTTP behind `--http`. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -23,7 +22,11 @@ if (transport === 'stdio') { // events (the board changing) are published through the handler's notifier instead. onBoardChanged(() => handler.notify.resourcesChanged()); onBoardUpdated(uri => handler.notify.resourceUpdated(uri)); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[todos] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/examples/tools/package.json b/examples/tools/package.json index d54e05fc22..8aafe0598e 100644 --- a/examples/tools/package.json +++ b/examples/tools/package.json @@ -7,9 +7,10 @@ "client": "tsx client.ts" }, "dependencies": { + "@hono/node-server": "catalog:runtimeServerOnly", "@mcp-examples/shared": "workspace:*", "@modelcontextprotocol/client": "workspace:*", - "@modelcontextprotocol/node": "workspace:*", + "@modelcontextprotocol/hono": "workspace:*", "@modelcontextprotocol/server": "workspace:*", "zod": "catalog:runtimeShared" }, diff --git a/examples/tools/server.ts b/examples/tools/server.ts index 3456aa1027..c10fc16996 100644 --- a/examples/tools/server.ts +++ b/examples/tools/server.ts @@ -6,10 +6,9 @@ * `structuredContent` from `outputSchema`, `annotations` for behavioral hints * (`readOnlyHint`, `destructiveHint`). One binary, either transport. */ -import { createServer } from 'node:http'; - +import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; -import { toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import type { CallToolResult } from '@modelcontextprotocol/server'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; @@ -59,7 +58,11 @@ if (transport === 'stdio') { console.error('[server] serving over stdio'); } else { const handler = createMcpHandler(buildServer); - createServer(toNodeHandler(handler)).listen(port, () => { + // `createMcpHonoApp()` binds the endpoint behind localhost host/origin + // validation by default, matching the framework factories' defaults. + const app = createMcpHonoApp(); + app.all('/mcp', c => handler.fetch(c.req.raw)); + serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); }); } diff --git a/packages/middleware/node/README.md b/packages/middleware/node/README.md index ceedbcc144..bc279cae3a 100644 --- a/packages/middleware/node/README.md +++ b/packages/middleware/node/README.md @@ -17,6 +17,8 @@ npm install @modelcontextprotocol/server @modelcontextprotocol/node - `NodeStreamableHTTPServerTransport` - `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`) - `toNodeHandler(handler, opts?)` — adapt a web-standard `{ fetch }` MCP handler to a Node `(req, res, parsedBody?)` handler +- `hostHeaderValidation(allowedHostnames)` / `localhostHostValidation()` — `Host` header guards for hand-wired `node:http` servers +- `originValidation(allowedOriginHostnames)` / `localhostOriginValidation()` — `Origin` header guards for hand-wired `node:http` servers - `ToNodeHandlerOptions`, `FetchLikeMcpHandler`, `NodeMcpRequestHandler` (types for `toNodeHandler`) - `toWebRequest(req, parsedBody?, opts?)` — the Node `IncomingMessage` → web-standard `Request` conversion `toNodeHandler` performs internally, exported on its own (for example to feed `isLegacyRequest()` from a hand-wired `(req, res)` handler) - `ToWebRequestOptions` (options type for `toWebRequest`) @@ -45,16 +47,27 @@ app.post('/mcp', async (req, res) => { ### Node.js `http` server +Plain `node:http` has no middleware chain, so bind loopback explicitly and +compose the `Host`/`Origin` guards in front of the transport — matching the +defaults the framework app factories (`createMcpExpressApp`, +`createMcpHonoApp`, `createMcpFastifyApp`) apply for you. The guards answer +rejected requests with `403` themselves and return `false`, so the handler +must not touch the request further. + ```ts import { createServer } from 'node:http'; -import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import { McpServer } from '@modelcontextprotocol/server'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); +const validateHost = localhostHostValidation(); +const validateOrigin = localhostOriginValidation(); + createServer(async (req, res) => { + if (!validateHost(req, res) || !validateOrigin(req, res)) return; const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); await transport.handleRequest(req, res); -}).listen(3000); +}).listen(3000, '127.0.0.1'); ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd813db79a..82fdee06b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -427,15 +427,18 @@ importers: examples/caching: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -496,15 +499,18 @@ importers: examples/custom-methods: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -518,15 +524,18 @@ importers: examples/custom-version: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -537,15 +546,18 @@ importers: examples/dual-era: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -581,15 +593,18 @@ importers: examples/extension-capabilities: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -647,15 +662,18 @@ importers: examples/json-response: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -703,15 +721,18 @@ importers: examples/mrtr: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -784,15 +805,18 @@ importers: examples/parallel-calls: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -806,15 +830,18 @@ importers: examples/prompts: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -862,15 +889,18 @@ importers: examples/resources: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -881,15 +911,18 @@ importers: examples/sampling: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -903,15 +936,18 @@ importers: examples/schema-validators: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1107,15 +1143,18 @@ importers: examples/stateless-legacy: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1129,15 +1168,18 @@ importers: examples/stickynotes: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1151,15 +1193,18 @@ importers: examples/streaming: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1173,15 +1218,18 @@ importers: examples/subscriptions: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1195,12 +1243,15 @@ importers: examples/todos-server: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server @@ -1214,15 +1265,18 @@ importers: examples/tools: dependencies: + '@hono/node-server': + specifier: catalog:runtimeServerOnly + version: 1.19.11(hono@4.12.9) '@mcp-examples/shared': specifier: workspace:* version: link:../shared '@modelcontextprotocol/client': specifier: workspace:* version: link:../../packages/client - '@modelcontextprotocol/node': + '@modelcontextprotocol/hono': specifier: workspace:* - version: link:../../packages/middleware/node + version: link:../../packages/middleware/hono '@modelcontextprotocol/server': specifier: workspace:* version: link:../../packages/server