Skip to content

Commit 0fa46aa

Browse files
authored
Merge pull request #294 from modelcontextprotocol/ihrpr/examples
Add examples of server and client for streamable http transport
2 parents 27d2996 + 86714c0 commit 0fa46aa

File tree

3 files changed

+300
-0
lines changed

3 files changed

+300
-0
lines changed

src/examples/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# MCP TypeScript SDK Examples
2+
3+
This directory contains example implementations of MCP clients and servers using the TypeScript SDK.
4+
5+
## Streamable HTTP - single node deployment with basic session state management
6+
7+
Multi node with stete management example will be added soon after we add support.
8+
9+
### Server (`server/simpleStreamableHttp.ts`)
10+
11+
A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:
12+
13+
- A simple `greet` tool that returns a greeting for a name
14+
- A `greeting-template` prompt that generates a greeting template
15+
- A static `greeting-resource` resource
16+
17+
#### Running the server
18+
19+
```bash
20+
npx tsx src/examples/server/simpleStreamableHttp.ts
21+
```
22+
23+
The server will start on port 3000. You can test the initialization and tool listing:
24+
25+
```bash
26+
# First initialize the server and save the session ID to a variable
27+
SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
28+
-d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \
29+
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
30+
echo "Session ID: $SESSION_ID"
31+
32+
# Then list tools using the saved session ID
33+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
34+
-H "mcp-session-id: $SESSION_ID" \
35+
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \
36+
http://localhost:3000/mcp
37+
```
38+
39+
### Client (`client/simpleStreamableHttp.ts`)
40+
41+
A client that connects to the server, initializes it, and demonstrates how to:
42+
43+
- List available tools and call the `greet` tool
44+
- List available prompts and get the `greeting-template` prompt
45+
- List available resources
46+
47+
#### Running the client
48+
49+
```bash
50+
npx tsx src/examples/client/simpleStreamableHttp.ts
51+
```
52+
53+
Make sure the server is running before starting the client.
54+
55+
## Notes
56+
57+
- These examples demonstrate the basic usage of the Streamable HTTP transport
58+
- The server manages sessions between the calls
59+
- The client handles both direct HTTP responses and SSE streaming responses
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Client } from '../../client/index.js';
2+
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
3+
import {
4+
ListToolsRequest,
5+
ListToolsResultSchema,
6+
CallToolRequest,
7+
CallToolResultSchema,
8+
ListPromptsRequest,
9+
ListPromptsResultSchema,
10+
GetPromptRequest,
11+
GetPromptResultSchema,
12+
ListResourcesRequest,
13+
ListResourcesResultSchema
14+
} from '../../types.js';
15+
16+
async function main(): Promise<void> {
17+
// Create a new client with streamable HTTP transport
18+
const client = new Client({
19+
name: 'example-client',
20+
version: '1.0.0'
21+
});
22+
const transport = new StreamableHTTPClientTransport(
23+
new URL('http://localhost:3000/mcp')
24+
);
25+
26+
// Connect the client using the transport and initialize the server
27+
await client.connect(transport);
28+
29+
console.log('Connected to MCP server');
30+
31+
// List available tools
32+
const toolsRequest: ListToolsRequest = {
33+
method: 'tools/list',
34+
params: {}
35+
};
36+
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
37+
console.log('Available tools:', toolsResult.tools);
38+
39+
// Call the 'greet' tool
40+
const greetRequest: CallToolRequest = {
41+
method: 'tools/call',
42+
params: {
43+
name: 'greet',
44+
arguments: { name: 'MCP User' }
45+
}
46+
};
47+
const greetResult = await client.request(greetRequest, CallToolResultSchema);
48+
console.log('Greeting result:', greetResult.content[0].text);
49+
50+
// List available prompts
51+
const promptsRequest: ListPromptsRequest = {
52+
method: 'prompts/list',
53+
params: {}
54+
};
55+
const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema);
56+
console.log('Available prompts:', promptsResult.prompts);
57+
58+
// Get a prompt
59+
const promptRequest: GetPromptRequest = {
60+
method: 'prompts/get',
61+
params: {
62+
name: 'greeting-template',
63+
arguments: { name: 'MCP User' }
64+
}
65+
};
66+
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
67+
console.log('Prompt template:', promptResult.messages[0].content.text);
68+
69+
// List available resources
70+
const resourcesRequest: ListResourcesRequest = {
71+
method: 'resources/list',
72+
params: {}
73+
};
74+
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
75+
console.log('Available resources:', resourcesResult.resources);
76+
77+
// Close the connection
78+
await client.close();
79+
}
80+
81+
main().catch((error: unknown) => {
82+
console.error('Error running MCP client:', error);
83+
process.exit(1);
84+
});
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import express, { Request, Response } from 'express';
2+
import { randomUUID } from 'node:crypto';
3+
import { McpServer } from '../../server/mcp.js';
4+
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
5+
import { z } from 'zod';
6+
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
7+
8+
// Create an MCP server with implementation details
9+
const server = new McpServer({
10+
name: 'simple-streamable-http-server',
11+
version: '1.0.0',
12+
});
13+
14+
// Register a simple tool that returns a greeting
15+
server.tool(
16+
'greet',
17+
'A simple greeting tool',
18+
{
19+
name: z.string().describe('Name to greet'),
20+
},
21+
async ({ name }): Promise<CallToolResult> => {
22+
return {
23+
content: [
24+
{
25+
type: 'text',
26+
text: `Hello, ${name}!`,
27+
},
28+
],
29+
};
30+
}
31+
);
32+
33+
// Register a simple prompt
34+
server.prompt(
35+
'greeting-template',
36+
'A simple greeting prompt template',
37+
{
38+
name: z.string().describe('Name to include in greeting'),
39+
},
40+
async ({ name }): Promise<GetPromptResult> => {
41+
return {
42+
messages: [
43+
{
44+
role: 'user',
45+
content: {
46+
type: 'text',
47+
text: `Please greet ${name} in a friendly manner.`,
48+
},
49+
},
50+
],
51+
};
52+
}
53+
);
54+
55+
// Create a simple resource at a fixed URI
56+
server.resource(
57+
'greeting-resource',
58+
'https://example.com/greetings/default',
59+
{ mimeType: 'text/plain' },
60+
async (): Promise<ReadResourceResult> => {
61+
return {
62+
contents: [
63+
{
64+
uri: 'https://example.com/greetings/default',
65+
text: 'Hello, world!',
66+
},
67+
],
68+
};
69+
}
70+
);
71+
72+
const app = express();
73+
app.use(express.json());
74+
75+
// Map to store transports by session ID
76+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
77+
78+
app.post('/mcp', async (req: Request, res: Response) => {
79+
console.log('Received MCP request:', req.body);
80+
try {
81+
// Check for existing session ID
82+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
83+
let transport: StreamableHTTPServerTransport;
84+
85+
if (sessionId && transports[sessionId]) {
86+
// Reuse existing transport
87+
transport = transports[sessionId];
88+
} else if (!sessionId && isInitializeRequest(req.body)) {
89+
// New initialization request
90+
transport = new StreamableHTTPServerTransport({
91+
sessionIdGenerator: () => randomUUID(),
92+
});
93+
94+
// Connect the transport to the MCP server BEFORE handling the request
95+
// so responses can flow back through the same transport
96+
await server.connect(transport);
97+
98+
// After handling the request, if we get a session ID back, store the transport
99+
await transport.handleRequest(req, res, req.body);
100+
101+
// Store the transport by session ID for future requests
102+
if (transport.sessionId) {
103+
transports[transport.sessionId] = transport;
104+
}
105+
return; // Already handled
106+
} else {
107+
// Invalid request - no session ID or not initialization request
108+
res.status(400).json({
109+
jsonrpc: '2.0',
110+
error: {
111+
code: -32000,
112+
message: 'Bad Request: No valid session ID provided',
113+
},
114+
id: null,
115+
});
116+
return;
117+
}
118+
119+
// Handle the request with existing transport - no need to reconnect
120+
// The existing transport is already connected to the server
121+
await transport.handleRequest(req, res, req.body);
122+
} catch (error) {
123+
console.error('Error handling MCP request:', error);
124+
if (!res.headersSent) {
125+
res.status(500).json({
126+
jsonrpc: '2.0',
127+
error: {
128+
code: -32603,
129+
message: 'Internal server error',
130+
},
131+
id: null,
132+
});
133+
}
134+
}
135+
});
136+
137+
// Helper function to detect initialize requests
138+
function isInitializeRequest(body: unknown): boolean {
139+
if (Array.isArray(body)) {
140+
return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize');
141+
}
142+
return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize';
143+
}
144+
145+
// Start the server
146+
const PORT = 3000;
147+
app.listen(PORT, () => {
148+
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
149+
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`);
150+
});
151+
152+
// Handle server shutdown
153+
process.on('SIGINT', async () => {
154+
console.log('Shutting down server...');
155+
await server.close();
156+
process.exit(0);
157+
});

0 commit comments

Comments
 (0)