-
Notifications
You must be signed in to change notification settings - Fork 770
Add examples of server and client for streamable http transport #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
c645ec9
streamable client and server examples
ihrpr 6c6df5e
improve readme
ihrpr b9faec3
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 161b8da
fix lint and rename
ihrpr 6c5efef
Merge branch 'ihrpr/streamable-http-client' into ihrpr/examples
ihrpr 1a3cf1b
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 3cf82c3
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 6436c07
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 70af109
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 78cf1bd
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr 86714c0
Merge branch 'ihrpr/fix-streamable-connection-close' into ihrpr/examples
ihrpr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# MCP TypeScript SDK Examples | ||
|
||
This directory contains example implementations of MCP clients and servers using the TypeScript SDK. | ||
|
||
## Streamable HTTP - single node deployment with basic session state management | ||
|
||
Multi node with stete management example will be added soon after we add support. | ||
|
||
### 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 and tool listing: | ||
|
||
```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 | ||
``` | ||
|
||
### Client (`client/simpleStreamableHttp.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/simpleStreamableHttp.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 between the calls | ||
- The client handles both direct HTTP responses and SSE streaming responses |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> { | ||
// 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); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CallToolResult> => { | ||
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<GetPromptResult> => { | ||
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<ReadResourceResult> => { | ||
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: unknown): boolean { | ||
if (Array.isArray(body)) { | ||
return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize'); | ||
} | ||
return typeof body === 'object' && body !== null && 'method' in body && 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); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, there's a race condition here AFAICT. I believe this sequence of events is possible:
This might be something we have to fix in the interfaces we offer—for example, maybe a callback like
onsessioncreated
or something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, good point, the session management is still in the follow ups, I'll address it there if that's okay