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
10 changes: 10 additions & 0 deletions docs/client-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Before you begin, it helps to have gone through the [server quickstart](./server

[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client-quickstart)

If you want one chatbot to aggregate tools from multiple MCP servers, see the multi-server variant at [`examples/client-quickstart/src/multiServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client-quickstart/src/multiServer.ts).

## Prerequisites

This quickstart assumes you have familiarity with:
Expand Down Expand Up @@ -351,6 +353,14 @@ $env:ANTHROPIC_API_KEY="your-key-here"; node build/index.js path\to\server\build
- See tool executions
- Get responses from Claude

For the multi-server variant, pass more than one server path:

```bash
ANTHROPIC_API_KEY=your-key-here node build/multiServer.js ./server-a/build/index.js ./server-b/build/index.js
```

That variant qualifies tool names per server (for example `weather__forecast`) so the chatbot can expose one combined tool list to Claude while still routing each tool call back to the correct MCP client.

## What's happening under the hood

When you submit a query:
Expand Down
22 changes: 12 additions & 10 deletions examples/client-quickstart/package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
{
"name": "@modelcontextprotocol/examples-client-quickstart",
"private": true,
"version": "2.0.0-alpha.0",
"type": "module",
{
"name": "@modelcontextprotocol/examples-client-quickstart",
"private": true,
"version": "2.0.0-alpha.0",
"type": "module",
"bin": {
"mcp-client-cli": "./build/index.js"
},
"scripts": {
"build": "tsc",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"@modelcontextprotocol/client": "workspace:^"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"@modelcontextprotocol/client": "workspace:^"
},
"devDependencies": {
"@types/node": "^24.10.1",
"typescript": "catalog:devTools"
"typescript": "catalog:devTools",
"vitest": "catalog:devTools"
}
}
222 changes: 222 additions & 0 deletions examples/client-quickstart/src/multiServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import Anthropic from '@anthropic-ai/sdk';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import readline from 'readline/promises';

import {
buildQualifiedToolDefinitions,
createUniqueServerLabel
} from './multiServerHelpers.js';

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

type ConnectedServer = {
client: Client;
label: string;
scriptPath: string;
};

type ToolBinding = {
client: Client;
originalToolName: string;
qualifiedToolName: string;
serverLabel: string;
};

class MultiServerMCPClient {
private readonly servers: ConnectedServer[] = [];
private readonly toolBindings = new Map<string, ToolBinding>();
private readonly tools: Anthropic.Tool[] = [];
private _anthropic: Anthropic | null = null;

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

async connectToServers(serverScriptPaths: string[]): Promise<void> {
const usedLabels = new Set<string>();

for (const serverScriptPath of serverScriptPaths) {
const label = createUniqueServerLabel(serverScriptPath, usedLabels);
const client = await this.connectSingleServer(label, serverScriptPath);
this.servers.push({ client, label, scriptPath: serverScriptPath });

const toolsResult = await client.listTools();
const qualifiedTools = buildQualifiedToolDefinitions(label, toolsResult.tools);

for (const tool of qualifiedTools) {
this.tools.push(tool.anthropicTool);
this.toolBindings.set(tool.qualifiedToolName, {
client,
originalToolName: tool.originalToolName,
qualifiedToolName: tool.qualifiedToolName,
serverLabel: label
});
}

console.log(
`Connected ${label} with tools: ${qualifiedTools.map((tool) => tool.qualifiedToolName).join(', ')}`
);
}
}

private async connectSingleServer(label: string, serverScriptPath: string): Promise<Client> {
const isJs = serverScriptPath.endsWith('.js');
const isPy = serverScriptPath.endsWith('.py');

if (!isJs && !isPy) {
throw new Error(`Server script must be a .js or .py file: ${serverScriptPath}`);
}

const command = isPy
? process.platform === 'win32'
? 'python'
: 'python3'
: process.execPath;

const client = new Client({
name: `mcp-client-cli-${label}`,
version: '1.0.0'
});
const transport = new StdioClientTransport({ command, args: [serverScriptPath] });

await client.connect(transport);
return client;
}

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

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

const finalText: string[] = [];

for (const content of response.content) {
if (content.type === 'text') {
finalText.push(content.text);
continue;
}

if (content.type !== 'tool_use') {
continue;
}

const binding = this.toolBindings.get(content.name);
if (!binding) {
throw new Error(`Unknown qualified tool name: ${content.name}`);
}

const toolArgs = content.input as Record<string, unknown> | undefined;
const result = await binding.client.callTool({
name: binding.originalToolName,
arguments: toolArgs
});

finalText.push(
`[Calling ${binding.originalToolName} on ${binding.serverLabel} with args ${JSON.stringify(toolArgs)}]`
);

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

messages.push({
role: 'assistant',
content: response.content
});
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: content.id,
content: toolResultText
}
]
});

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

finalText.push(followUp.content[0]?.type === 'text' ? followUp.content[0].text : '');
}

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

async chatLoop(): Promise<void> {
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.');

while (true) {
const message = await rl.question('\nQuery: ');
if (message.toLowerCase() === 'quit') {
break;
}

const response = await this.processQuery(message);
console.log(`\n${response}`);
}
} finally {
rl.close();
}
}

async cleanup(): Promise<void> {
await Promise.allSettled(this.servers.map(({ client }) => client.close()));
}
}

async function main(): Promise<void> {
const serverScriptPaths = process.argv.slice(2);
if (serverScriptPaths.length === 0) {
console.log('Usage: node build/multiServer.js <path_to_server_script> [more_paths...]');
return;
}

const mcpClient = new MultiServerMCPClient();
try {
await mcpClient.connectToServers(serverScriptPaths);

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

await mcpClient.chatLoop();
} catch (error) {
console.error('Error:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
} finally {
await mcpClient.cleanup();
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
}
}

await main();
73 changes: 73 additions & 0 deletions examples/client-quickstart/src/multiServerHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import path from 'node:path';

import Anthropic from '@anthropic-ai/sdk';

type ToolLike = {
name: string;
description?: string | null;
inputSchema: unknown;
};

export type QualifiedToolDefinition = {
anthropicTool: Anthropic.Tool;
originalToolName: string;
qualifiedToolName: string;
serverLabel: string;
};

export function sanitizeServerLabel(serverScriptPath: string): string {
const fileName = path.basename(serverScriptPath, path.extname(serverScriptPath));
const normalized = fileName
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');

if (!normalized) {
return 'server';
}

return /^[0-9]/.test(normalized) ? `server_${normalized}` : normalized;
}

export function createUniqueServerLabel(serverScriptPath: string, usedLabels: Set<string>): string {
const baseLabel = sanitizeServerLabel(serverScriptPath);
let candidate = baseLabel;
let suffix = 2;

while (usedLabels.has(candidate)) {
candidate = `${baseLabel}_${suffix}`;
suffix += 1;
}

usedLabels.add(candidate);
return candidate;
}

export function buildQualifiedToolName(serverLabel: string, toolName: string): string {
return `${serverLabel}__${toolName}`;
}

export function buildQualifiedToolDefinitions(
serverLabel: string,
tools: ToolLike[]
): QualifiedToolDefinition[] {
return tools.map((tool) => {
const qualifiedToolName = buildQualifiedToolName(serverLabel, tool.name);
const descriptionPrefix = `[server:${serverLabel}] Original MCP tool: ${tool.name}.`;
const description = tool.description
? `${descriptionPrefix} ${tool.description}`
: descriptionPrefix;

return {
originalToolName: tool.name,
qualifiedToolName,
serverLabel,
anthropicTool: {
name: qualifiedToolName,
description,
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema
}
};
});
}
Loading
Loading