refactor: make Server class framework-agnostic by moving express to separate module (#1223)

This commit is contained in:
Wuma 2025-12-03 19:58:42 +08:00 • committed by GitHub
commit 7c9257cdf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 93 additions and 92 deletions

74
src/server/express.ts Normal file
View file

@ -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;
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;