Skip to content

Commit 26ebf24

Browse files
committed
Merge branch 'main' of https://github.com/modelcontextprotocol/typescript-sdk into feat/elicitation-sampling-streaming
2 parents d41bcf8 + bfad917 commit 26ebf24

28 files changed

+930
-107
lines changed

docs/server.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,34 @@ For a minimal “getting started” experience:
6666

6767
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.
6868

69+
## DNS rebinding protection
70+
71+
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:
72+
73+
```typescript
74+
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/index.js';
75+
76+
// Protection auto-enabled (default host is 127.0.0.1)
77+
const app = createMcpExpressApp();
78+
79+
// Protection auto-enabled for localhost
80+
const app = createMcpExpressApp({ host: 'localhost' });
81+
82+
// No auto protection when binding to all interfaces
83+
const app = createMcpExpressApp({ host: '0.0.0.0' });
84+
```
85+
86+
For custom host validation, use the middleware directly:
87+
88+
```typescript
89+
import express from 'express';
90+
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
91+
92+
const app = express();
93+
app.use(express.json());
94+
app.use(hostHeaderValidation(['localhost', '127.0.0.1', 'myhost.local']));
95+
```
96+
6997
## Tools, resources, and prompts
7098

7199
### Tools

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.23.0",
3+
"version": "1.24.0",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

src/client/streamableHttp.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,68 @@ describe('StreamableHTTPClientTransport', () => {
15011501
});
15021502
});
15031503

