Skip to content

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 11 commits into from
Apr 9, 2025
Merged
59 changes: 59 additions & 0 deletions src/examples/README.md
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
84 changes: 84 additions & 0 deletions src/examples/client/simpleStreamableHttp.ts
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);
});
157 changes: 157 additions & 0 deletions src/examples/server/simpleStreamableHttp.ts
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;
}
Comment on lines +98 to +104
Copy link
Member

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:

  1. Client sends initialization request to server
  2. Server handles the request, assigns a session ID, and sends an initialization response
  3. Before line 102 is reached, the client sends another request
  4. Server can't find the session matching its ID

This might be something we have to fix in the interfaces we offer—for example, maybe a callback like onsessioncreated or something?

Copy link
Contributor Author

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

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);
});