diff --git a/.changeset/config.json b/.changeset/config.json index eb43bdc7f..766607786 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -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", diff --git a/.prettierignore b/.prettierignore index 1aecab1f5..f6b406996 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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/ diff --git a/examples/client-multi-server/.gitignore b/examples/client-multi-server/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/examples/client-multi-server/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/client-multi-server/README.md b/examples/client-multi-server/README.md new file mode 100644 index 000000000..fd48e4b41 --- /dev/null +++ b/examples/client-multi-server/README.md @@ -0,0 +1,33 @@ +# Multi-Server Routing Example + +Minimal example showing how to connect to multiple MCP servers and route tool calls to the correct one. + +Spawns two in-repo servers (`server-quickstart` and `mcpServerOutputSchema`), discovers their tools, builds a prefixed routing table, and calls one tool on each server to demonstrate the routing. + +No API key required. No external config file needed. + +## Quick Start + +```bash +# Install dependencies (from repo root) +pnpm install + +# Run the example +npx tsx examples/client-multi-server/src/index.ts +``` + +## How It Works + +1. Spawns each server as a child process via `StdioClientTransport` +2. Connects a `Client` to each and calls `listTools()` to discover available tools +3. Builds a routing map that prefixes each tool name with its server name (e.g. `weather-nws__get-alerts`) to avoid collisions +4. Calls one tool on each server to prove routing works +5. Cleans up all connections + +## Adapting This Pattern + +To route tool calls in your own multi-server setup: + +- Prefix tool names with the server name when presenting them to an LLM +- When the LLM calls a prefixed tool, strip the prefix and forward to the correct server +- Check for collisions if multiple servers expose tools with the same name diff --git a/examples/client-multi-server/package.json b/examples/client-multi-server/package.json new file mode 100644 index 000000000..d385a6a71 --- /dev/null +++ b/examples/client-multi-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "@modelcontextprotocol/examples-client-multi-server", + "private": true, + "version": "2.0.0-alpha.0", + "type": "module", + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/client": "workspace:^" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "catalog:devTools" + } +} diff --git a/examples/client-multi-server/src/index.ts b/examples/client-multi-server/src/index.ts new file mode 100644 index 000000000..25190bcd1 --- /dev/null +++ b/examples/client-multi-server/src/index.ts @@ -0,0 +1,112 @@ +/** + * Minimal multi-server routing example. + * + * Spawns two in-repo MCP servers (server-quickstart and mcpServerOutputSchema), + * connects a Client to each, discovers their tools, and routes tool calls to + * the correct server based on which one registered the tool. + * + * Run: npx tsx examples/client-multi-server/src/index.ts + */ + +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { Client, StdioClientTransport } from '@modelcontextprotocol/client'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Each server entry: a name and the path to its stdio entrypoint. +const servers = [ + { + name: 'weather-nws', + script: resolve(__dirname, '../../server-quickstart/src/index.ts'), + }, + { + name: 'weather-structured', + script: resolve(__dirname, '../../server/src/mcpServerOutputSchema.ts'), + }, +]; + +// Maps prefixed tool name -> { client, originalName, serverName } +const toolRouter = new Map< + string, + { client: Client; originalName: string; serverName: string } +>(); + +async function main() { + const clients: Client[] = []; + + // 1. Connect to each server and discover tools + for (const { name, script } of servers) { + const transport = new StdioClientTransport({ + command: process.execPath, + args: ['--import', 'tsx', script], + }); + const client = new Client({ name: `router-${name}`, version: '1.0.0' }); + await client.connect(transport); + clients.push(client); + + const { tools } = await client.listTools(); + for (const tool of tools) { + const prefixed = `${name}__${tool.name}`; + toolRouter.set(prefixed, { + client, + originalName: tool.name, + serverName: name, + }); + } + + console.log(`[${name}] connected, tools: ${tools.map((t) => t.name).join(', ')}`); + } + + console.log(`\nRouting table (${toolRouter.size} tools):`); + for (const [prefixed, { serverName, originalName }] of toolRouter) { + console.log(` ${prefixed} -> ${serverName} (${originalName})`); + } + + // 2. Demonstrate routing: call one tool from each server + console.log('\n--- Routing demo ---\n'); + + // Call get-alerts from weather-nws (server-quickstart) + const alertsKey = 'weather-nws__get-alerts'; + const alertsRoute = toolRouter.get(alertsKey); + if (alertsRoute) { + console.log(`Calling ${alertsKey} ...`); + const result = await alertsRoute.client.callTool({ + name: alertsRoute.originalName, + arguments: { state: 'CA' }, + }); + const text = result.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n'); + console.log(`Result: ${text.slice(0, 200)}...\n`); + } + + // Call get_weather from weather-structured (mcpServerOutputSchema) + const weatherKey = 'weather-structured__get_weather'; + const weatherRoute = toolRouter.get(weatherKey); + if (weatherRoute) { + console.log(`Calling ${weatherKey} ...`); + const result = await weatherRoute.client.callTool({ + name: weatherRoute.originalName, + arguments: { city: 'Seattle', country: 'US' }, + }); + const text = result.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n'); + console.log(`Result: ${text.slice(0, 200)}...\n`); + } + + // 3. Cleanup + for (const client of clients) { + await client.close(); + } + console.log('All servers disconnected.'); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/examples/client-multi-server/tsconfig.json b/examples/client-multi-server/tsconfig.json new file mode 100644 index 000000000..9c229e5fe --- /dev/null +++ b/examples/client-multi-server/tsconfig.json @@ -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"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 899586750..710311bf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,6 +318,19 @@ importers: specifier: catalog:devTools version: 0.18.4(@typescript/native-preview@7.0.0-dev.20260327.2)(typescript@5.9.3) + examples/client-multi-server: + dependencies: + '@modelcontextprotocol/client': + specifier: workspace:^ + version: link:../../packages/client + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.12.0 + typescript: + specifier: catalog:devTools + version: 5.9.3 + examples/client-quickstart: dependencies: '@anthropic-ai/sdk':