Merge pull request #294 from modelcontextprotocol/ihrpr/examples
Add examples of server and client for streamable http transport
This commit is contained in:
commit
0fa46aa2e9
3 changed files with 300 additions and 0 deletions
59
src/examples/README.md
Normal file
59
src/examples/README.md
Normal file
|
|
@ -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
|
||||
84
src/examples/client/simpleStreamableHttp.ts
Normal file
84
src/examples/client/simpleStreamableHttp.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
157
src/examples/server/simpleStreamableHttp.ts
Normal file
157
src/examples/server/simpleStreamableHttp.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in a new issue