Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"updateInternalDependencies": "patch",
"ignore": [
"@modelcontextprotocol/examples-client",
"@modelcontextprotocol/examples-client-multi-server",
"@modelcontextprotocol/examples-client-quickstart",
"@modelcontextprotocol/examples-server",
"@modelcontextprotocol/examples-server-quickstart",
Expand Down
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pnpm-lock.yaml
# Ignore generated files
src/spec.types.ts

# Quickstart examples uses 2-space indent to match ecosystem conventions
# Standalone examples use 2-space indent to match ecosystem conventions
examples/client-multi-server/
examples/client-quickstart/
examples/server-quickstart/
1 change: 1 addition & 0 deletions examples/client-multi-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
82 changes: 82 additions & 0 deletions examples/client-multi-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Multi-Server MCP Client Example

A CLI chatbot that connects to multiple MCP servers simultaneously, aggregates their tools, and routes tool calls to the correct server. This is the TypeScript equivalent of the [Python SDK's simple-chatbot example](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/clients/simple-chatbot).

## Prerequisites

- Node.js 20+
- An [Anthropic API key](https://console.anthropic.com/)

## Quick Start

```bash
# Install dependencies (from repo root)
pnpm install

# Set your API key
export ANTHROPIC_API_KEY=your-api-key-here

# Run with the default servers.json config
cd examples/client-multi-server
npx tsx src/index.ts
```

## Configuration

Servers are configured via a JSON file (default: `servers.json` in the working directory). Pass a custom path as the first argument:

```bash
npx tsx src/index.ts /path/to/my-servers.json
```

The config file uses the same format as Claude Desktop and other MCP clients:

```json
{
"mcpServers": {
"everything": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"]
},
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
}
}
}
```

Each entry under `mcpServers` defines a server to connect to via stdio:

- `command`: the executable to run
- `args`: command-line arguments (optional)
- `env`: additional environment variables (optional, merged with the current environment)

## How It Works

1. Reads the server config and connects to each MCP server in sequence
2. Discovers tools from every connected server and builds a unified tool list
3. Maintains a mapping from each tool name to its originating server
4. Sends the full tool list to Claude with each request
5. When Claude calls a tool, routes the call to the correct server
6. Supports multi-step tool use (agentic loop) where Claude can chain multiple tool calls

## Usage

```
$ npx tsx src/index.ts
Connecting to server: everything...
Connected to everything with tools: echo, add, ...

Total tools available: 12

Multi-Server MCP Client Started!
Type your queries or "quit" to exit.

Query: What tools do you have access to?

I have access to 12 tools from the "everything" server...

Query: quit
Disconnecting from everything...
```
21 changes: 21 additions & 0 deletions examples/client-multi-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@modelcontextprotocol/examples-client-multi-server",
"private": true,
"version": "2.0.0-alpha.0",
"type": "module",
"bin": {
"mcp-multi-server-client": "./build/index.js"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"@modelcontextprotocol/client": "workspace:^"
},
"devDependencies": {
"@types/node": "^24.10.1",
"typescript": "catalog:devTools"
}
}
8 changes: 8 additions & 0 deletions examples/client-multi-server/servers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"everything": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"]
}
}
}
218 changes: 218 additions & 0 deletions examples/client-multi-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import readline from 'readline/promises';

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

const ANTHROPIC_MODEL = 'claude-sonnet-4-5';

interface ServerConfig {
command: string;
args?: string[];
env?: Record<string, string>;
}

interface ServersConfig {
mcpServers: Record<string, ServerConfig>;
}

