Skip to content

Commit aaeff28

Browse files
committed
hono-server updates
1 parent 223fcb0 commit aaeff28

File tree

6 files changed

+335
-22
lines changed

6 files changed

+335
-22
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { BearerAuthMiddlewareOptions } from '@modelcontextprotocol/server';
2+
import { requireBearerAuth as requireBearerAuthWeb } from '@modelcontextprotocol/server';
3+
import type { MiddlewareHandler } from 'hono';
4+
/**
5+
* Hono middleware wrapper for the Web-standard `requireBearerAuth` helper.
6+
*
7+
* On success, sets `c.set('auth', authInfo)` and calls `next()`.
8+
* On failure, returns the JSON error response.
9+
*/
10+
export function requireBearerAuth(options: BearerAuthMiddlewareOptions): MiddlewareHandler {
11+
return async (c, next) => {
12+
const result = await requireBearerAuthWeb(c.req.raw, options);
13+
if ('authInfo' in result) {
14+
c.set('auth', result.authInfo);
15+
return await next();
16+
}
17+
return result.response;
18+
};
19+
}
Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,61 @@
11
import type { AuthMetadataOptions, AuthRoute, AuthRouterOptions } from '@modelcontextprotocol/server';
2-
import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server';
3-
import type { Handler, Hono } from 'hono';
4-
5-
export type RegisterMcpAuthRoutesOptions = AuthRouterOptions;
2+
import {
3+
getParsedBody,
4+
mcpAuthMetadataRouter as createWebAuthMetadataRouter,
5+
mcpAuthRouter as createWebAuthRouter
6+
} from '@modelcontextprotocol/server';
7+
import type { Handler } from 'hono';
8+
import { Hono } from 'hono';
69

