Skip to content

Commit 5b6d35e

Browse files
authored
Merge pull request #363 from modelcontextprotocol/ihrpr/stateless-example-fix
Streamable Http - fix examples for streamable HTTP
2 parents 64653f5 + b6c8c60 commit 5b6d35e

File tree

5 files changed

+270
-23
lines changed

5 files changed

+270
-23
lines changed

Diff for: README.md

+58-21
Original file line numberDiff line numberDiff line change
@@ -309,35 +309,72 @@ app.listen(3000);
309309
For simpler use cases where session management isn't needed:
310310

311311
```typescript
312-
import express from "express";
313-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
314-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
312+
const app = express();
313+
app.use(express.json());
315314

316-
const server = new McpServer({
317-
name: "stateless-server",
318-
version: "1.0.0"
315+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
316+
sessionIdGenerator: undefined, // set to undefined for stateless servers
319317
});
320318

321-
// ... set up server resources, tools, and prompts ...
319+
// Setup routes for the server
320+
const setupServer = async () => {
321+
await server.connect(transport);
322+
};
322323

323-
const app = express();
324-
app.use(express.json());
324+
app.post('/mcp', async (req: Request, res: Response) => {
325+
console.log('Received MCP request:', req.body);
326+
try {
327+
await transport.handleRequest(req, res, req.body);
328+
} catch (error) {
329+
console.error('Error handling MCP request:', error);
330+
if (!res.headersSent) {
331+
res.status(500).json({
332+
jsonrpc: '2.0',
333+
error: {
334+
code: -32603,
335+
message: 'Internal server error',
336+
},
337+
id: null,
338+
});
339+
}
340+
}
341+
});
325342

326-
// Handle all MCP requests (GET, POST, DELETE) at a single endpoint
327-
app.all('/mcp', async (req, res) => {
328-
// Disable session tracking by setting sessionIdGenerator to undefined
329-
const transport = new StreamableHTTPServerTransport({
330-
sessionIdGenerator: undefined,
331-
req,
332-
res
343+
app.get('/mcp', async (req: Request, res: Response) => {
344+
console.log('Received GET MCP request');
345+
res.writeHead(405).end(JSON.stringify({
346+
jsonrpc: "2.0",
347+
error: {
348+
code: -32000,
349+
message: "Method not allowed."
350+
},
351+
id: null
352+
}));
353+
});
354+
355+
app.delete('/mcp', async (req: Request, res: Response) => {
356+
console.log('Received DELETE MCP request');
357+
res.writeHead(405).end(JSON.stringify({
358+
jsonrpc: "2.0",
359+
error: {
360+
code: -32000,
361+
message: "Method not allowed."
362+
},
363+
id: null
364+
}));
365+
});
366+
367+
// Start the server
368+
const PORT = 3000;
369+
setupServer().then(() => {
370+
app.listen(PORT, () => {
371+
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
333372
});
334-
335-
// Connect to server and handle the request
336-
await server.connect(transport);
337-
await transport.handleRequest(req, res);
373+
}).catch(error => {
374+
console.error('Failed to set up the server:', error);
375+
process.exit(1);
338376
});
339377

340-
app.listen(3000);
341378
```
342379

343380
This stateless approach is useful for:

Diff for: package.json

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

