Skip to content

Commit 73f464f

Browse files
authored
Merge pull request #377 from modelcontextprotocol/ihrpr/example-clients
Streamable Http - fix server initialization validation in Stateless servers
2 parents ac939a5 + ded0ce3 commit 73f464f

9 files changed

+806
-445
lines changed

Diff for: README.md

+18-18
Original file line numberDiff line numberDiff line change
@@ -310,19 +310,23 @@ For simpler use cases where session management isn't needed:
310310
const app = express();
311311
app.use(express.json());
312312

313-
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
314-
sessionIdGenerator: undefined, // set to undefined for stateless servers
315-
});
316-
317-
// Setup routes for the server
318-
const setupServer = async () => {
319-
await server.connect(transport);
320-
};
321-
322313
app.post('/mcp', async (req: Request, res: Response) => {
323-
console.log('Received MCP request:', req.body);
314+
// In stateless mode, create a new instance of transport and server for each request
315+
// to ensure complete isolation. A single instance would cause request ID collisions
316+
// when multiple clients connect concurrently.
317+
324318
try {
325-
await transport.handleRequest(req, res, req.body);
319+
const server = getServer();
320+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
321+
sessionIdGenerator: undefined,
322+
});
323+
await server.connect(transport);
324+
await transport.handleRequest(req, res, req.body);
325+
res.on('close', () => {
326+
console.log('Request closed');
327+
transport.close();
328+
server.close();
329+
});
326330
} catch (error) {
327331
console.error('Error handling MCP request:', error);
328332
if (!res.headersSent) {
@@ -362,15 +366,11 @@ app.delete('/mcp', async (req: Request, res: Response) => {
362366
}));
363367
});
364368

369+
365370
// Start the server
366371
const PORT = 3000;
367-
setupServer().then(() => {
368-
app.listen(PORT, () => {
369-
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
370-
});
371-
}).catch(error => {
372-
console.error('Failed to set up the server:', error);
373-
process.exit(1);
372+
app.listen(PORT, () => {
373+
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
374374
});
375375

376376
```

Diff for: src/examples/client/multipleClientsParallel.ts

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Client } from '../../client/index.js';
2+
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
3+
import {
4+
CallToolRequest,
5+
CallToolResultSchema,
6+
LoggingMessageNotificationSchema,
7+
CallToolResult,
8+
} from '../../types.js';
9+
10+
/**
11+
* Multiple Clients MCP Example
12+
*
13+
* This client demonstrates how to:
14+
* 1. Create multiple MCP clients in parallel
15+
* 2. Each client calls a single tool
16+
* 3. Track notifications from each client independently
17+
*/
18+
19+
// Command line args processing
20+
const args = process.argv.slice(2);
21+
const serverUrl = args[0] || 'http://localhost:3000/mcp';
22+
23+
interface ClientConfig {
24+
id: string;
25+
name: string;
26+
toolName: string;
27+
toolArguments: Record<string, string | number | boolean>;
28+
}
29+
30+
async function createAndRunClient(config: ClientConfig): Promise<{ id: string; result: CallToolResult }> {
31+
console.log(`[${config.id}] Creating client: ${config.name}`);
32+
33+
const client = new Client({
34+
name: config.name,
35+
version: '1.0.0'
36+
});
37+
38+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
39+
40+
// Set up client-specific error handler
41+
client.onerror = (error) => {
42+
console.error(`[${config.id}] Client error:`, error);
43+
};
44+
45+
// Set up client-specific notification handler
46+
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
47+
console.log(`[${config.id}] Notification: ${notification.params.data}`);
48+
});
49+
50+
try {
51+
// Connect to the server
52+
await client.connect(transport);
53+
console.log(`[${config.id}] Connected to MCP server`);
54+
55+
// Call the specified tool
56+
console.log(`[${config.id}] Calling tool: ${config.toolName}`);
57+
const toolRequest: CallToolRequest = {
58+
method: 'tools/call',
59+
params: {
60+
name: config.toolName,
61+
arguments: {
62+
...config.toolArguments,
63+
// Add client ID to arguments for identification in notifications
64+
caller: config.id
65+
}
66+
}
67+
};
68+
69+
const result = await client.request(toolRequest, CallToolResultSchema);
70+
console.log(`[${config.id}] Tool call completed`);
71+
72+
// Keep the connection open for a bit to receive notifications
73+
await new Promise(resolve => setTimeout(resolve, 5000));
74+
75+
// Disconnect
76+
await transport.close();
77+
console.log(`[${config.id}] Disconnected from MCP server`);
78+
79+
return { id: config.id, result };
80+
} catch (error) {
81+
console.error(`[${config.id}] Error:`, error);
82+
throw error;
83+
}
84+
}
85+
86+
async function main(): Promise<void> {
87+
console.log('MCP Multiple Clients Example');
88+
console.log('============================');
89+
console.log(`Server URL: ${serverUrl}`);
90+
console.log('');
91+
92+
try {
93+
// Define client configurations
94+
const clientConfigs: ClientConfig[] = [
95+
{
96+
id: 'client1',
97+
name: 'basic-client-1',
98+
toolName: 'start-notification-stream',
99+
toolArguments: {
100+
interval: 3, // 1 second between notifications
101+
count: 5 // Send 5 notifications
102+
}
103+
},
104+
{
105+
id: 'client2',
106+
name: 'basic-client-2',
107+
toolName: 'start-notification-stream',
108+
toolArguments: {
109+
interval: 2, // 2 seconds between notifications
110+
count: 3 // Send 3 notifications
111+
}
112+
},
113+
{
114+
id: 'client3',
115+
name: 'basic-client-3',
116+
toolName: 'start-notification-stream',
117+
toolArguments: {
118+
interval: 1, // 0.5 second between notifications
119+
count: 8 // Send 8 notifications
120+
}
121+
}
122+
];
123+
124+
// Start all clients in parallel
125+
console.log(`Starting ${clientConfigs.length} clients in parallel...`);
126+
console.log('');
127+
128+
const clientPromises = clientConfigs.map(config => createAndRunClient(config));
129+
const results = await Promise.all(clientPromises);
130+
131+
// Display results from all clients
132+
console.log('\n=== Final Results ===');
133+
results.forEach(({ id, result }) => {
134+
console.log(`\n[${id}] Tool result:`);
135+
if (Array.isArray(result.content)) {
136+
result.content.forEach((item: { type: string; text?: string }) => {
137+
if (item.type === 'text' && item.text) {
138+
console.log(` ${item.text}`);
139+
} else {
140+
console.log(` ${item.type} content:`, item);
141+
}
142+
});
143+
} else {
144+
console.log(` Unexpected result format:`, result);
145+
}
146+
});
147+
148+
console.log('\n=== All clients completed successfully ===');
149+
150+
} catch (error) {
151+
console.error('Error running multiple clients:', error);
152+
process.exit(1);
153+
}
154+
}
155+
156+
// Start the example
157+
main().catch((error: unknown) => {
158+
console.error('Error running MCP multiple clients example:', error);
159+
process.exit(1);
160+
});

0 commit comments

Comments
 (0)