Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ For a minimal “getting started” experience:

For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS rebind protection), see the examples above and the MCP spec sections on transports.

## DNS rebinding protection

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

// Protection auto-enabled (default host is 127.0.0.1)
const app = createMcpExpressApp();

// Protection auto-enabled for localhost
const app = createMcpExpressApp({ host: 'localhost' });

// No auto protection when binding to all interfaces
const app = createMcpExpressApp({ host: '0.0.0.0' });
```

For custom host validation, use the middleware directly:

```typescript
import express from 'express';
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';

const app = express();
app.use(express.json());
app.use(hostHeaderValidation(['localhost', '127.0.0.1', 'myhost.local']));
```

## Tools, resources, and prompts

### Tools
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/sdk",
"version": "1.23.0",
"version": "1.24.0",
"description": "Model Context Protocol implementation for TypeScript",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
Expand Down
62 changes: 62 additions & 0 deletions src/client/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,68 @@ describe('StreamableHTTPClientTransport', () => {
});
});

describe('Reconnection Logic with maxRetries 0', () => {
let transport: StreamableHTTPClientTransport;

// Use fake timers to control setTimeout and make the test instant.
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it('should not schedule any reconnection attempts when maxRetries is 0', async () => {
// ARRANGE
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
reconnectionOptions: {
initialReconnectionDelay: 10,
maxRetries: 0, // This should disable retries completely
maxReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1
}
});

const errorSpy = vi.fn();
transport.onerror = errorSpy;

// ACT - directly call _scheduleReconnection which is the code path the fix affects
transport['_scheduleReconnection']({});

// ASSERT - should immediately report max retries exceeded, not schedule a retry
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Maximum reconnection attempts (0) exceeded.'
})
);

// Verify no timeout was scheduled (no reconnection attempt)
expect(transport['_reconnectionTimeout']).toBeUndefined();
});

it('should schedule reconnection when maxRetries is greater than 0', async () => {
// ARRANGE
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
reconnectionOptions: {
initialReconnectionDelay: 10,
maxRetries: 1, // Allow 1 retry
maxReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1
}
});

const errorSpy = vi.fn();
transport.onerror = errorSpy;

// ACT - call _scheduleReconnection with attemptCount 0
transport['_scheduleReconnection']({});

// ASSERT - should schedule a reconnection, not report error yet
expect(errorSpy).not.toHaveBeenCalled();
expect(transport['_reconnectionTimeout']).toBeDefined();

// Clean up the timeout to avoid test pollution
clearTimeout(transport['_reconnectionTimeout']);
});
});

describe('prevent infinite recursion when server returns 401 after successful auth', () => {
it('should throw error when server returns 401 after successful auth', async () => {
const message: JSONRPCMessage = {
Expand Down
2 changes: 1 addition & 1 deletion src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export class StreamableHTTPClientTransport implements Transport {
const maxRetries = this._reconnectionOptions.maxRetries;

// Check if we've exceeded maximum retry attempts
if (maxRetries > 0 && attemptCount >= maxRetries) {
if (attemptCount >= maxRetries) {
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
return;
}
Expand Down
15 changes: 3 additions & 12 deletions src/examples/server/elicitationFormExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
// to collect *sensitive* user input via a browser.

import { randomUUID } from 'node:crypto';
import cors from 'cors';
import express, { type Request, type Response } from 'express';
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';

// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
// The validator supports format validation (email, date, etc.) if ajv-formats is installed
Expand Down Expand Up @@ -320,16 +320,7 @@ mcpServer.registerTool(
async function main() {
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;

const app = express();
app.use(express.json());

// Allow CORS for all domains, expose the Mcp-Session-Id header
app.use(
cors({
origin: '*',
exposedHeaders: ['Mcp-Session-Id']
})
);
const app = createMcpExpressApp();

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
Expand Down
4 changes: 2 additions & 2 deletions src/examples/server/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +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 { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
Expand Down Expand Up @@ -214,8 +215,7 @@ function completeURLElicitation(elicitationId: string) {
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;

const app = express();
app.use(express.json());
const app = createMcpExpressApp();

// Allow CORS all domains, expose the Mcp-Session-Id header
app.use(
Expand Down
15 changes: 3 additions & 12 deletions src/examples/server/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
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 cors from 'cors';
import { createMcpExpressApp } from '../../server/index.js';

// Create an MCP server with implementation details
const getServer = () => {
Expand Down Expand Up @@ -90,16 +90,7 @@ const getServer = () => {
return server;
};

const app = express();
app.use(express.json());

// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
cors({
origin: '*', // Allow all origins - adjust as needed for production
exposedHeaders: ['Mcp-Session-Id']
})
);
const app = createMcpExpressApp();

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
Expand Down
6 changes: 3 additions & 3 deletions src/examples/server/simpleSseServer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
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';

/**
* This example server demonstrates the deprecated HTTP+SSE transport
Expand Down Expand Up @@ -75,8 +76,7 @@ const getServer = () => {
return server;
};

const app = express();
app.use(express.json());
const app = createMcpExpressApp();

// Store transports by session ID
const transports: Record<string, SSEServerTransport> = {};
Expand Down
15 changes: 3 additions & 12 deletions src/examples/server/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
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 cors from 'cors';
import { createMcpExpressApp } from '../../server/index.js';

const getServer = () => {
// Create an MCP server with implementation details
Expand Down Expand Up @@ -96,16 +96,7 @@ const getServer = () => {
return server;
};

const app = express();
app.use(express.json());

// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
cors({
origin: '*', // Allow all origins - adjust as needed for production
exposedHeaders: ['Mcp-Session-Id']
})
);
const app = createMcpExpressApp();

app.post('/mcp', async (req: Request, res: Response) => {
const server = getServer();
Expand Down
16 changes: 3 additions & 13 deletions src/examples/server/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import * as z from 'zod/v4';
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 {
CallToolResult,
ElicitResultSchema,
Expand All @@ -20,8 +21,6 @@ import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
import { OAuthMetadata } from '../../shared/auth.js';
import { checkResourceAllowed } from '../../shared/auth-utils.js';

import cors from 'cors';

// Check for OAuth flag
const useOAuth = process.argv.includes('--oauth');
const strictOAuth = process.argv.includes('--oauth-strict');
Expand Down Expand Up @@ -507,16 +506,7 @@ const getServer = () => {
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;

const app = express();
app.use(express.json());

// Allow CORS all domains, expose the Mcp-Session-Id header
app.use(
cors({
origin: '*', // Allow all origins
exposedHeaders: ['Mcp-Session-Id']
})
);
const app = createMcpExpressApp();

// Set up OAuth if enabled
let authMiddleware = null;
Expand Down
7 changes: 3 additions & 4 deletions src/examples/server/simpleTaskInteractive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* creates a task, and the result is fetched via tasks/result endpoint.
*/

import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { Server } from '../../server/index.js';
import { createMcpExpressApp, Server } from '../../server/index.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import {
CallToolResult,
Expand Down Expand Up @@ -630,8 +630,7 @@ const createServer = (): Server => {
// Express App Setup
// ============================================================================

const app = express();
app.use(express.json());
const app = createMcpExpressApp();

// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
Expand Down
15 changes: 3 additions & 12 deletions src/examples/server/sseAndStreamableHttpCompatibleServer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
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 cors from 'cors';
import { createMcpExpressApp } from '../../server/index.js';

/**
* This example server demonstrates backwards compatibility with both:
Expand Down Expand Up @@ -71,16 +71,7 @@ const getServer = () => {
};

// Create Express application
const app = express();
app.use(express.json());

// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
cors({
origin: '*', // Allow all origins - adjust as needed for production
exposedHeaders: ['Mcp-Session-Id']
})
);
const app = createMcpExpressApp();

// Store transports by session ID
const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {};
Expand Down
9 changes: 5 additions & 4 deletions src/examples/server/ssePollingExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
* Run with: npx tsx src/examples/server/ssePollingExample.ts
* Test with: curl or the MCP Inspector
*/
import express, { Request, Response } from 'express';
import { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '../../server/mcp.js';
import { createMcpExpressApp } from '../../server/index.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { CallToolResult } from '../../types.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
Expand Down Expand Up @@ -103,7 +104,7 @@ server.tool(
);

// Set up Express app
const app = express();
const app = createMcpExpressApp();
app.use(cors());

// Create event store for resumability
Expand All @@ -112,8 +113,8 @@ const eventStore = new InMemoryEventStore();
// Track transports by session ID for session reuse
const transports = new Map<string, StreamableHTTPServerTransport>();

// Handle all MCP requests - use express.json() only for this route
app.all('/mcp', express.json(), async (req: Request, res: Response) => {
// Handle all MCP requests
app.all('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

// Reuse existing transport or create new one
Expand Down
Loading
Loading