Diff for: src/examples/server/simpleStatelessStreamableHttp.ts

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import express, { Request, Response } from 'express';
2+
import { McpServer } from '../../server/mcp.js';
3+
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
4+
import { z } from 'zod';
5+
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
6+
7+
// Create an MCP server with implementation details
8+
const server = new McpServer({
9+
name: 'stateless-streamable-http-server',
10+
version: '1.0.0',
11+
}, { capabilities: { logging: {} } });
12+
13+
// Register a simple prompt
14+
server.prompt(
15+
'greeting-template',
16+
'A simple greeting prompt template',
17+
{
18+
name: z.string().describe('Name to include in greeting'),
19+
},
20+
async ({ name }): Promise<GetPromptResult> => {
21+
return {
22+
messages: [
23+
{
24+
role: 'user',
25+
content: {
26+
type: 'text',
27+
text: `Please greet ${name} in a friendly manner.`,
28+
},
29+
},
30+
],
31+
};
32+
}
33+
);
34+
35+
// Register a tool specifically for testing resumability
36+
server.tool(
37+
'start-notification-stream',
38+
'Starts sending periodic notifications for testing resumability',
39+
{
40+
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
41+
count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
42+
},
43+
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
44+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
45+
let counter = 0;
46+
47+
while (count === 0 || counter < count) {
48+
counter++;
49+
try {
50+
await sendNotification({
51+
method: "notifications/message",
52+
params: {
53+
level: "info",
54+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
55+
}
56+
});
57+
}
58+
catch (error) {
59+
console.error("Error sending notification:", error);
60+
}
61+
// Wait for the specified interval
62+
await sleep(interval);
63+
}
64+
65+
return {
66+
content: [
67+
{
68+
type: 'text',
69+
text: `Started sending periodic notifications every ${interval}ms`,
70+
}
71+
],
72+
};
73+
}
74+
);
75+
76+
// Create a simple resource at a fixed URI
77+
server.resource(
78+
'greeting-resource',
79+
'https://example.com/greetings/default',
80+
{ mimeType: 'text/plain' },
81+
async (): Promise<ReadResourceResult> => {
82+
return {
83+
contents: [
84+
{
85+
uri: 'https://example.com/greetings/default',
86+
text: 'Hello, world!',
87+
},
88+
],
89+
};
90+
}
91+
);
92+
93+
const app = express();
94+
app.use(express.json());
95+
96+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
97+
sessionIdGenerator: undefined,
98+
});
99+
100+
// Setup routes for the server
101+
const setupServer = async () => {
102+
await server.connect(transport);
103+
};
104+
105+
app.post('/mcp', async (req: Request, res: Response) => {
106+
console.log('Received MCP request:', req.body);
107+
try {
108+
await transport.handleRequest(req, res, req.body);
109+
} catch (error) {
110+
console.error('Error handling MCP request:', error);
111+
if (!res.headersSent) {
112+
res.status(500).json({
113+
jsonrpc: '2.0',
114+
error: {
115+
code: -32603,
116+
message: 'Internal server error',
117+
},
118+
id: null,
119+
});
120+
}
121+
}
122+
});
123+
124+
app.get('/mcp', async (req: Request, res: Response) => {
125+
console.log('Received GET MCP request');
126+
res.writeHead(405).end(JSON.stringify({
127+
jsonrpc: "2.0",
128+
error: {
129+
code: -32000,
130+
message: "Method not allowed."
131+
},
132+
id: null
133+
}));
134+
});
135+
136+
app.delete('/mcp', async (req: Request, res: Response) => {
137+
console.log('Received DELETE MCP request');
138+
res.writeHead(405).end(JSON.stringify({
139+
jsonrpc: "2.0",
140+
error: {
141+
code: -32000,
142+
message: "Method not allowed."
143+
},
144+
id: null
145+
}));
146+
});
147+
148+
// Start the server
149+
const PORT = 3000;
150+
setupServer().then(() => {
151+
app.listen(PORT, () => {
152+
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
153+
});
154+
}).catch(error => {
155+
console.error('Failed to set up the server:', error);
156+
process.exit(1);
157+
});
158+
159+
// Handle server shutdown
160+
process.on('SIGINT', async () => {
161+
console.log('Shutting down server...');
162+
try {
163+
console.log(`Closing transport`);
164+
await transport.close();
165+
} catch (error) {
166+
console.error(`Error closing transport:`, error);
167+
}
168+
169+
await server.close();
170+
console.log('Server shutdown complete');
171+
process.exit(0);
172+
});

Diff for: src/integration-tests/stateManagementStreamableHttp.test.ts

+38
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,44 @@ describe('Streamable HTTP Transport Session Management', () => {
109109
server.close();
110110
});
111111

112+
it('should support multiple client connections', async () => {
113+
// Create and connect a client
114+
const client1 = new Client({
115+
name: 'test-client',
116+
version: '1.0.0'
117+
});
118+
119+
const transport1 = new StreamableHTTPClientTransport(baseUrl);
120+
await client1.connect(transport1);
121+
122+
// Verify that no session ID was set
123+
expect(transport1.sessionId).toBeUndefined();
124+
125+
// List available tools
126+
await client1.request({
127+
method: 'tools/list',
128+
params: {}
129+
}, ListToolsResultSchema);
130+
131+
const client2 = new Client({
132+
name: 'test-client',
133+
version: '1.0.0'
134+
});
135+
136+
const transport2 = new StreamableHTTPClientTransport(baseUrl);
137+
await client2.connect(transport2);
138+
139+
// Verify that no session ID was set
140+
expect(transport2.sessionId).toBeUndefined();
141+
142+
// List available tools
143+
await client2.request({
144+
method: 'tools/list',
145+
params: {}
146+
}, ListToolsResultSchema);
147+
148+
149+
});
112150
it('should operate without session management', async () => {
113151
// Create and connect a client
114152
const client = new Client({

Diff for: src/server/streamableHttp.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export class StreamableHTTPServerTransport implements Transport {
343343
if (isInitializationRequest) {
344344
// If it's a server with session management and the session ID is already set we should reject the request
345345
// to avoid re-initialization.
346-
if (this._initialized) {
346+
if (this._initialized && this.sessionId !== undefined) {
347347
res.writeHead(400).end(JSON.stringify({
348348
jsonrpc: "2.0",
349349
error: {

0 commit comments

Comments
 (0)