class MultiServerClient {
private servers: Map<string, Client> = new Map();
private toolToServer: Map<string, { serverName: string; originalName: string }> = new Map();
private _anthropic: Anthropic | null = null;
private tools: Anthropic.Tool[] = [];

private get anthropic(): Anthropic {
return this._anthropic ??= new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
}

async connectToServers(configPath: string) {
const raw = readFileSync(resolve(configPath), 'utf-8');
const config: ServersConfig = JSON.parse(raw);

for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
console.log(`Connecting to server: ${name}...`);
try {
const transport = new StdioClientTransport({
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env
? { ...process.env as Record<string, string>, ...serverConfig.env }
: undefined,
});
const client = new Client({ name: `multi-server-client-${name}`, version: '1.0.0' });
await client.connect(transport);
this.servers.set(name, client);

// Discover tools from this server
const toolsResult = await client.listTools();
for (const tool of toolsResult.tools) {
const prefixedName = `${name}__${tool.name}`;
if (this.toolToServer.has(prefixedName)) {
console.warn(
` Warning: tool "${tool.name}" from server "${name}" collides with an existing tool.`
);
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
}
this.toolToServer.set(prefixedName, { serverName: name, originalName: tool.name });
this.tools.push({
name: prefixedName,
description: `[${name}] ${tool.description ?? ''}`,
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
});
}
console.log(
` Connected to ${name} with tools: ${toolsResult.tools.map((t) => t.name).join(', ')}`
);
} catch (e) {
console.error(` Failed to connect to ${name}:`, e);
throw e;
}
}

console.log(`\nTotal tools available: ${this.tools.length}`);
}

async processQuery(query: string) {
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: query }];

// Agentic loop: keep processing until the model stops issuing tool calls
let response = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
tools: this.tools,
});

const finalText: string[] = [];

while (response.stop_reason === 'tool_use') {
const assistantContent = response.content;
messages.push({ role: 'assistant', content: assistantContent });

const toolResults: Anthropic.ToolResultBlockParam[] = [];

for (const block of assistantContent) {
if (block.type === 'text') {
finalText.push(block.text);
} else if (block.type === 'tool_use') {
const mapping = this.toolToServer.get(block.name);
if (!mapping) {
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: `Error: unknown tool "${block.name}"`,
is_error: true,
});
continue;
}
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

const { serverName, originalName } = mapping;
const client = this.servers.get(serverName)!;
console.log(` [Calling ${originalName} on server "${serverName}"]`);

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

const resultText = result.content
.filter((c) => c.type === 'text')
.map((c) => c.text)
.join('\n');

toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: resultText,
});
} catch (e) {
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: `Error executing tool: ${e}`,
is_error: true,
});
}
}
}

messages.push({ role: 'user', content: toolResults });

response = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
tools: this.tools,
});
}

// Collect any remaining text from the final response
for (const block of response.content) {
if (block.type === 'text') {
finalText.push(block.text);
}
}

return finalText.join('\n');
}

async chatLoop() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

try {
console.log('\nMulti-Server MCP Client Started!');
console.log('Type your queries or "quit" to exit.\n');

while (true) {
const message = await rl.question('Query: ');
if (message.toLowerCase() === 'quit') {
break;
}
const response = await this.processQuery(message);
console.log('\n' + response + '\n');
}
} finally {
rl.close();
}
}

async cleanup() {
for (const [name, client] of this.servers) {
console.log(`Disconnecting from ${name}...`);
await client.close();
}
}
}

async function main() {
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
const configPath = process.argv[2] ?? 'servers.json';

const mcpClient = new MultiServerClient();
try {
await mcpClient.connectToServers(configPath);

const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.log(
'\nNo ANTHROPIC_API_KEY found. To chat with these tools via Claude, set your API key:'
+ '\n export ANTHROPIC_API_KEY=your-api-key-here'
);
return;
}

await mcpClient.chatLoop();
} catch (e) {
console.error('Error:', e);
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
process.exit(1);
} finally {
await mcpClient.cleanup();
process.exit(0);
}
}

main();
23 changes: 23 additions & 0 deletions examples/client-multi-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"],
"@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"],
"@modelcontextprotocol/core": [
"./node_modules/@modelcontextprotocol/client/node_modules/@modelcontextprotocol/core/src/index.ts"
]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading