diff --git a/.changeset/suppress-dns-rebinding-warning.md b/.changeset/suppress-dns-rebinding-warning.md new file mode 100644 index 000000000..44c5f9d9b --- /dev/null +++ b/.changeset/suppress-dns-rebinding-warning.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/express': patch +'@modelcontextprotocol/hono': patch +--- + +Add `quiet` option to `createMcpExpressApp` and `createMcpHonoApp` to suppress the DNS rebinding warning when binding to `0.0.0.0` or `::` without `allowedHosts`. diff --git a/packages/middleware/express/src/express.ts b/packages/middleware/express/src/express.ts index 252502952..674fc95d1 100644 --- a/packages/middleware/express/src/express.ts +++ b/packages/middleware/express/src/express.ts @@ -31,6 +31,13 @@ export interface CreateMcpExpressAppOptions { * @example '1mb', '500kb', '10mb' */ jsonLimit?: string; + + /** + * When `true`, suppresses the warning logged when binding to `'0.0.0.0'` or `'::'` + * without `allowedHosts`. Useful when the server is behind a reverse proxy or + * in a containerised environment where DNS rebinding is not a concern. + */ + quiet?: boolean; } /** @@ -60,7 +67,7 @@ export interface CreateMcpExpressAppOptions { * ``` */ export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { - const { host = '127.0.0.1', allowedHosts, jsonLimit } = options; + const { host = '127.0.0.1', allowedHosts, jsonLimit, quiet } = options; const app = express(); app.use(express.json(jsonLimit ? { limit: jsonLimit } : undefined)); @@ -73,7 +80,7 @@ export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): E const localhostHosts = ['127.0.0.1', 'localhost', '::1']; if (localhostHosts.includes(host)) { app.use(localhostHostValidation()); - } else if (host === '0.0.0.0' || host === '::') { + } else if ((host === '0.0.0.0' || host === '::') && !quiet) { // Warn when binding to all interfaces without DNS rebinding protection // eslint-disable-next-line no-console console.warn( diff --git a/packages/middleware/express/test/express.test.ts b/packages/middleware/express/test/express.test.ts index f4be9f998..745169786 100644 --- a/packages/middleware/express/test/express.test.ts +++ b/packages/middleware/express/test/express.test.ts @@ -167,6 +167,26 @@ describe('@modelcontextprotocol/express', () => { warn.mockRestore(); }); + test('should not warn for 0.0.0.0 when quiet is true', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpExpressApp({ host: '0.0.0.0', quiet: true }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + + test('should not warn for :: when quiet is true', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpExpressApp({ host: '::', quiet: true }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + test('should not apply host validation for non-localhost hosts without allowedHosts', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); diff --git a/packages/middleware/hono/src/hono.ts b/packages/middleware/hono/src/hono.ts index eda3e5d8f..a8d9ee17c 100644 --- a/packages/middleware/hono/src/hono.ts +++ b/packages/middleware/hono/src/hono.ts @@ -22,6 +22,13 @@ export interface CreateMcpHonoAppOptions { * to restrict which hostnames are allowed. */ allowedHosts?: string[]; + + /** + * When `true`, suppresses the warning logged when binding to `'0.0.0.0'` or `'::'` + * without `allowedHosts`. Useful when the server is behind a reverse proxy or + * in a containerised environment where DNS rebinding is not a concern. + */ + quiet?: boolean; } /** @@ -39,7 +46,7 @@ export interface CreateMcpHonoAppOptions { * @returns A configured Hono application */ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { - const { host = '127.0.0.1', allowedHosts } = options; + const { host = '127.0.0.1', allowedHosts, quiet } = options; const app = new Hono(); @@ -75,7 +82,7 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { const localhostHosts = ['127.0.0.1', 'localhost', '::1']; if (localhostHosts.includes(host)) { app.use('*', localhostHostValidation()); - } else if (host === '0.0.0.0' || host === '::') { + } else if ((host === '0.0.0.0' || host === '::') && !quiet) { // Warn when binding to all interfaces without DNS rebinding protection. // eslint-disable-next-line no-console console.warn( diff --git a/packages/middleware/hono/test/hono.test.ts b/packages/middleware/hono/test/hono.test.ts index a080f1ffb..3d40cd70e 100644 --- a/packages/middleware/hono/test/hono.test.ts +++ b/packages/middleware/hono/test/hono.test.ts @@ -64,6 +64,26 @@ describe('@modelcontextprotocol/hono', () => { expect(res.status).toBe(200); }); + test('createMcpHonoApp does not warn for 0.0.0.0 when quiet is true', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpHonoApp({ host: '0.0.0.0', quiet: true }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + + test('createMcpHonoApp does not warn for :: when quiet is true', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + createMcpHonoApp({ host: '::', quiet: true }); + + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); + test('createMcpHonoApp parses JSON bodies into parsedBody (express.json()-like)', async () => { const app = createMcpHonoApp(); app.post('/echo', (c: Context) => c.json(c.get('parsedBody')));