You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(hono): add mcp() middleware to serve MCP in one call
Adds mcp(factory, options?) — a single Hono MiddlewareHandler that serves
MCP over Streamable HTTP with JSON body parsing and localhost
DNS-rebinding / origin protection applied (the same defaults as
createMcpHonoApp), so it can be mounted on a route you already own:
app.all('/mcp', mcp(() => new McpServer({ name: '...', version: '...' })))
It builds on createMcpHandler rather than a raw transport, so the endpoint
serves the modern 2026-07-28 protocol and falls back to stateless 2025-era
serving, with a fresh server per request. The inner app is self-contained,
so only the raw request is forwarded to it — this also avoids
c.executionCtx, which throws off Cloudflare Workers.
Feature and API shape inspired by yusukebe's mcp-server-hono-middleware.
- packages/middleware/hono/src/hono.ts: mcp() + McpMiddlewareOptions
- packages/middleware/hono/test/mcp.test.ts: real-client tests over both
the legacy (2025) and modern (2026-07-28) paths, plus protection tests
- README + docs/serving/hono.md: feature mcp() as the preferred path,
keep createMcpHonoApp + createMcpHandler as the own-your-routing form
`createMcpHandler` turns a server factory into a web-standard HTTP handler, and `handler.fetch` takes the `Request` a Hono route already holds as `c.req.raw` — no Node adapter. `createMcpHonoApp` is `new Hono()` with JSON body parsing and DNS rebinding protection already applied.
12
+
`mcp(factory)` is the shortest path: it returns a single Hono middleware that serves MCP over Streamable HTTP, with JSON body parsing and DNS rebinding protection already applied. It builds on `createMcpHandler`, so the endpoint serves the modern 2026-07-28 protocol and falls back to stateless 2025-era serving — a fresh `McpServer` from your factory backs every request. Mount it on a route you already own.
`app` is an ordinary Hono app, and `export default app` is the `{ fetch }` object Cloudflare Workers, Deno, and Bun serve directly; on Node, pass `app` to `serve` from `@hono/node-server`.
40
+
41
+
### Wire the handler and route yourself
42
+
43
+
When you want to own the routing — mount multiple endpoints, add your own middleware, or pass `authInfo` per request — drop down to `createMcpHandler` + `createMcpHonoApp` (which is what `mcp()` composes for you). `createMcpHandler` turns the same factory into a web-standard HTTP handler, and `handler.fetch` takes the `Request` a Hono route already holds as `c.req.raw` — no Node adapter. `createMcpHonoApp` is `new Hono()` with the same JSON body parsing and DNS rebinding protection applied.
`app` is an ordinary Hono app, and `export default app` is the `{ fetch }` object Cloudflare Workers, Deno, and Bun serve directly; on Node, pass `app` to `serve` from `@hono/node-server`. The factory runs once per request, so a fresh `McpServer` serves every call: [Serve over HTTP](./http.md#understand-the-per-request-factory) covers that model.
66
+
The factory runs once per request, so a fresh `McpServer` serves every call: [Serve over HTTP](./http.md#understand-the-per-request-factory) covers that model.
35
67
36
68
::: tip
37
69
Keep the explicit `c: Context` annotation: on an inferred callback context `c.get`'s key parameter narrows to `never` and `c.get('parsedBody')` does not compile.
38
70
:::
39
71
40
72
## Protect against DNS rebinding
41
73
42
-
A malicious page can DNS-rebind its own domain to `127.0.0.1` and reach a localhost server as if it were same-origin. `createMcpHonoApp`validates the `Host` and `Origin` headers against that: with the default `127.0.0.1` bind (and `localhost` / `::1`), a request carrying a non-localhost value gets `403` before your handler runs.
74
+
A malicious page can DNS-rebind its own domain to `127.0.0.1` and reach a localhost server as if it were same-origin. `mcp()` and `createMcpHonoApp`both validate the `Host` and `Origin` headers against that: with the default `127.0.0.1` bind (and `localhost` / `::1`), a request carrying a non-localhost value gets `403` before your handler runs.
43
75
44
-
Binding to all interfaces drops that default — name the hosts you serve instead.
76
+
Binding to all interfaces drops that default — name the hosts you serve instead.`mcp()` takes the same `host` / `allowedHosts` / `allowedOrigins` options.
@@ -82,8 +114,8 @@ data: {"result":{"tools":[{"name":"add-note","description":"Append a note","inpu
82
114
83
115
## Recap
84
116
85
-
- One install line, one file: `createMcpHonoApp()` plus one `app.all('/mcp', …)` route over `createMcpHandler(factory).fetch`.
117
+
-`mcp(factory)` is one Hono middleware — `app.all('/mcp', mcp(factory))` serves the whole endpoint (modern + legacy) with body parsing and Host/Origin validation applied.
118
+
- Want to own the routing? Use `createMcpHonoApp()` plus one `app.all('/mcp', …)` route over `createMcpHandler(factory).fetch` — the same pieces `mcp()` composes.
86
119
- Hono hands `c.req.raw` straight to `handler.fetch` — no Node adapter.
87
-
- A fresh server instance from your factory serves every request.
88
120
- The default `127.0.0.1` bind validates `Host` and `Origin`; pass `allowedHosts` when binding to `0.0.0.0`.
89
121
-`authInfo` and `parsedBody` travel in `handler.fetch`'s second argument; handlers read auth as `ctx.http.authInfo`.
0 commit comments