1504+
describe('Reconnection Logic with maxRetries 0', () => {
1505+
let transport: StreamableHTTPClientTransport;
1506+
1507+
// Use fake timers to control setTimeout and make the test instant.
1508+
beforeEach(() => vi.useFakeTimers());
1509+
afterEach(() => vi.useRealTimers());
1510+
1511+
it('should not schedule any reconnection attempts when maxRetries is 0', async () => {
1512+
// ARRANGE
1513+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
1514+
reconnectionOptions: {
1515+
initialReconnectionDelay: 10,
1516+
maxRetries: 0, // This should disable retries completely
1517+
maxReconnectionDelay: 1000,
1518+
reconnectionDelayGrowFactor: 1
1519+
}
1520+
});
1521+
1522+
const errorSpy = vi.fn();
1523+
transport.onerror = errorSpy;
1524+
1525+
// ACT - directly call _scheduleReconnection which is the code path the fix affects
1526+
transport['_scheduleReconnection']({});
1527+
1528+
// ASSERT - should immediately report max retries exceeded, not schedule a retry
1529+
expect(errorSpy).toHaveBeenCalledTimes(1);
1530+
expect(errorSpy).toHaveBeenCalledWith(
1531+
expect.objectContaining({
1532+
message: 'Maximum reconnection attempts (0) exceeded.'
1533+
})
1534+
);
1535+
1536+
// Verify no timeout was scheduled (no reconnection attempt)
1537+
expect(transport['_reconnectionTimeout']).toBeUndefined();
1538+
});
1539+
1540+
it('should schedule reconnection when maxRetries is greater than 0', async () => {
1541+
// ARRANGE
1542+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
1543+
reconnectionOptions: {
1544+
initialReconnectionDelay: 10,
1545+
maxRetries: 1, // Allow 1 retry
1546+
maxReconnectionDelay: 1000,
1547+
reconnectionDelayGrowFactor: 1
1548+
}
1549+
});
1550+
1551+
const errorSpy = vi.fn();
1552+
transport.onerror = errorSpy;
1553+
1554+
// ACT - call _scheduleReconnection with attemptCount 0
1555+
transport['_scheduleReconnection']({});
1556+
1557+
// ASSERT - should schedule a reconnection, not report error yet
1558+
expect(errorSpy).not.toHaveBeenCalled();
1559+
expect(transport['_reconnectionTimeout']).toBeDefined();
1560+
1561+
// Clean up the timeout to avoid test pollution
1562+
clearTimeout(transport['_reconnectionTimeout']);
1563+
});
1564+
});
1565+
15041566
describe('prevent infinite recursion when server returns 401 after successful auth', () => {
15051567
it('should throw error when server returns 401 after successful auth', async () => {
15061568
const message: JSONRPCMessage = {

src/client/streamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export class StreamableHTTPClientTransport implements Transport {
279279
const maxRetries = this._reconnectionOptions.maxRetries;
280280

281281
// Check if we've exceeded maximum retry attempts
282-
if (maxRetries > 0 && attemptCount >= maxRetries) {
282+
if (attemptCount >= maxRetries) {
283283
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
284284
return;
285285
}

src/examples/server/elicitationFormExample.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
// to collect *sensitive* user input via a browser.
99

1010
import { randomUUID } from 'node:crypto';
11-
import cors from 'cors';
12-
import express, { type Request, type Response } from 'express';
11+
import { type Request, type Response } from 'express';
1312
import { McpServer } from '../../server/mcp.js';
1413
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
1514
import { isInitializeRequest } from '../../types.js';
15+
import { createMcpExpressApp } from '../../server/index.js';
1616

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

323-
const app = express();
324-
app.use(express.json());
325-
326-
// Allow CORS for all domains, expose the Mcp-Session-Id header
327-
app.use(
328-
cors({
329-
origin: '*',
330-
exposedHeaders: ['Mcp-Session-Id']
331-
})
332-
);
323+
const app = createMcpExpressApp();
333324

334325
// Map to store transports by session ID
335326
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

src/examples/server/elicitationUrlExample.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import express, { Request, Response } from 'express';
1111
import { randomUUID } from 'node:crypto';
1212
import { z } from 'zod';
1313
import { McpServer } from '../../server/mcp.js';
14+
import { createMcpExpressApp } from '../../server/index.js';
1415
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
1516
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
1617
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
@@ -214,8 +215,7 @@ function completeURLElicitation(elicitationId: string) {
214215
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
215216
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
216217

217-
const app = express();
218-
app.use(express.json());
218+
const app = createMcpExpressApp();
219219

220220
// Allow CORS all domains, expose the Mcp-Session-Id header
221221
app.use(

src/examples/server/jsonResponseStreamableHttp.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import express, { Request, Response } from 'express';
1+
import { Request, Response } from 'express';
22
import { randomUUID } from 'node:crypto';
33
import { McpServer } from '../../server/mcp.js';
44
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
55
import * as z from 'zod/v4';
66
import { CallToolResult, isInitializeRequest } from '../../types.js';
7-
import cors from 'cors';
7+
import { createMcpExpressApp } from '../../server/index.js';
88

99
// Create an MCP server with implementation details
1010
const getServer = () => {
@@ -90,16 +90,7 @@ const getServer = () => {
9090
return server;
9191
};
9292

93-
const app = express();
94-
app.use(express.json());
95-
96-
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
97-
app.use(
98-
cors({
99-
origin: '*', // Allow all origins - adjust as needed for production
100-
exposedHeaders: ['Mcp-Session-Id']
101-
})
102-
);
93+
const app = createMcpExpressApp();
10394

10495
// Map to store transports by session ID
10596
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

src/examples/server/simpleSseServer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import express, { Request, Response } from 'express';
1+
import { Request, Response } from 'express';
22
import { McpServer } from '../../server/mcp.js';
33
import { SSEServerTransport } from '../../server/sse.js';
44
import * as z from 'zod/v4';
55
import { CallToolResult } from '../../types.js';
6+
import { createMcpExpressApp } from '../../server/index.js';
67

78
/**
89
* This example server demonstrates the deprecated HTTP+SSE transport
@@ -75,8 +76,7 @@ const getServer = () => {
7576
return server;
7677
};
7778

78-
const app = express();
79-
app.use(express.json());
79+
const app = createMcpExpressApp();
8080

8181
// Store transports by session ID
8282
const transports: Record<string, SSEServerTransport> = {};

src/examples/server/simpleStatelessStreamableHttp.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import express, { Request, Response } from 'express';
1+
import { Request, Response } from 'express';
22
import { McpServer } from '../../server/mcp.js';
33
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
44
import * as z from 'zod/v4';
55
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
6-
import cors from 'cors';
6+
import { createMcpExpressApp } from '../../server/index.js';
77

88
const getServer = () => {
99
// Create an MCP server with implementation details
@@ -96,16 +96,7 @@ const getServer = () => {
9696
return server;
9797
};
9898

99-
const app = express();
100-
app.use(express.json());
101-
102-
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
103-
app.use(
104-
cors({
105-
origin: '*', // Allow all origins - adjust as needed for production
106-
exposedHeaders: ['Mcp-Session-Id']
107-
})
108-
);
99+
const app = createMcpExpressApp();
109100

110101
app.post('/mcp', async (req: Request, res: Response) => {
111102
const server = getServer();

0 commit comments

Comments
 (0)