Skip to content

Commit 0bf8b37

Browse files
committed
examples for stateless servers
1 parent 64653f5 commit 0bf8b37

File tree

4 files changed

+243
-23
lines changed

4 files changed

+243
-23
lines changed

README.md

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -309,35 +309,54 @@ 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,
319317
});
318+
await server.connect(transport);
320319

321-
// ... set up server resources, tools, and prompts ...
320+
app.post('/mcp', async (req: Request, res: Response) => {
321+
console.log('Received MCP request:', req.body);
322+
try {
323+
await transport.handleRequest(req, res, req.body);
324+
} catch (error) {
325+
// ... handle error
326+
}
327+
}
328+
});
322329

323-
const app = express();
324-
app.use(express.json());
330+
app.get('/mcp', async (req: Request, res: Response) => {
331+
console.log('Received GET MCP request');
332+
res.writeHead(405).end(JSON.stringify({
333+
jsonrpc: "2.0",
334+
error: {
335+
code: -32000,
336+
message: "Method not allowed."
337+
},
338+
id: null
339+
}));
340+
});
325341

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
333-
});
334-
335-
// Connect to server and handle the request
336-
await server.connect(transport);
337-
await transport.handleRequest(req, res);
342+
app.delete('/mcp', async (req: Request, res: Response) => {
343+
console.log('Received DELETE MCP request');
344+
res.writeHead(405).end(JSON.stringify({
345+
jsonrpc: "2.0",
346+
error: {
347+
code: -32000,
348+
message: "Method not allowed."
349+
},
350+
id: null
351+
}));
352+
});
353+
354+
// Start the server
355+
const PORT = 3000;
356+
app.listen(PORT, () => {
357+
console.log(`Stateless MCP Streamable HTTP Server listening on port ${PORT}`);
338358
});
339359

340-
app.listen(3000);
341360
```
342361

343362
This stateless approach is useful for:
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
await server.connect(transport);
100+
101+
app.post('/mcp', async (req: Request, res: Response) => {
102+
console.log('Received MCP request:', req.body);
103+
try {
104+
await transport.handleRequest(req, res, req.body);
105+
} catch (error) {
106+
console.error('Error handling MCP request:', error);
107+
if (!res.headersSent) {
108+
res.status(500).json({
109+
jsonrpc: '2.0',
110+
error: {
111+
code: -32603,
112+
message: 'Internal server error',
113+
},
114+
id: null,
115+
});
116+
}
117+
}
118+
});
119+
120+
app.get('/mcp', async (req: Request, res: Response) => {
121+
console.log('Received GET MCP request');
122+
res.writeHead(405).end(JSON.stringify({
123+
jsonrpc: "2.0",
124+
error: {
125+
code: -32000,
126+
message: "Method not allowed."
127+
},
128+
id: null
129+
}));
130+
});
131+
132+
app.delete('/mcp', async (req: Request, res: Response) => {
133+
console.log('Received DELETE MCP request');
134+
res.writeHead(405).end(JSON.stringify({
135+
jsonrpc: "2.0",
136+
error: {
137+
code: -32000,
138+
message: "Method not allowed."
139+
},
140+
id: null
141+
}));
142+
});
143+
144+
// Start the server
145+
const PORT = 3000;
146+
app.listen(PORT, () => {
147+
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
148+
});
149+
150+
// Handle server shutdown
151+
process.on('SIGINT', async () => {
152+
console.log('Shutting down server...');
153+
try {
154+
console.log(`Closing transport`);
155+
await transport.close();
156+
} catch (error) {
157+
console.error(`Error closing transport:`, error);
158+
}
159+
160+
await server.close();
161+
console.log('Server shutdown complete');
162+
process.exit(0);
163+
});

src/integration-tests/stateManagementStreamableHttp.test.ts

Lines changed: 38 additions & 0 deletions
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 client1.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({

src/server/streamableHttp.ts

Lines changed: 1 addition & 1 deletion
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)