Skip to content

Commit 3eacbab

Browse files
feat(examples): add multi-server chatbot example (closes #740)
Adds three files demonstrating how one Anthropic-powered chatbot can connect to multiple MCP servers simultaneously and route tool calls to the correct server using a Map<toolName, Client> routing table. - examples/server/src/weatherServer.ts — stateless Streamable HTTP server on :3001 with get_weather and get_forecast tools - examples/server/src/mathServer.ts — stateless Streamable HTTP server on :3002 with add, multiply, and convert_temperature tools - examples/client/src/multiServerChatbot.ts — connects to both servers, builds the routing table, runs an interactive Anthropic SDK chat loop, and dispatches parallel tool calls to the correct server Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5fc42e9 commit 3eacbab

7 files changed

Lines changed: 493 additions & 12 deletions

File tree

examples/client/README.md

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,45 @@ Most clients expect a server to be running. Start one from [`../server/README.md
2424

2525
## Example index
2626

27-
| Scenario | Description | File |
28-
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
29-
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
30-
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
31-
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
32-
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
33-
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
34-
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
35-
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
36-
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
37-
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
38-
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
27+
| Scenario | Description | File |
28+
| --------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
29+
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
30+
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
31+
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
32+
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
33+
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
34+
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
35+
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
36+
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
37+
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
38+
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
39+
| Multi-server chatbot | Claude-powered chatbot that connects to two MCP servers and routes tool calls automatically. | [`src/multiServerChatbot.ts`](src/multiServerChatbot.ts) |
40+
41+
## Multi-server chatbot example
42+
43+
Shows how one chatbot client can connect to multiple MCP servers simultaneously and route tool calls to the correct server based on which server registered the tool.
44+
45+
Start both servers first (each in its own terminal):
46+
47+
```bash
48+
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/weatherServer.ts
49+
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/mathServer.ts
50+
```
51+
52+
Then run the chatbot:
53+
54+
```bash
55+
ANTHROPIC_API_KEY=sk-... \
56+
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/multiServerChatbot.ts
57+
```
58+
59+
Try these prompts to exercise both servers in one turn:
60+
61+
```
62+
What's the weather in Tokyo?
63+
What is 17 × 19?
64+
Convert 100°C to Fahrenheit and give me a 3-day forecast for Paris.
65+
```
3966

4067
## URL elicitation example (server + client)
4168

examples/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"client": "tsx scripts/cli.ts client"
3333
},
3434
"dependencies": {
35+
"@anthropic-ai/sdk": "^0.74.0",
3536
"@modelcontextprotocol/client": "workspace:^",
3637
"ajv": "catalog:runtimeShared",
3738
"open": "^11.0.0",
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Multi-server MCP chatbot.
3+
*
4+
* Demonstrates how a single Anthropic-powered chatbot connects to multiple MCP
5+
* servers simultaneously and routes each tool call to the correct server.
6+
*
7+
* Architecture:
8+
* Client ──► Map<toolName, Client> ──► weather-server (:3001) or math-server (:3002)
9+
*
10+
* Two servers must be running before starting the chatbot:
11+
* Terminal 1: pnpm --filter @modelcontextprotocol/examples-server exec tsx src/weatherServer.ts
12+
* Terminal 2: pnpm --filter @modelcontextprotocol/examples-server exec tsx src/mathServer.ts
13+
*
14+
* Run the chatbot:
15+
* ANTHROPIC_API_KEY=sk-... \
16+
* pnpm --filter @modelcontextprotocol/examples-client exec tsx src/multiServerChatbot.ts
17+
*
18+
* Example prompts:
19+
* "What's the weather in Tokyo?"
20+
* "What is 17 × 19?"
21+
* "Convert 100°C to Fahrenheit and give me a 3-day forecast for Paris."
22+
*
23+
* Closes #740
24+
*/
25+
26+
import { createInterface } from 'node:readline';
27+
28+
import Anthropic from '@anthropic-ai/sdk';
29+
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
30+
31+
const MODEL = 'claude-opus-4-5';
32+
33+
const SERVER_CONFIGS = [
34+
{ url: 'http://localhost:3001/mcp', label: 'weather-server', file: 'weatherServer.ts' },
35+
{ url: 'http://localhost:3002/mcp', label: 'math-server', file: 'mathServer.ts' }
36+
] as const;
37+
38+
async function main(): Promise<void> {
39+
// --- Validate API key ---
40+
const apiKey = process.env.ANTHROPIC_API_KEY;
41+
if (!apiKey) {
42+
console.error('Error: ANTHROPIC_API_KEY is not set.');
43+
console.error(' export ANTHROPIC_API_KEY=sk-...');
44+
// eslint-disable-next-line unicorn/no-process-exit
45+
process.exit(1);
46+
}
47+
48+
const anthropic = new Anthropic({ apiKey });
49+
50+
// --- Connect to all servers ---
51+
const clients: Client[] = [];
52+
53+
for (const { url, label, file } of SERVER_CONFIGS) {
54+
const client = new Client({ name: 'multi-server-chatbot', version: '1.0.0' });
55+
try {
56+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
57+
clients.push(client);
58+
} catch {
59+
console.error(`\nFailed to connect to ${label} at ${url}.`);
60+
console.error('Is the server running? Start it with:');
61+
console.error(` pnpm --filter @modelcontextprotocol/examples-server exec tsx src/${file}`);
62+
await Promise.all(clients.map(c => c.close()));
63+
// eslint-disable-next-line unicorn/no-process-exit
64+
process.exit(1);
65+
}
66+
}
67+
68+
// --- Build routing table and aggregate tool list ---
69+
// toolRouter maps each tool name to the client that owns it so tool calls
70+
// can be dispatched to the right server without any manual bookkeeping.
71+
const toolRouter = new Map<string, Client>();
72+
const allTools: Anthropic.Tool[] = [];
73+
74+
for (const [i, client] of clients.entries()) {
75+
const { tools } = await client.listTools();
76+
const { label } = SERVER_CONFIGS[i]!;
77+
78+
for (const tool of tools) {
79+
if (toolRouter.has(tool.name)) {
80+
console.warn(`[warning] tool "${tool.name}" is on multiple servers — ${label} will be used`);
81+
}
82+
toolRouter.set(tool.name, client);
83+
allTools.push({
84+
name: tool.name,
85+
description: tool.description ?? '',
86+
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema
87+
});
88+
}
89+
}
90+
91+
console.log(`\nConnected to ${clients.length} MCP servers.`);
92+
console.log(`Tools available: ${allTools.map(t => t.name).join(', ')}`);
93+
console.log('Type your question or "quit" to exit.\n');
94+
95+
// --- Clean shutdown ---
96+
const shutdown = async () => {
97+
console.log('\nShutting down...');
98+
await Promise.all(clients.map(c => c.close()));
99+
// eslint-disable-next-line unicorn/no-process-exit
100+
process.exit(0);
101+
};
102+
103+
process.on('SIGINT', () => {
104+
shutdown().catch(console.error);
105+
});
106+
107+
// --- readline interface ---
108+
const rl = createInterface({ input: process.stdin, output: process.stdout });
109+
const prompt = (): Promise<string> => new Promise(resolve => rl.question('You: ', resolve));
110+
111+
// --- Chat loop ---
112+
while (true) {
113+
const rawInput = await prompt();
114+
const userInput = rawInput.trim();
115+
116+
if (!userInput) continue;
117+
if (userInput.toLowerCase() === 'quit' || userInput.toLowerCase() === 'exit') break;
118+
119+
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userInput }];
120+
121+
// Agentic loop: keep going until the model stops requesting tool calls.
122+
while (true) {
123+
const response = await anthropic.messages.create({
124+
model: MODEL,
125+
max_tokens: 4096,
126+
tools: allTools,
127+
messages
128+
});
129+
130+
if (response.stop_reason !== 'tool_use') {
131+
// No tool calls — print the final text response.
132+
const text = response.content
133+
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
134+
.map(b => b.text)
135+
.join('');
136+
console.log(`\nAssistant: ${text}\n`);
137+
break;
138+
}
139+
140+
// Execute all tool calls in parallel, each routed to the correct server.
141+
const toolUseBlocks = response.content.filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use');
142+
143+
const toolResultContent = await Promise.all(
144+
toolUseBlocks.map(async (block): Promise<Anthropic.ToolResultBlockParam> => {
145+
const client = toolRouter.get(block.name);
146+
if (!client) {
147+
return {
148+
type: 'tool_result',
149+
tool_use_id: block.id,
150+
content: `Unknown tool: ${block.name}`,
151+
is_error: true
152+
};
153+
}
154+
155+
console.log(` [tool] ${block.name}(${JSON.stringify(block.input)})`);
156+
157+
const result = await client.callTool({
158+
name: block.name,
159+
arguments: block.input as Record<string, unknown>
160+
});
161+
162+
const text = result.content
163+
.filter((c): c is { type: 'text'; text: string } => c.type === 'text')
164+
.map(c => c.text)
165+
.join('\n');
166+
167+
console.log(` [result] ${text}`);
168+
return { type: 'tool_result', tool_use_id: block.id, content: text };
169+
})
170+
);
171+
172+
// Append this assistant turn and all tool results, then loop.
173+
messages.push({ role: 'assistant', content: response.content }, { role: 'user', content: toolResultContent });
174+
}
175+
}
176+
177+
rl.close();
178+
await Promise.all(clients.map(c => c.close()));
179+
}
180+
181+
try {
182+
await main();
183+
} catch (error) {
184+
console.error('Error:', error);
185+
// eslint-disable-next-line unicorn/no-process-exit
186+
process.exit(1);
187+
}

examples/server/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pnpm tsx src/simpleStreamableHttp.ts
3939
| Task interactive server | Task-based execution with interactive server→client requests. | [`src/simpleTaskInteractive.ts`](src/simpleTaskInteractive.ts) |
4040
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
4141
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |
42+
| Multi-server chatbot — weather server | Stateless Streamable HTTP server on :3001 with `get_weather` and `get_forecast` tools. | [`src/weatherServer.ts`](src/weatherServer.ts) |
43+
| Multi-server chatbot — math server | Stateless Streamable HTTP server on :3002 with `add`, `multiply`, and `convert_temperature`. | [`src/mathServer.ts`](src/mathServer.ts) |
4244

4345
## OAuth demo flags (Streamable HTTP server)
4446

0 commit comments

Comments
 (0)