Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions examples/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,45 @@ Most clients expect a server to be running. Start one from [`../server/README.md

## Example index

| Scenario | Description | File |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
| Scenario | Description | File |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
| Multi-server chatbot | Claude-powered chatbot that connects to two MCP servers and routes tool calls automatically. | [`src/multiServerChatbot.ts`](src/multiServerChatbot.ts) |

## Multi-server chatbot example

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.

Start both servers first (each in its own terminal):

```bash
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/weatherServer.ts
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/mathServer.ts
```

Then run the chatbot:

```bash
ANTHROPIC_API_KEY=sk-... \
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/multiServerChatbot.ts
```

Try these prompts to exercise both servers in one turn:

```
What's the weather in Tokyo?
What is 17 × 19?
Convert 100°C to Fahrenheit and give me a 3-day forecast for Paris.
```

## URL elicitation example (server + client)

Expand Down
1 change: 1 addition & 0 deletions examples/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"client": "tsx scripts/cli.ts client"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"@modelcontextprotocol/client": "workspace:^",
"ajv": "catalog:runtimeShared",
"open": "^11.0.0",
Expand Down
187 changes: 187 additions & 0 deletions examples/client/src/multiServerChatbot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* Multi-server MCP chatbot.
*
* Demonstrates how a single Anthropic-powered chatbot connects to multiple MCP
* servers simultaneously and routes each tool call to the correct server.
*
* Architecture:
* Client ──► Map<toolName, Client> ──► weather-server (:3001) or math-server (:3002)
*
* Two servers must be running before starting the chatbot:
* Terminal 1: pnpm --filter @modelcontextprotocol/examples-server exec tsx src/weatherServer.ts
* Terminal 2: pnpm --filter @modelcontextprotocol/examples-server exec tsx src/mathServer.ts
*
* Run the chatbot:
* ANTHROPIC_API_KEY=sk-... \
* pnpm --filter @modelcontextprotocol/examples-client exec tsx src/multiServerChatbot.ts
*
* Example prompts:
* "What's the weather in Tokyo?"
* "What is 17 × 19?"
* "Convert 100°C to Fahrenheit and give me a 3-day forecast for Paris."
*
* Closes #740
*/

import { createInterface } from 'node:readline';

import Anthropic from '@anthropic-ai/sdk';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

const MODEL = 'claude-opus-4-5';

const SERVER_CONFIGS = [
{ url: 'http://localhost:3001/mcp', label: 'weather-server', file: 'weatherServer.ts' },
{ url: 'http://localhost:3002/mcp', label: 'math-server', file: 'mathServer.ts' }
] as const;

async function main(): Promise<void> {
// --- Validate API key ---
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error('Error: ANTHROPIC_API_KEY is not set.');
console.error(' export ANTHROPIC_API_KEY=sk-...');
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

const anthropic = new Anthropic({ apiKey });

// --- Connect to all servers ---
const clients: Client[] = [];

for (const { url, label, file } of SERVER_CONFIGS) {
const client = new Client({ name: 'multi-server-chatbot', version: '1.0.0' });
try {
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
clients.push(client);
} catch {
console.error(`\nFailed to connect to ${label} at ${url}.`);
console.error('Is the server running? Start it with:');
console.error(` pnpm --filter @modelcontextprotocol/examples-server exec tsx src/${file}`);
await Promise.all(clients.map(c => c.close()));
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}

// --- Build routing table and aggregate tool list ---
// toolRouter maps each tool name to the client that owns it so tool calls
// can be dispatched to the right server without any manual bookkeeping.
const toolRouter = new Map<string, Client>();
const allTools: Anthropic.Tool[] = [];

for (const [i, client] of clients.entries()) {
const { tools } = await client.listTools();
const { label } = SERVER_CONFIGS[i]!;

for (const tool of tools) {
if (toolRouter.has(tool.name)) {
console.warn(`[warning] tool "${tool.name}" is on multiple servers — ${label} will be used`);
}
toolRouter.set(tool.name, client);
allTools.push({
name: tool.name,
description: tool.description ?? '',
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema
});
}
}

console.log(`\nConnected to ${clients.length} MCP servers.`);
console.log(`Tools available: ${allTools.map(t => t.name).join(', ')}`);
console.log('Type your question or "quit" to exit.\n');

// --- Clean shutdown ---
const shutdown = async () => {
console.log('\nShutting down...');
await Promise.all(clients.map(c => c.close()));
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
};

process.on('SIGINT', () => {
shutdown().catch(console.error);
});

// --- readline interface ---
const rl = createInterface({ input: process.stdin, output: process.stdout });
const prompt = (): Promise<string> => new Promise(resolve => rl.question('You: ', resolve));

// --- Chat loop ---
while (true) {
const rawInput = await prompt();
const userInput = rawInput.trim();

if (!userInput) continue;
if (userInput.toLowerCase() === 'quit' || userInput.toLowerCase() === 'exit') break;

const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userInput }];

// Agentic loop: keep going until the model stops requesting tool calls.
while (true) {
const response = await anthropic.messages.create({
model: MODEL,
max_tokens: 4096,
tools: allTools,
messages
});

if (response.stop_reason !== 'tool_use') {
// No tool calls — print the final text response.
const text = response.content
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
.map(b => b.text)
.join('');
console.log(`\nAssistant: ${text}\n`);
break;
}

// Execute all tool calls in parallel, each routed to the correct server.
const toolUseBlocks = response.content.filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use');

const toolResultContent = await Promise.all(
toolUseBlocks.map(async (block): Promise<Anthropic.ToolResultBlockParam> => {
const client = toolRouter.get(block.name);
if (!client) {
return {
type: 'tool_result',
tool_use_id: block.id,
content: `Unknown tool: ${block.name}`,
is_error: true
};
}

console.log(` [tool] ${block.name}(${JSON.stringify(block.input)})`);

const result = await client.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>
});

const text = result.content
.filter((c): c is { type: 'text'; text: string } => c.type === 'text')
.map(c => c.text)
.join('\n');

console.log(` [result] ${text}`);
return { type: 'tool_result', tool_use_id: block.id, content: text };
})
);

// Append this assistant turn and all tool results, then loop.
messages.push({ role: 'assistant', content: response.content }, { role: 'user', content: toolResultContent });
}
}

rl.close();
await Promise.all(clients.map(c => c.close()));
}

try {
await main();
} catch (error) {
console.error('Error:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
2 changes: 2 additions & 0 deletions examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pnpm tsx src/simpleStreamableHttp.ts
| Task interactive server | Task-based execution with interactive server→client requests. | [`src/simpleTaskInteractive.ts`](src/simpleTaskInteractive.ts) |
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |
| Multi-server chatbot — weather server | Stateless Streamable HTTP server on :3001 with `get_weather` and `get_forecast` tools. | [`src/weatherServer.ts`](src/weatherServer.ts) |
| Multi-server chatbot — math server | Stateless Streamable HTTP server on :3002 with `add`, `multiply`, and `convert_temperature`. | [`src/mathServer.ts`](src/mathServer.ts) |

## OAuth demo flags (Streamable HTTP server)

Expand Down
Loading
Loading