diff --git a/README.md b/README.md index ed97b83a..200cfab6 100644 --- a/README.md +++ b/README.md @@ -309,35 +309,72 @@ 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, // set to undefined for stateless servers }); -// ... set up server resources, tools, and prompts ... +// Setup routes for the server +const setupServer = async () => { + await server.connect(transport); +}; -const app = express(); -app.use(express.json()); +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, + }); + } + } +}); -// 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 +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; +setupServer().then(() => { + app.listen(PORT, () => { + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); }); - - // Connect to server and handle the request - await server.connect(transport); - await transport.handleRequest(req, res); +}).catch(error => { + console.error('Failed to set up the server:', error); + process.exit(1); }); -app.listen(3000); ``` This stateless approach is useful for: 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)", diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts new file mode 100644 index 00000000..f1f37510 --- /dev/null +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -0,0 +1,172 @@ +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, +}); + +// 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) { + 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; +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 +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..b7ff17e6 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 client2.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: {