Skip to content

Commit c645ec9

Browse files
committed
streamable client and server examples
1 parent 84ca647 commit c645ec9

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed

src/examples/README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 Examples
6+
7+
### List Tool Request Example
8+
9+
Using `curl` to list available tools:
10+
11+
```bash
12+
# First initialize the server and save the session ID to a variable
13+
SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
14+
-d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \
15+
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
16+
echo "Session ID: $SESSION_ID"
17+
18+
# Then list tools using the saved session ID
19+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
20+
-H "mcp-session-id: $SESSION_ID" \
21+
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \
22+
http://localhost:3000/mcp
23+
```
24+
25+
Using the TypeScript client (session management is handled automatically):
26+
27+
```typescript
28+
const toolsRequest = { method: 'tools/list', params: {} };
29+
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
30+
console.log('Available tools:', toolsResult.tools);
31+
```
32+
33+
### Call Tool Request Example
34+
35+
Using `curl` to call a tool:
36+
37+
```bash
38+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
39+
-H "mcp-session-id: $SESSION_ID" \
40+
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"greet","arguments":{"name":"User"}},"id":"3"}' \
41+
http://localhost:3000/mcp
42+
```
43+
44+
Using the TypeScript client:
45+
46+
```typescript
47+
const greetRequest = {
48+
method: 'tools/call',
49+
params: {
50+
name: 'greet',
51+
arguments: { name: 'MCP User' }
52+
}
53+
};
54+
const greetResult = await client.request(greetRequest, CallToolResultSchema);
55+
```
56+
57+
### Get Prompt Request Example
58+
59+
Using `curl` to get a prompt:
60+
61+
```bash
62+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
63+
-H "mcp-session-id: $SESSION_ID" \
64+
-d '{"jsonrpc":"2.0","method":"prompts/get","params":{"name":"greeting-template","arguments":{"name":"User"}},"id":"4"}' \
65+
http://localhost:3000/mcp
66+
```
67+
68+
Using the TypeScript client:
69+
70+
```typescript
71+
const promptRequest = {
72+
method: 'prompts/get',
73+
params: {
74+
name: 'greeting-template',
75+
arguments: { name: 'MCP User' }
76+
}
77+
};
78+
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
79+
```
80+
81+
### Server (`server/simpleStreamableHttp.ts`)
82+
83+
A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:
84+
85+
- A simple `greet` tool that returns a greeting for a name
86+
- A `greeting-template` prompt that generates a greeting template
87+
- A static `greeting-resource` resource
88+
89+
#### Running the server
90+
91+
```bash
92+
npx tsx src/examples/server/simpleStreamableHttp.ts
93+
```
94+
95+
The server will start on port 3000. You can test the initialization with:
96+
97+
```bash
98+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
99+
-d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \
100+
http://localhost:3000/mcp
101+
```
102+
103+
### Client (`client/simpleStreamableHttpClient.ts`)
104+
105+
A client that connects to the server, initializes it, and demonstrates how to:
106+
107+
- List available tools and call the `greet` tool
108+
- List available prompts and get the `greeting-template` prompt
109+
- List available resources
110+
111+
#### Running the client
112+
113+
```bash
114+
npx tsx src/examples/client/simpleStreamableHttpClient.ts
115+
```
116+
117+
Make sure the server is running before starting the client.
118+
119+
## Notes
120+
121+
- These examples demonstrate the basic usage of the Streamable HTTP transport
122+
- The server manages sessions for stateful connections
123+
- 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: any): boolean {
139+
if (Array.isArray(body)) {
140+
return body.some(msg => msg.method === 'initialize');
141+
}
142+
return 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)