From 0bf8b376dfd410e778299d6853b8c74ae428dde5 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Fri, 18 Apr 2025 11:46:17 +0100 Subject: [PATCH 1/4] examples for stateless servers --- README.md | 63 ++++--- .../server/simpleStatelessStreamableHttp.ts | 163 ++++++++++++++++++ .../stateManagementStreamableHttp.test.ts | 38 ++++ src/server/streamableHttp.ts | 2 +- 4 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 src/examples/server/simpleStatelessStreamableHttp.ts diff --git a/README.md b/README.md index ed97b83a..929abd84 100644 --- a/README.md +++ b/README.md @@ -309,35 +309,54 @@ app.listen(3000); For simpler use cases where session management isn't needed: ```typescript -import express from "express"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +const app = express(); +app.use(express.json()); -const server = new McpServer({ - name: "stateless-server", - version: "1.0.0" +const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, }); +await server.connect(transport); -// ... set up server resources, tools, and prompts ... +app.post('/mcp', async (req: Request, res: Response) => { + console.log('Received MCP request:', req.body); + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + // ... handle error + } + } +}); -const app = express(); -app.use(express.json()); +app.get('/mcp', async (req: Request, res: Response) => { + console.log('Received GET MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed." + }, + id: null + })); +}); -// Handle all MCP requests (GET, POST, DELETE) at a single endpoint -app.all('/mcp', async (req, res) => { - // Disable session tracking by setting sessionIdGenerator to undefined - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - req, - res - }); - - // Connect to server and handle the request - await server.connect(transport); - await transport.handleRequest(req, res); +app.delete('/mcp', async (req: Request, res: Response) => { + console.log('Received DELETE MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed." + }, + id: null + })); +}); + +// Start the server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Stateless MCP Streamable HTTP Server listening on port ${PORT}`); }); -app.listen(3000); ``` This stateless approach is useful for: diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts new file mode 100644 index 00000000..89433ecb --- /dev/null +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -0,0 +1,163 @@ +import express, { Request, Response } from 'express'; +import { McpServer } from '../../server/mcp.js'; +import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; +import { z } from 'zod'; +import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; + +// Create an MCP server with implementation details +const server = new McpServer({ + name: 'stateless-streamable-http-server', + version: '1.0.0', +}, { capabilities: { logging: {} } }); + +// Register a simple prompt +server.prompt( + 'greeting-template', + 'A simple greeting prompt template', + { + name: z.string().describe('Name to include in greeting'), + }, + async ({ name }): Promise => { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please greet ${name} in a friendly manner.`, + }, + }, + ], + }; + } +); + +// Register a tool specifically for testing resumability +server.tool( + 'start-notification-stream', + 'Starts sending periodic notifications for testing resumability', + { + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(10), + }, + async ({ interval, count }, { sendNotification }): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + let counter = 0; + + while (count === 0 || counter < count) { + counter++; + try { + await sendNotification({ + method: "notifications/message", + params: { + level: "info", + data: `Periodic notification #${counter} at ${new Date().toISOString()}` + } + }); + } + catch (error) { + console.error("Error sending notification:", error); + } + // Wait for the specified interval + await sleep(interval); + } + + return { + content: [ + { + type: 'text', + text: `Started sending periodic notifications every ${interval}ms`, + } + ], + }; + } +); + +// Create a simple resource at a fixed URI +server.resource( + 'greeting-resource', + 'https://example.com/greetings/default', + { mimeType: 'text/plain' }, + async (): Promise => { + return { + contents: [ + { + uri: 'https://example.com/greetings/default', + text: 'Hello, world!', + }, + ], + }; + } +); + +const app = express(); +app.use(express.json()); + +const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, +}); +await server.connect(transport); + +app.post('/mcp', async (req: Request, res: Response) => { + console.log('Received MCP request:', req.body); + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); + +app.get('/mcp', async (req: Request, res: Response) => { + console.log('Received GET MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed." + }, + id: null + })); +}); + +app.delete('/mcp', async (req: Request, res: Response) => { + console.log('Received DELETE MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Method not allowed." + }, + id: null + })); +}); + +// Start the server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); +}); + +// Handle server shutdown +process.on('SIGINT', async () => { + console.log('Shutting down server...'); + try { + console.log(`Closing transport`); + await transport.close(); + } catch (error) { + console.error(`Error closing transport:`, error); + } + + await server.close(); + console.log('Server shutdown complete'); + process.exit(0); +}); \ No newline at end of file diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index 6d553727..88a06d38 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -109,6 +109,44 @@ describe('Streamable HTTP Transport Session Management', () => { server.close(); }); + it('should support multiple client connections', async () => { + // Create and connect a client + const client1 = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + const transport1 = new StreamableHTTPClientTransport(baseUrl); + await client1.connect(transport1); + + // Verify that no session ID was set + expect(transport1.sessionId).toBeUndefined(); + + // List available tools + await client1.request({ + method: 'tools/list', + params: {} + }, ListToolsResultSchema); + + const client2 = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + const transport2 = new StreamableHTTPClientTransport(baseUrl); + await client2.connect(transport2); + + // Verify that no session ID was set + expect(transport2.sessionId).toBeUndefined(); + + // List available tools + await client1.request({ + method: 'tools/list', + params: {} + }, ListToolsResultSchema); + + + }); it('should operate without session management', async () => { // Create and connect a client const client = new Client({ diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index d40ec930..85a8b9f1 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -343,7 +343,7 @@ export class StreamableHTTPServerTransport implements Transport { if (isInitializationRequest) { // If it's a server with session management and the session ID is already set we should reject the request // to avoid re-initialization. - if (this._initialized) { + if (this._initialized && this.sessionId !== undefined) { res.writeHead(400).end(JSON.stringify({ jsonrpc: "2.0", error: { From ea04be4cf84a6415e73c9fec34c4aeb00e8848dd Mon Sep 17 00:00:00 2001 From: ihrpr Date: Fri, 18 Apr 2025 11:52:51 +0100 Subject: [PATCH 2/4] fix build --- README.md | 28 +++++++++++++++---- .../server/simpleStatelessStreamableHttp.ts | 15 ++++++++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 929abd84..200cfab6 100644 --- a/README.md +++ b/README.md @@ -313,16 +313,29 @@ const app = express(); app.use(express.json()); const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, + sessionIdGenerator: undefined, // set to undefined for stateless servers }); -await server.connect(transport); + +// Setup routes for the server +const setupServer = async () => { + await server.connect(transport); +}; app.post('/mcp', async (req: Request, res: Response) => { console.log('Received MCP request:', req.body); try { await transport.handleRequest(req, res, req.body); } catch (error) { - // ... handle error + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); } } }); @@ -353,8 +366,13 @@ app.delete('/mcp', async (req: Request, res: Response) => { // Start the server const PORT = 3000; -app.listen(PORT, () => { - console.log(`Stateless MCP Streamable HTTP Server listening on port ${PORT}`); +setupServer().then(() => { + app.listen(PORT, () => { + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); + }); +}).catch(error => { + console.error('Failed to set up the server:', error); + process.exit(1); }); ``` diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index 89433ecb..f1f37510 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -96,7 +96,11 @@ app.use(express.json()); const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); -await server.connect(transport); + +// Setup routes for the server +const setupServer = async () => { + await server.connect(transport); +}; app.post('/mcp', async (req: Request, res: Response) => { console.log('Received MCP request:', req.body); @@ -143,8 +147,13 @@ app.delete('/mcp', async (req: Request, res: Response) => { // Start the server const PORT = 3000; -app.listen(PORT, () => { - console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); +setupServer().then(() => { + app.listen(PORT, () => { + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); + }); +}).catch(error => { + console.error('Failed to set up the server:', error); + process.exit(1); }); // Handle server shutdown From 1414d6a52c3cd0ec11f599be18cedec8d485175e Mon Sep 17 00:00:00 2001 From: ihrpr Date: Fri, 18 Apr 2025 11:57:59 +0100 Subject: [PATCH 3/4] improve test --- src/integration-tests/stateManagementStreamableHttp.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index 88a06d38..b7ff17e6 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -140,7 +140,7 @@ describe('Streamable HTTP Transport Session Management', () => { expect(transport2.sessionId).toBeUndefined(); // List available tools - await client1.request({ + await client2.request({ method: 'tools/list', params: {} }, ListToolsResultSchema); From b6c8c6019b1d40c94e5eba748b2e0365d4af654f Mon Sep 17 00:00:00 2001 From: ihrpr Date: Fri, 18 Apr 2025 11:58:18 +0100 Subject: [PATCH 4/4] bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7963ec6..7d72e6e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.10.0", + "version": "1.10.1", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",