diff --git a/README.md b/README.md index 5345c5460..e5a283d96 100644 --- a/README.md +++ b/README.md @@ -310,19 +310,23 @@ For simpler use cases where session management isn't needed: const app = express(); app.use(express.json()); -const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // set to undefined for stateless servers -}); - -// 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); + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + try { - await transport.handleRequest(req, res, req.body); + const server = getServer(); + const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + res.on('close', () => { + console.log('Request closed'); + transport.close(); + server.close(); + }); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { @@ -362,15 +366,11 @@ app.delete('/mcp', async (req: Request, res: Response) => { })); }); + // 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); +app.listen(PORT, () => { + console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`); }); ``` diff --git a/src/examples/client/multipleClientsParallel.ts b/src/examples/client/multipleClientsParallel.ts new file mode 100644 index 000000000..cc01fc06e --- /dev/null +++ b/src/examples/client/multipleClientsParallel.ts @@ -0,0 +1,160 @@ +import { Client } from '../../client/index.js'; +import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js'; +import { + CallToolRequest, + CallToolResultSchema, + LoggingMessageNotificationSchema, + CallToolResult, +} from '../../types.js'; + +/** + * Multiple Clients MCP Example + * + * This client demonstrates how to: + * 1. Create multiple MCP clients in parallel + * 2. Each client calls a single tool + * 3. Track notifications from each client independently + */ + +// Command line args processing +const args = process.argv.slice(2); +const serverUrl = args[0] || 'http://localhost:3000/mcp'; + +interface ClientConfig { + id: string; + name: string; + toolName: string; + toolArguments: Record; +} + +async function createAndRunClient(config: ClientConfig): Promise<{ id: string; result: CallToolResult }> { + console.log(`[${config.id}] Creating client: ${config.name}`); + + const client = new Client({ + name: config.name, + version: '1.0.0' + }); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + // Set up client-specific error handler + client.onerror = (error) => { + console.error(`[${config.id}] Client error:`, error); + }; + + // Set up client-specific notification handler + client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + console.log(`[${config.id}] Notification: ${notification.params.data}`); + }); + + try { + // Connect to the server + await client.connect(transport); + console.log(`[${config.id}] Connected to MCP server`); + + // Call the specified tool + console.log(`[${config.id}] Calling tool: ${config.toolName}`); + const toolRequest: CallToolRequest = { + method: 'tools/call', + params: { + name: config.toolName, + arguments: { + ...config.toolArguments, + // Add client ID to arguments for identification in notifications + caller: config.id + } + } + }; + + const result = await client.request(toolRequest, CallToolResultSchema); + console.log(`[${config.id}] Tool call completed`); + + // Keep the connection open for a bit to receive notifications + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Disconnect + await transport.close(); + console.log(`[${config.id}] Disconnected from MCP server`); + + return { id: config.id, result }; + } catch (error) { + console.error(`[${config.id}] Error:`, error); + throw error; + } +} + +async function main(): Promise { + console.log('MCP Multiple Clients Example'); + console.log('============================'); + console.log(`Server URL: ${serverUrl}`); + console.log(''); + + try { + // Define client configurations + const clientConfigs: ClientConfig[] = [ + { + id: 'client1', + name: 'basic-client-1', + toolName: 'start-notification-stream', + toolArguments: { + interval: 3, // 1 second between notifications + count: 5 // Send 5 notifications + } + }, + { + id: 'client2', + name: 'basic-client-2', + toolName: 'start-notification-stream', + toolArguments: { + interval: 2, // 2 seconds between notifications + count: 3 // Send 3 notifications + } + }, + { + id: 'client3', + name: 'basic-client-3', + toolName: 'start-notification-stream', + toolArguments: { + interval: 1, // 0.5 second between notifications + count: 8 // Send 8 notifications + } + } + ]; + + // Start all clients in parallel + console.log(`Starting ${clientConfigs.length} clients in parallel...`); + console.log(''); + + const clientPromises = clientConfigs.map(config => createAndRunClient(config)); + const results = await Promise.all(clientPromises); + + // Display results from all clients + console.log('\n=== Final Results ==='); + results.forEach(({ id, result }) => { + console.log(`\n[${id}] Tool result:`); + if (Array.isArray(result.content)) { + result.content.forEach((item: { type: string; text?: string }) => { + if (item.type === 'text' && item.text) { + console.log(` ${item.text}`); + } else { + console.log(` ${item.type} content:`, item); + } + }); + } else { + console.log(` Unexpected result format:`, result); + } + }); + + console.log('\n=== All clients completed successfully ==='); + + } catch (error) { + console.error('Error running multiple clients:', error); + process.exit(1); + } +} + +// Start the example +main().catch((error: unknown) => { + console.error('Error running MCP multiple clients example:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/examples/client/parallelToolCallsClient.ts b/src/examples/client/parallelToolCallsClient.ts new file mode 100644 index 000000000..3783992d6 --- /dev/null +++ b/src/examples/client/parallelToolCallsClient.ts @@ -0,0 +1,197 @@ +import { Client } from '../../client/index.js'; +import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js'; +import { + ListToolsRequest, + ListToolsResultSchema, + CallToolResultSchema, + LoggingMessageNotificationSchema, + CallToolResult, +} from '../../types.js'; + +/** + * Parallel Tool Calls MCP Client + * + * This client demonstrates how to: + * 1. Start multiple tool calls in parallel + * 2. Track notifications from each tool call using a caller parameter + */ + +// Command line args processing +const args = process.argv.slice(2); +const serverUrl = args[0] || 'http://localhost:3000/mcp'; + +async function main(): Promise { + console.log('MCP Parallel Tool Calls Client'); + console.log('=============================='); + console.log(`Connecting to server at: ${serverUrl}`); + + let client: Client; + let transport: StreamableHTTPClientTransport; + + try { + // Create client with streamable HTTP transport + client = new Client({ + name: 'parallel-tool-calls-client', + version: '1.0.0' + }); + + client.onerror = (error) => { + console.error('Client error:', error); + }; + + // Connect to the server + transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + await client.connect(transport); + console.log('Successfully connected to MCP server'); + + // Set up notification handler with caller identification + client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + console.log(`Notification: ${notification.params.data}`); + }); + + console.log("List tools") + const toolsRequest = await listTools(client); + console.log("Tools: ", toolsRequest) + + + // 2. Start multiple notification tools in parallel + console.log('\n=== Starting Multiple Notification Streams in Parallel ==='); + const toolResults = await startParallelNotificationTools(client); + + // Log the results from each tool call + for (const [caller, result] of Object.entries(toolResults)) { + console.log(`\n=== Tool result for ${caller} ===`); + result.content.forEach((item: { type: string; text?: string; }) => { + if (item.type === 'text') { + console.log(` ${item.text}`); + } else { + console.log(` ${item.type} content:`, item); + } + }); + } + + // 3. Wait for all notifications (10 seconds) + console.log('\n=== Waiting for all notifications ==='); + await new Promise(resolve => setTimeout(resolve, 10000)); + + // 4. Disconnect + console.log('\n=== Disconnecting ==='); + await transport.close(); + console.log('Disconnected from MCP server'); + + } catch (error) { + console.error('Error running client:', error); + process.exit(1); + } +} + +/** + * List available tools on the server + */ +async function listTools(client: Client): Promise { + try { + const toolsRequest: ListToolsRequest = { + method: 'tools/list', + params: {} + }; + const toolsResult = await client.request(toolsRequest, ListToolsResultSchema); + + console.log('Available tools:'); + if (toolsResult.tools.length === 0) { + console.log(' No tools available'); + } else { + for (const tool of toolsResult.tools) { + console.log(` - ${tool.name}: ${tool.description}`); + } + } + } catch (error) { + console.log(`Tools not supported by this server: ${error}`); + } +} + +/** + * Start multiple notification tools in parallel with different configurations + * Each tool call includes a caller parameter to identify its notifications + */ +async function startParallelNotificationTools(client: Client): Promise> { + try { + // Define multiple tool calls with different configurations + const toolCalls = [ + { + caller: 'fast-notifier', + request: { + method: 'tools/call', + params: { + name: 'start-notification-stream', + arguments: { + interval: 2, // 0.5 second between notifications + count: 10, // Send 10 notifications + caller: 'fast-notifier' // Identify this tool call + } + } + } + }, + { + caller: 'slow-notifier', + request: { + method: 'tools/call', + params: { + name: 'start-notification-stream', + arguments: { + interval: 5, // 2 seconds between notifications + count: 5, // Send 5 notifications + caller: 'slow-notifier' // Identify this tool call + } + } + } + }, + { + caller: 'burst-notifier', + request: { + method: 'tools/call', + params: { + name: 'start-notification-stream', + arguments: { + interval: 1, // 0.1 second between notifications + count: 3, // Send just 3 notifications + caller: 'burst-notifier' // Identify this tool call + } + } + } + } + ]; + + console.log(`Starting ${toolCalls.length} notification tools in parallel...`); + + // Start all tool calls in parallel + const toolPromises = toolCalls.map(({ caller, request }) => { + console.log(`Starting tool call for ${caller}...`); + return client.request(request, CallToolResultSchema) + .then(result => ({ caller, result })) + .catch(error => { + console.error(`Error in tool call for ${caller}:`, error); + throw error; + }); + }); + + // Wait for all tool calls to complete + const results = await Promise.all(toolPromises); + + // Organize results by caller + const resultsByTool: Record = {}; + results.forEach(({ caller, result }) => { + resultsByTool[caller] = result; + }); + + return resultsByTool; + } catch (error) { + console.error(`Error starting parallel notification tools:`, error); + throw error; + } +} + +// Start the client +main().catch((error: unknown) => { + console.error('Error running MCP client:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index bcd6a9602..02d8c2de0 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -5,74 +5,78 @@ import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { z } from 'zod'; import { CallToolResult, isInitializeRequest } from '../../types.js'; + // Create an MCP server with implementation details -const server = new McpServer({ - name: 'json-response-streamable-http-server', - version: '1.0.0', -}, { - capabilities: { - logging: {}, - } -}); +const getServer = () => { + const server = new McpServer({ + name: 'json-response-streamable-http-server', + version: '1.0.0', + }, { + capabilities: { + logging: {}, + } + }); + + // Register a simple tool that returns a greeting + server.tool( + 'greet', + 'A simple greeting tool', + { + name: z.string().describe('Name to greet'), + }, + async ({ name }): Promise => { + return { + content: [ + { + type: 'text', + text: `Hello, ${name}!`, + }, + ], + }; + } + ); + + // Register a tool that sends multiple greetings with notifications + server.tool( + 'multi-greet', + 'A tool that sends different greetings with delays between them', + { + name: z.string().describe('Name to greet'), + }, + async ({ name }, { sendNotification }): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + await sendNotification({ + method: "notifications/message", + params: { level: "debug", data: `Starting multi-greet for ${name}` } + }); -// Register a simple tool that returns a greeting -server.tool( - 'greet', - 'A simple greeting tool', - { - name: z.string().describe('Name to greet'), - }, - async ({ name }): Promise => { - return { - content: [ - { - type: 'text', - text: `Hello, ${name}!`, - }, - ], - }; - } -); - -// Register a tool that sends multiple greetings with notifications -server.tool( - 'multi-greet', - 'A tool that sends different greetings with delays between them', - { - name: z.string().describe('Name to greet'), - }, - async ({ name }, { sendNotification }): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - await sendNotification({ - method: "notifications/message", - params: { level: "debug", data: `Starting multi-greet for ${name}` } - }); - - await sleep(1000); // Wait 1 second before first greeting - - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending first greeting to ${name}` } - }); - - await sleep(1000); // Wait another second before second greeting - - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending second greeting to ${name}` } - }); - - return { - content: [ - { - type: 'text', - text: `Good morning, ${name}!`, - } - ], - }; - } -); + await sleep(1000); // Wait 1 second before first greeting + + await sendNotification({ + method: "notifications/message", + params: { level: "info", data: `Sending first greeting to ${name}` } + }); + + await sleep(1000); // Wait another second before second greeting + + await sendNotification({ + method: "notifications/message", + params: { level: "info", data: `Sending second greeting to ${name}` } + }); + + return { + content: [ + { + type: 'text', + text: `Good morning, ${name}!`, + } + ], + }; + } + ); + return server; +} const app = express(); app.use(express.json()); @@ -104,6 +108,7 @@ app.post('/mcp', async (req: Request, res: Response) => { }); // Connect the transport to the MCP server BEFORE handling the request + const server = getServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); return; // Already handled @@ -153,6 +158,5 @@ app.listen(PORT, () => { // Handle server shutdown process.on('SIGINT', async () => { console.log('Shutting down server...'); - await server.close(); process.exit(0); }); \ No newline at end of file diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index 74cdcaac3..cae3be301 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -15,60 +15,63 @@ import { CallToolResult } from '../../types.js'; */ // Create an MCP server instance -const server = new McpServer({ - name: 'simple-sse-server', - version: '1.0.0', -}, { capabilities: { logging: {} } }); - -server.tool( - 'start-notification-stream', - 'Starts sending periodic notifications', - { - interval: z.number().describe('Interval in milliseconds between notifications').default(1000), - count: z.number().describe('Number of notifications to send').default(10), - }, - async ({ interval, count }, { sendNotification }): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - let counter = 0; - - // Send the initial notification - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Starting notification stream with ${count} messages every ${interval}ms` +const getServer = () => { + const server = new McpServer({ + name: 'simple-sse-server', + version: '1.0.0', + }, { capabilities: { logging: {} } }); + + server.tool( + 'start-notification-stream', + 'Starts sending periodic notifications', + { + interval: z.number().describe('Interval in milliseconds between notifications').default(1000), + count: z.number().describe('Number of notifications to send').default(10), + }, + async ({ interval, count }, { sendNotification }): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + let counter = 0; + + // Send the initial notification + await sendNotification({ + method: "notifications/message", + params: { + level: "info", + data: `Starting notification stream with ${count} messages every ${interval}ms` + } + }); + + // Send periodic notifications + while (counter < count) { + counter++; + await sleep(interval); + + try { + await sendNotification({ + method: "notifications/message", + params: { + level: "info", + data: `Notification #${counter} at ${new Date().toISOString()}` + } + }); + } + catch (error) { + console.error("Error sending notification:", error); + } } - }); - - // Send periodic notifications - while (counter < count) { - counter++; - await sleep(interval); - - try { - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Notification #${counter} at ${new Date().toISOString()}` + + return { + content: [ + { + type: 'text', + text: `Completed sending ${count} notifications every ${interval}ms`, } - }); - } - catch (error) { - console.error("Error sending notification:", error); - } + ], + }; } - - return { - content: [ - { - type: 'text', - text: `Completed sending ${count} notifications every ${interval}ms`, - } - ], - }; - } -); + ); + return server; +}; const app = express(); app.use(express.json()); @@ -96,6 +99,7 @@ app.get('/mcp', async (req: Request, res: Response) => { }; // Connect the transport to the MCP server + const server = getServer(); await server.connect(transport); // Start the SSE transport to begin streaming @@ -163,7 +167,6 @@ process.on('SIGINT', async () => { console.error(`Error closing transport for session ${sessionId}:`, error); } } - await server.close(); console.log('Server shutdown complete'); process.exit(0); }); \ No newline at end of file diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index f1f375100..6fb2ae831 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -4,108 +4,111 @@ 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: {} } }); +const getServer = () => { + // 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 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; + // 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); + 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); } - // Wait for the specified interval - await sleep(interval); - } - return { - content: [ - { - type: 'text', - text: `Started sending periodic notifications every ${interval}ms`, - } - ], - }; - } -); + 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!', - }, - ], - }; - } -); + // 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!', + }, + ], + }; + } + ); + return server; +} 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); + const server = getServer(); try { - await transport.handleRequest(req, res, req.body); + const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + res.on('close', () => { + console.log('Request closed'); + transport.close(); + server.close(); + }); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { @@ -145,28 +148,15 @@ app.delete('/mcp', async (req: Request, res: Response) => { })); }); + // 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); +app.listen(PORT, () => { + console.log(`MCP Stateless 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/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 2b25f4b24..d94908bdc 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -7,164 +7,167 @@ import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResul import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; // Create an MCP server with implementation details -const server = new McpServer({ - name: 'simple-streamable-http-server', - version: '1.0.0', -}, { capabilities: { logging: {} } }); - -// Log the capability invocation details -server.onCapabilityChange((event) => { - switch (event.action) { - case 'invoked': - console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' started`); - break; - case 'completed': - console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' completed in ${event.durationMs}ms`); - break; - case 'error': - console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' failed in ${event.durationMs}ms: ${event.error}`); - break; - } -}); - -// Register a simple tool that returns a greeting -server.tool( - 'greet', - 'A simple greeting tool', - { - name: z.string().describe('Name to greet'), - }, - async ({ name }): Promise => { - return { - content: [ - { - type: 'text', - text: `Hello, ${name}!`, - }, - ], - }; - } -); - -// Register a tool that sends multiple greetings with notifications -server.tool( - 'multi-greet', - 'A tool that sends different greetings with delays between them', - { - name: z.string().describe('Name to greet'), - }, - async ({ name }, { sendNotification }): Promise => { - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - await sendNotification({ - method: "notifications/message", - params: { level: "debug", data: `Starting multi-greet for ${name}` } - }); - - await sleep(1000); // Wait 1 second before first greeting - - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending first greeting to ${name}` } - }); - - await sleep(1000); // Wait another second before second greeting - - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending second greeting to ${name}` } - }); - - return { - content: [ - { - type: 'text', - text: `Good morning, ${name}!`, - } - ], - }; - } -); - -// 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: { +const getServer = () => { + const server = new McpServer({ + name: 'simple-streamable-http-server', + version: '1.0.0', + }, { capabilities: { logging: {} } }); + + // Log the capability invocation details + server.onCapabilityChange((event) => { + switch (event.action) { + case 'invoked': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' started`); + break; + case 'completed': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' completed in ${event.durationMs}ms`); + break; + case 'error': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' failed in ${event.durationMs}ms: ${event.error}`); + break; + } + }); + + // Register a simple tool that returns a greeting + server.tool( + 'greet', + 'A simple greeting tool', + { + name: z.string().describe('Name to greet'), + }, + async ({ name }): Promise => { + return { + content: [ + { type: 'text', - text: `Please greet ${name} in a friendly manner.`, + text: `Hello, ${name}!`, }, - }, - ], - }; - } -); - -// 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(50), - }, - 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); + ], + }; } + ); + + // Register a tool that sends multiple greetings with notifications + server.tool( + 'multi-greet', + 'A tool that sends different greetings with delays between them', + { + name: z.string().describe('Name to greet'), + }, + async ({ name }, { sendNotification }): Promise => { + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + await sendNotification({ + method: "notifications/message", + params: { level: "debug", data: `Starting multi-greet for ${name}` } + }); + + await sleep(1000); // Wait 1 second before first greeting + + await sendNotification({ + method: "notifications/message", + params: { level: "info", data: `Sending first greeting to ${name}` } + }); + + await sleep(1000); // Wait another second before second greeting + + await sendNotification({ + method: "notifications/message", + params: { level: "info", data: `Sending second greeting to ${name}` } + }); - return { - content: [ - { - type: 'text', - text: `Started sending periodic notifications every ${interval}ms`, + return { + content: [ + { + type: 'text', + text: `Good morning, ${name}!`, + } + ], + }; + } + ); + + // 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(50), + }, + 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()}` + } + }); } - ], - }; - } -); - -// 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!', - }, - ], - }; - } -); + 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!', + }, + ], + }; + } + ); + return server; +}; const app = express(); app.use(express.json()); @@ -207,6 +210,7 @@ app.post('/mcp', async (req: Request, res: Response) => { // Connect the transport to the MCP server BEFORE handling the request // so responses can flow back through the same transport + const server = getServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); @@ -303,7 +307,6 @@ process.on('SIGINT', async () => { console.error(`Error closing transport for session ${sessionId}:`, error); } } - await server.close(); console.log('Server shutdown complete'); process.exit(0); }); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index d644dd070..7a0ab04e1 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -18,52 +18,54 @@ import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; * - /messages: The deprecated POST endpoint for older clients (POST to send messages) */ +const getServer = () => { + const server = new McpServer({ + name: 'backwards-compatible-server', + version: '1.0.0', + }, { capabilities: { logging: {} } }); + + // Register a simple tool that sends notifications over time + 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(50), + }, + 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); + } -const server = new McpServer({ - name: 'backwards-compatible-server', - version: '1.0.0', -}, { capabilities: { logging: {} } }); - -// Register a simple tool that sends notifications over time -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(50), - }, - 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()}` + return { + content: [ + { + type: 'text', + text: `Started sending periodic notifications every ${interval}ms`, } - }); - } - 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`, - } - ], - }; - } -); + ); + return server; +}; // Create Express application const app = express(); @@ -125,6 +127,7 @@ app.all('/mcp', async (req: Request, res: Response) => { }; // Connect the transport to the MCP server + const server = getServer(); await server.connect(transport); } else { // Invalid request - no session ID or not initialization request @@ -167,6 +170,7 @@ app.get('/sse', async (req: Request, res: Response) => { res.on("close", () => { delete transports[transport.sessionId]; }); + const server = getServer(); await server.connect(transport); }); @@ -237,7 +241,6 @@ process.on('SIGINT', async () => { console.error(`Error closing transport for session ${sessionId}:`, error); } } - await server.close(); console.log('Server shutdown complete'); process.exit(0); }); \ No newline at end of file diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 85a8b9f11..ed83076ac 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -463,6 +463,11 @@ export class StreamableHTTPServerTransport implements Transport { * Returns true if the session is valid, false otherwise */ private validateSession(req: IncomingMessage, res: ServerResponse): boolean { + if (this.sessionIdGenerator === undefined) { + // If the sessionIdGenerator ID is not set, the session management is disabled + // and we don't need to validate the session ID + return true; + } if (!this._initialized) { // If the server has not been initialized yet, reject all requests res.writeHead(400).end(JSON.stringify({ @@ -475,11 +480,7 @@ export class StreamableHTTPServerTransport implements Transport { })); return false; } - if (this.sessionId === undefined) { - // If the session ID is not set, the session management is disabled - // and we don't need to validate the session ID - return true; - } + const sessionId = req.headers["mcp-session-id"]; if (!sessionId) {