710
/**
8-
* Registers the standard MCP OAuth endpoints on a Hono app.
11+
* Hono router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`.
12+
*
13+
* IMPORTANT: This router MUST be mounted at the application root.
914
*
10-
* IMPORTANT: These routes MUST be mounted at the application root.
15+
* @example
16+
* ```ts
17+
* app.route('/', mcpAuthRouter(...))
18+
* ```
1119
*/
12-
export function registerMcpAuthRoutes(app: Hono, options: RegisterMcpAuthRoutesOptions): void {
20+
export function mcpAuthRouter(options: AuthRouterOptions): Hono {
1321
const web = createWebAuthRouter(options);
14-
registerRoutes(app, web.routes);
22+
const router = new Hono();
23+
registerRoutes(router, web.routes);
24+
return router;
1525
}
1626

1727
/**
18-
* Registers only the auth metadata endpoints (RFC 8414 + RFC 9728) on a Hono app.
28+
* Hono router adapter for the Web-standard `mcpAuthMetadataRouter` from `@modelcontextprotocol/server`.
1929
*
20-
* IMPORTANT: These routes MUST be mounted at the application root.
30+
* IMPORTANT: This router MUST be mounted at the application root.
2131
*/
22-
export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void {
32+
export function mcpAuthMetadataRouter(options: AuthMetadataOptions): Hono {
2333
const web = createWebAuthMetadataRouter(options);
24-
registerRoutes(app, web.routes);
34+
const router = new Hono();
35+
registerRoutes(router, web.routes);
36+
return router;
2537
}
2638

2739
function registerRoutes(app: Hono, routes: AuthRoute[]): void {
2840
for (const route of routes) {
29-
// Hono's `on()` expects methods like 'GET', 'POST', etc.
30-
const handler: Handler = c => route.handler(c.req.raw);
31-
app.on(route.methods, route.path, handler);
41+
// Use `all()` so unsupported methods still reach the handler and can return 405,
42+
// matching the Express adapter behavior.
43+
const handler: Handler = async c => {
44+
let parsedBody = c.get('parsedBody');
45+
if (parsedBody === undefined && c.req.method === 'POST') {
46+
// Parse from a clone so we don't consume the original request stream.
47+
parsedBody = await getParsedBody(c.req.raw.clone());
48+
}
49+
return route.handler(c.req.raw, { parsedBody });
50+
};
51+
app.all(route.path, handler);
3252
}
3353
}
54+
55+
export function registerMcpAuthRoutes(app: Hono, options: AuthRouterOptions): void {
56+
app.route('/', mcpAuthRouter(options));
57+
}
58+
59+
export function registerMcpAuthMetadataRoutes(app: Hono, options: AuthMetadataOptions): void {
60+
app.route('/', mcpAuthMetadataRouter(options));
61+
}

packages/server-hono/src/hono.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Context } from 'hono';
2+
import { Hono } from 'hono';
3+
4+
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
5+
6+
/**
7+
* Options for creating an MCP Hono application.
8+
*/
9+
export interface CreateMcpHonoAppOptions {
10+
/**
11+
* The hostname to bind to. Defaults to '127.0.0.1'.
12+
* When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled.
13+
*/
14+
host?: string;
15+
16+
/**
17+
* List of allowed hostnames for DNS rebinding protection.
18+
* If provided, host header validation will be applied using this list.
19+
* For IPv6, provide addresses with brackets (e.g., '[::1]').
20+
*
21+
* This is useful when binding to '0.0.0.0' or '::' but still wanting
22+
* to restrict which hostnames are allowed.
23+
*/
24+
allowedHosts?: string[];
25+
}
26+
27+
/**
28+
* Creates a Hono application pre-configured for MCP servers.
29+
*
30+
* When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'),
31+
* DNS rebinding protection middleware is automatically applied to protect against
32+
* DNS rebinding attacks on localhost servers.
33+
*
34+
* This also installs a small JSON body parsing middleware (similar to `express.json()`)
35+
* that stashes the parsed body into `c.set('parsedBody', ...)` when `Content-Type` includes
36+
* `application/json`.
37+
*
38+
* @param options - Configuration options
39+
* @returns A configured Hono application
40+
*/
41+
export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
42+
const { host = '127.0.0.1', allowedHosts } = options;
43+
44+
const app = new Hono();
45+
46+
// Similar to `express.json()`: parse JSON bodies and make them available to MCP adapters via `parsedBody`.
47+
app.use('*', async (c: Context, next) => {
48+
// If an upstream middleware already set parsedBody, keep it.
49+
if (c.get('parsedBody') !== undefined) {
50+
return await next();
51+
}
52+
53+
const ct = c.req.header('content-type') ?? '';
54+
if (!ct.includes('application/json')) {
55+
return await next();
56+
}
57+
58+
try {
59+
// Parse from a clone so we don't consume the original request stream.
60+
const parsed = await c.req.raw.clone().json();
61+
c.set('parsedBody', parsed);
62+
} catch {
63+
// Mirror express.json() behavior loosely: reject invalid JSON.
64+
return c.text('Invalid JSON', 400);
65+
}
66+
67+
return await next();
68+
});
69+
70+
// If allowedHosts is explicitly provided, use that for validation.
71+
if (allowedHosts) {
72+
app.use('*', hostHeaderValidation(allowedHosts));
73+
} else {
74+
// Apply DNS rebinding protection automatically for localhost hosts.
75+
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
76+
if (localhostHosts.includes(host)) {
77+
app.use('*', localhostHostValidation());
78+
} else if (host === '0.0.0.0' || host === '::') {
79+
// Warn when binding to all interfaces without DNS rebinding protection.
80+
// eslint-disable-next-line no-console
81+
console.warn(
82+
`Warning: Server is binding to ${host} without DNS rebinding protection. ` +
83+
'Consider using the allowedHosts option to restrict allowed hosts, ' +
84+
'or use authentication to protect your server.'
85+
);
86+
}
87+
}
88+
89+
return app;
90+
}

packages/server-hono/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from './auth/bearerAuth.js';
12
export * from './auth/router.js';
3+
export * from './hono.js';
24
export * from './middleware/hostHeaderValidation.js';
35
export * from './streamableHttp.js';

packages/server-hono/src/streamableHttp.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
2+
import { getParsedBody } from '@modelcontextprotocol/server';
23
import type { Context, Handler } from 'hono';
34

45
/**
@@ -10,5 +11,13 @@ import type { Context, Handler } from 'hono';
1011
* ```
1112
*/
1213
export function mcpStreamableHttpHandler(transport: WebStandardStreamableHTTPServerTransport): Handler {
13-
return (c: Context) => transport.handleRequest(c.req.raw);
14+
return async (c: Context) => {
15+
let parsedBody = c.get('parsedBody');
16+
if (parsedBody === undefined && c.req.method === 'POST') {
17+
// Parse from a clone so we don't consume the original request stream.
18+
parsedBody = await getParsedBody(c.req.raw.clone());
19+
}
20+
const authInfo = c.get('auth');
21+
return transport.handleRequest(c.req.raw, { authInfo, parsedBody });
22+
};
1423
}

0 commit comments

Comments
 (0)