From c645ec9ad8507dede5120bab01d880f7a6958ed9 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Tue, 8 Apr 2025 13:13:58 +0100 Subject: [PATCH 1/3] streamable client and server examples --- src/examples/README.md | 123 ++++++++++++++ .../client/simpleStreamableHttpClient.ts | 84 ++++++++++ src/examples/server/simpleStreamableHttp.ts | 157 ++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 src/examples/README.md create mode 100644 src/examples/client/simpleStreamableHttpClient.ts create mode 100644 src/examples/server/simpleStreamableHttp.ts diff --git a/src/examples/README.md b/src/examples/README.md new file mode 100644 index 00000000..878a2fef --- /dev/null +++ b/src/examples/README.md @@ -0,0 +1,123 @@ +# MCP TypeScript SDK Examples + +This directory contains example implementations of MCP clients and servers using the TypeScript SDK. + +## Streamable HTTP Examples + +### List Tool Request Example + +Using `curl` to list available tools: + +```bash +# First initialize the server and save the session ID to a variable +SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \ + -i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r') +echo "Session ID: $SESSION_ID" + +# Then list tools using the saved session ID +curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \ + http://localhost:3000/mcp +``` + +Using the TypeScript client (session management is handled automatically): + +```typescript +const toolsRequest = { method: 'tools/list', params: {} }; +const toolsResult = await client.request(toolsRequest, ListToolsResultSchema); +console.log('Available tools:', toolsResult.tools); +``` + +### Call Tool Request Example + +Using `curl` to call a tool: + +```bash +curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"greet","arguments":{"name":"User"}},"id":"3"}' \ + http://localhost:3000/mcp +``` + +Using the TypeScript client: + +```typescript +const greetRequest = { + method: 'tools/call', + params: { + name: 'greet', + arguments: { name: 'MCP User' } + } +}; +const greetResult = await client.request(greetRequest, CallToolResultSchema); +``` + +### Get Prompt Request Example + +Using `curl` to get a prompt: + +```bash +curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{"jsonrpc":"2.0","method":"prompts/get","params":{"name":"greeting-template","arguments":{"name":"User"}},"id":"4"}' \ + http://localhost:3000/mcp +``` + +Using the TypeScript client: + +```typescript +const promptRequest = { + method: 'prompts/get', + params: { + name: 'greeting-template', + arguments: { name: 'MCP User' } + } +}; +const promptResult = await client.request(promptRequest, GetPromptResultSchema); +``` + +### Server (`server/simpleStreamableHttp.ts`) + +A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides: + +- A simple `greet` tool that returns a greeting for a name +- A `greeting-template` prompt that generates a greeting template +- A static `greeting-resource` resource + +#### Running the server + +```bash +npx tsx src/examples/server/simpleStreamableHttp.ts +``` + +The server will start on port 3000. You can test the initialization with: + +```bash +curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \ + http://localhost:3000/mcp +``` + +### Client (`client/simpleStreamableHttpClient.ts`) + +A client that connects to the server, initializes it, and demonstrates how to: + +- List available tools and call the `greet` tool +- List available prompts and get the `greeting-template` prompt +- List available resources + +#### Running the client + +```bash +npx tsx src/examples/client/simpleStreamableHttpClient.ts +``` + +Make sure the server is running before starting the client. + +## Notes + +- These examples demonstrate the basic usage of the Streamable HTTP transport +- The server manages sessions for stateful connections +- The client handles both direct HTTP responses and SSE streaming responses \ No newline at end of file diff --git a/src/examples/client/simpleStreamableHttpClient.ts b/src/examples/client/simpleStreamableHttpClient.ts new file mode 100644 index 00000000..9bf43ce8 --- /dev/null +++ b/src/examples/client/simpleStreamableHttpClient.ts @@ -0,0 +1,84 @@ +import { Client } from '../../client/index.js'; +import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js'; +import { + ListToolsRequest, + ListToolsResultSchema, + CallToolRequest, + CallToolResultSchema, + ListPromptsRequest, + ListPromptsResultSchema, + GetPromptRequest, + GetPromptResultSchema, + ListResourcesRequest, + ListResourcesResultSchema +} from '../../types.js'; + +async function main(): Promise { + // Create a new client with streamable HTTP transport + const client = new Client({ + name: 'example-client', + version: '1.0.0' + }); + const transport = new StreamableHTTPClientTransport( + new URL('http://localhost:3000/mcp') + ); + + // Connect the client using the transport and initialize the server + await client.connect(transport); + + console.log('Connected to MCP server'); + + // List available tools + const toolsRequest: ListToolsRequest = { + method: 'tools/list', + params: {} + }; + const toolsResult = await client.request(toolsRequest, ListToolsResultSchema); + console.log('Available tools:', toolsResult.tools); + + // Call the 'greet' tool + const greetRequest: CallToolRequest = { + method: 'tools/call', + params: { + name: 'greet', + arguments: { name: 'MCP User' } + } + }; + const greetResult = await client.request(greetRequest, CallToolResultSchema); + console.log('Greeting result:', greetResult.content[0].text); + + // List available prompts + const promptsRequest: ListPromptsRequest = { + method: 'prompts/list', + params: {} + }; + const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema); + console.log('Available prompts:', promptsResult.prompts); + + // Get a prompt + const promptRequest: GetPromptRequest = { + method: 'prompts/get', + params: { + name: 'greeting-template', + arguments: { name: 'MCP User' } + } + }; + const promptResult = await client.request(promptRequest, GetPromptResultSchema); + console.log('Prompt template:', promptResult.messages[0].content.text); + + // List available resources + const resourcesRequest: ListResourcesRequest = { + method: 'resources/list', + params: {} + }; + const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema); + console.log('Available resources:', resourcesResult.resources); + + // Close the connection + await client.close(); +} + +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/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts new file mode 100644 index 00000000..8db0e236 --- /dev/null +++ b/src/examples/server/simpleStreamableHttp.ts @@ -0,0 +1,157 @@ +import express, { Request, Response } from 'express'; +import { randomUUID } from 'node:crypto'; +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: 'simple-streamable-http-server', + version: '1.0.0', +}); + +// 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 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.`, + }, + }, + ], + }; + } +); + +// 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()); + +// Map to store transports by session ID +const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + +app.post('/mcp', async (req: Request, res: Response) => { + console.log('Received MCP request:', req.body); + try { + // Check for existing session ID + const sessionId = req.headers['mcp-session-id'] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); + + // Connect the transport to the MCP server BEFORE handling the request + // so responses can flow back through the same transport + await server.connect(transport); + + // After handling the request, if we get a session ID back, store the transport + await transport.handleRequest(req, res, req.body); + + // Store the transport by session ID for future requests + if (transport.sessionId) { + transports[transport.sessionId] = transport; + } + return; // Already handled + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + + // Handle the request with existing transport - no need to reconnect + // The existing transport is already connected to the server + 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, + }); + } + } +}); + +// Helper function to detect initialize requests +function isInitializeRequest(body: any): boolean { + if (Array.isArray(body)) { + return body.some(msg => msg.method === 'initialize'); + } + return body.method === 'initialize'; +} + +// Start the server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); + console.log(`Test with: curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' http://localhost:${PORT}/mcp`); +}); + +// Handle server shutdown +process.on('SIGINT', async () => { + console.log('Shutting down server...'); + await server.close(); + process.exit(0); +}); \ No newline at end of file From 6c6df5e0111ef8973409284c17ee16c181464997 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Tue, 8 Apr 2025 15:34:27 +0100 Subject: [PATCH 2/3] improve readme --- src/examples/README.md | 92 +++++++----------------------------------- 1 file changed, 14 insertions(+), 78 deletions(-) diff --git a/src/examples/README.md b/src/examples/README.md index 878a2fef..40898578 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -2,81 +2,9 @@ This directory contains example implementations of MCP clients and servers using the TypeScript SDK. -## Streamable HTTP Examples +## Streamable HTTP - single node deployment with basic session state management -### List Tool Request Example - -Using `curl` to list available tools: - -```bash -# First initialize the server and save the session ID to a variable -SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ - -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \ - -i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r') -echo "Session ID: $SESSION_ID" - -# Then list tools using the saved session ID -curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ - -H "mcp-session-id: $SESSION_ID" \ - -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \ - http://localhost:3000/mcp -``` - -Using the TypeScript client (session management is handled automatically): - -```typescript -const toolsRequest = { method: 'tools/list', params: {} }; -const toolsResult = await client.request(toolsRequest, ListToolsResultSchema); -console.log('Available tools:', toolsResult.tools); -``` - -### Call Tool Request Example - -Using `curl` to call a tool: - -```bash -curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ - -H "mcp-session-id: $SESSION_ID" \ - -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"greet","arguments":{"name":"User"}},"id":"3"}' \ - http://localhost:3000/mcp -``` - -Using the TypeScript client: - -```typescript -const greetRequest = { - method: 'tools/call', - params: { - name: 'greet', - arguments: { name: 'MCP User' } - } -}; -const greetResult = await client.request(greetRequest, CallToolResultSchema); -``` - -### Get Prompt Request Example - -Using `curl` to get a prompt: - -```bash -curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ - -H "mcp-session-id: $SESSION_ID" \ - -d '{"jsonrpc":"2.0","method":"prompts/get","params":{"name":"greeting-template","arguments":{"name":"User"}},"id":"4"}' \ - http://localhost:3000/mcp -``` - -Using the TypeScript client: - -```typescript -const promptRequest = { - method: 'prompts/get', - params: { - name: 'greeting-template', - arguments: { name: 'MCP User' } - } -}; -const promptResult = await client.request(promptRequest, GetPromptResultSchema); -``` +Multi node with stete management example will be added soon after we add support. ### Server (`server/simpleStreamableHttp.ts`) @@ -92,11 +20,19 @@ A simple MCP server that uses the Streamable HTTP transport, implemented with Ex npx tsx src/examples/server/simpleStreamableHttp.ts ``` -The server will start on port 3000. You can test the initialization with: +The server will start on port 3000. You can test the initialization and tool listing: ```bash -curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ +# First initialize the server and save the session ID to a variable +SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \ + -i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r') +echo "Session ID: $SESSION_ID" + +# Then list tools using the saved session ID +curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \ http://localhost:3000/mcp ``` @@ -119,5 +55,5 @@ Make sure the server is running before starting the client. ## Notes - These examples demonstrate the basic usage of the Streamable HTTP transport -- The server manages sessions for stateful connections -- The client handles both direct HTTP responses and SSE streaming responses \ No newline at end of file +- The server manages sessions between the calls +- The client handles both direct HTTP responses and SSE streaming responses From 161b8da3e08790120d264f8c51bf2be08d6363aa Mon Sep 17 00:00:00 2001 From: ihrpr Date: Tue, 8 Apr 2025 16:16:51 +0100 Subject: [PATCH 3/3] fix lint and rename --- src/examples/README.md | 4 ++-- ...impleStreamableHttpClient.ts => simpleStreamableHttp.ts} | 0 src/examples/server/simpleStreamableHttp.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/examples/client/{simpleStreamableHttpClient.ts => simpleStreamableHttp.ts} (100%) diff --git a/src/examples/README.md b/src/examples/README.md index 40898578..6e53fdec 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -36,7 +36,7 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, t http://localhost:3000/mcp ``` -### Client (`client/simpleStreamableHttpClient.ts`) +### Client (`client/simpleStreamableHttp.ts`) A client that connects to the server, initializes it, and demonstrates how to: @@ -47,7 +47,7 @@ A client that connects to the server, initializes it, and demonstrates how to: #### Running the client ```bash -npx tsx src/examples/client/simpleStreamableHttpClient.ts +npx tsx src/examples/client/simpleStreamableHttp.ts ``` Make sure the server is running before starting the client. diff --git a/src/examples/client/simpleStreamableHttpClient.ts b/src/examples/client/simpleStreamableHttp.ts similarity index 100% rename from src/examples/client/simpleStreamableHttpClient.ts rename to src/examples/client/simpleStreamableHttp.ts diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 8db0e236..e6ebe4b9 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -135,11 +135,11 @@ app.post('/mcp', async (req: Request, res: Response) => { }); // Helper function to detect initialize requests -function isInitializeRequest(body: any): boolean { +function isInitializeRequest(body: unknown): boolean { if (Array.isArray(body)) { - return body.some(msg => msg.method === 'initialize'); + return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize'); } - return body.method === 'initialize'; + return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize'; } // Start the server