diff --git a/docs/server.md b/docs/server.md index b319e6f0f..bfb8dad21 100644 --- a/docs/server.md +++ b/docs/server.md @@ -71,7 +71,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/index.js'; +import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'; // Protection auto-enabled (default host is 127.0.0.1) const app = createMcpExpressApp(); diff --git a/src/examples/server/elicitationFormExample.ts b/src/examples/server/elicitationFormExample.ts index e3ce083d1..6c0800949 100644 --- a/src/examples/server/elicitationFormExample.ts +++ b/src/examples/server/elicitationFormExample.ts @@ -12,7 +12,7 @@ import { type Request, type Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults // The validator supports format validation (email, date, etc.) if ajv-formats is installed diff --git a/src/examples/server/elicitationUrlExample.ts b/src/examples/server/elicitationUrlExample.ts index e4d3d2268..5ddecc4e1 100644 --- a/src/examples/server/elicitationUrlExample.ts +++ b/src/examples/server/elicitationUrlExample.ts @@ -11,7 +11,7 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { McpServer } from '../../server/mcp.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index 9be3d7204..224955c46 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -4,7 +4,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create an MCP server with implementation details const getServer = () => { diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index bc6fd2cab..1cd10cd2d 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -3,7 +3,7 @@ import { McpServer } from '../../server/mcp.js'; import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; /** * This example server demonstrates the deprecated HTTP+SSE transport diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index e2cefffd8..748d82fda 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -3,7 +3,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; const getServer = () => { // Create an MCP server with implementation details diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 3500ac066..ca1363198 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { CallToolResult, ElicitResultSchema, diff --git a/src/examples/server/simpleTaskInteractive.ts b/src/examples/server/simpleTaskInteractive.ts index 51e97b7e9..c35126dc0 100644 --- a/src/examples/server/simpleTaskInteractive.ts +++ b/src/examples/server/simpleTaskInteractive.ts @@ -11,7 +11,8 @@ import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; -import { createMcpExpressApp, Server } from '../../server/index.js'; +import { Server } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult, diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index 317cb2bfe..5c91b7e33 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -6,7 +6,7 @@ import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; /** * This example server demonstrates backwards compatibility with both: diff --git a/src/examples/server/ssePollingExample.ts b/src/examples/server/ssePollingExample.ts index 83ef8e4b1..bbecf2fdb 100644 --- a/src/examples/server/ssePollingExample.ts +++ b/src/examples/server/ssePollingExample.ts @@ -15,7 +15,7 @@ import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; diff --git a/src/examples/server/standaloneSseWithGetStreamableHttp.ts b/src/examples/server/standaloneSseWithGetStreamableHttp.ts index 33bd73d04..546d35c70 100644 --- a/src/examples/server/standaloneSseWithGetStreamableHttp.ts +++ b/src/examples/server/standaloneSseWithGetStreamableHttp.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest, ReadResourceResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create an MCP server with implementation details const server = new McpServer({ diff --git a/src/server/express.ts b/src/server/express.ts new file mode 100644 index 000000000..a542acd7a --- /dev/null +++ b/src/server/express.ts @@ -0,0 +1,74 @@ +import express, { Express } from 'express'; +import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; + +/** + * Options for creating an MCP Express application. + */ +export interface CreateMcpExpressAppOptions { + /** + * The hostname to bind to. Defaults to '127.0.0.1'. + * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. + */ + host?: string; + + /** + * List of allowed hostnames for DNS rebinding protection. + * If provided, host header validation will be applied using this list. + * For IPv6, provide addresses with brackets (e.g., '[::1]'). + * + * This is useful when binding to '0.0.0.0' or '::' but still wanting + * to restrict which hostnames are allowed. + */ + allowedHosts?: string[]; +} + +/** + * Creates an Express application pre-configured for MCP servers. + * + * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), + * DNS rebinding protection middleware is automatically applied to protect against + * DNS rebinding attacks on localhost servers. + * + * @param options - Configuration options + * @returns A configured Express application + * + * @example + * ```typescript + * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * const app = createMcpExpressApp(); + * + * // Custom host - DNS rebinding protection only applied for localhost hosts + * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + * + * // Custom allowed hosts for non-localhost binding + * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + * ``` + */ +export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { + const { host = '127.0.0.1', allowedHosts } = options; + + const app = express(); + app.use(express.json()); + + // If allowedHosts is explicitly provided, use that for validation + if (allowedHosts) { + app.use(hostHeaderValidation(allowedHosts)); + } else { + // Apply DNS rebinding protection automatically for localhost hosts + const localhostHosts = ['127.0.0.1', 'localhost', '::1']; + if (localhostHosts.includes(host)) { + app.use(localhostHostValidation()); + } else if (host === '0.0.0.0' || host === '::') { + // Warn when binding to all interfaces without DNS rebinding protection + // eslint-disable-next-line no-console + console.warn( + `Warning: Server is binding to ${host} without DNS rebinding protection. ` + + 'Consider using the allowedHosts option to restrict allowed hosts, ' + + 'or use authentication to protect your server.' + ); + } + } + + return app; +} diff --git a/src/server/index.test.ts b/src/server/index.test.ts index c01e638d0..035754a47 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -3,7 +3,7 @@ import supertest from 'supertest'; import { Client } from '../client/index.js'; import { InMemoryTransport } from '../inMemory.js'; import type { Transport } from '../shared/transport.js'; -import { createMcpExpressApp } from './index.js'; +import { createMcpExpressApp } from './express.js'; import { CreateMessageRequestSchema, CreateMessageResultSchema, diff --git a/src/server/index.ts b/src/server/index.ts index 43aca3b93..aa1a62d00 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,4 @@ -import express, { Express } from 'express'; import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js'; -import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; import { type ClientCapabilities, type CreateMessageRequest, @@ -669,75 +667,3 @@ export class Server< return this.notification({ method: 'notifications/prompts/list_changed' }); } } - -/** - * Options for creating an MCP Express application. - */ -export interface CreateMcpExpressAppOptions { - /** - * The hostname to bind to. Defaults to '127.0.0.1'. - * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. - */ - host?: string; - - /** - * List of allowed hostnames for DNS rebinding protection. - * If provided, host header validation will be applied using this list. - * For IPv6, provide addresses with brackets (e.g., '[::1]'). - * - * This is useful when binding to '0.0.0.0' or '::' but still wanting - * to restrict which hostnames are allowed. - */ - allowedHosts?: string[]; -} - -/** - * Creates an Express application pre-configured for MCP servers. - * - * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), - * DNS rebinding protection middleware is automatically applied to protect against - * DNS rebinding attacks on localhost servers. - * - * @param options - Configuration options - * @returns A configured Express application - * - * @example - * ```typescript - * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection - * const app = createMcpExpressApp(); - * - * // Custom host - DNS rebinding protection only applied for localhost hosts - * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection - * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled - * - * // Custom allowed hosts for non-localhost binding - * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); - * ``` - */ -export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { - const { host = '127.0.0.1', allowedHosts } = options; - - const app = express(); - app.use(express.json()); - - // If allowedHosts is explicitly provided, use that for validation - if (allowedHosts) { - app.use(hostHeaderValidation(allowedHosts)); - } else { - // Apply DNS rebinding protection automatically for localhost hosts - const localhostHosts = ['127.0.0.1', 'localhost', '::1']; - if (localhostHosts.includes(host)) { - app.use(localhostHostValidation()); - } else if (host === '0.0.0.0' || host === '::') { - // Warn when binding to all interfaces without DNS rebinding protection - // eslint-disable-next-line no-console - console.warn( - `Warning: Server is binding to ${host} without DNS rebinding protection. ` + - 'Consider using the allowedHosts option to restrict allowed hosts, ' + - 'or use authentication to protect your server.' - ); - } - } - - return app; -} diff --git a/src/server/sse.ts b/src/server/sse.ts index 270eebc19..b7450a09e 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -17,7 +17,7 @@ export interface SSEServerTransportOptions { * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedHosts?: string[]; @@ -25,7 +25,7 @@ export interface SSEServerTransportOptions { * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedOrigins?: string[]; @@ -33,7 +33,7 @@ export interface SSEServerTransportOptions { * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; } diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 841d6654d..658592c19 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -105,7 +105,7 @@ export interface StreamableHTTPServerTransportOptions { * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedHosts?: string[]; @@ -113,7 +113,7 @@ export interface StreamableHTTPServerTransportOptions { * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedOrigins?: string[]; @@ -121,7 +121,7 @@ export interface StreamableHTTPServerTransportOptions { * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean;