From f7d44a9c4471bc02843ee1bd48f07c0517f170ee Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 12 May 2025 16:00:50 -0700 Subject: [PATCH] sample: Add a sample using a group of MCP clients. This is introduced as the first step of adding dedicated functionality for managing client groups. It demonstrate what the UX is like right now, and will evolve and hopefully get simpler as we introduce dedicated client group functionality. --- src/examples/client/clientGroupSample.ts | 129 +++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/examples/client/clientGroupSample.ts diff --git a/src/examples/client/clientGroupSample.ts b/src/examples/client/clientGroupSample.ts new file mode 100644 index 000000000..a1d2ed02f --- /dev/null +++ b/src/examples/client/clientGroupSample.ts @@ -0,0 +1,129 @@ +import { Tool } from "../../types.js"; +import { Client } from "../../client/index.js"; +import { InMemoryTransport } from "../../inMemory.js"; +import { McpServer, ToolCallback } from "../../server/mcp.js"; +import { Transport } from "../../shared/transport.js"; + +async function main(): Promise { + console.log("MCP Client Group Example"); + console.log("============================"); + + const clientTransports = await spinUpServers(); + + const client1 = new Client({ + name: "client-1", + version: "1.0.0", + }); + client1.connect(clientTransports[0]); + + const client2 = new Client({ + name: "client-2", + version: "1.0.0", + }); + client2.connect(clientTransports[1]); + + const client3 = new Client({ + name: "client-3", + version: "1.0.0", + }); + client3.connect(clientTransports[2]); + + const allClients = [client1, client2, client3]; + const toolToClient: { [key: string]: Client } = {}; + const allTools = []; + + for (const client of allClients) { + for (const tool of (await client.listTools()).tools) { + if (toolToClient[tool.name]) { + console.warn( + `Tool name: ${tool.name} is available on multiple servers, picking an arbitrary one`, + ); + } + toolToClient[tool.name] = client; + allTools.push(tool); + } + } + + const allResources = []; + allResources.push(...(await client1.listResources()).resources); + allResources.push(...(await client2.listResources()).resources); + allResources.push(...(await client3.listResources()).resources); + + const toolName = simulatePromptModel(allTools); + + console.log(`Invoking tool: ${toolName}`); + const toolResult = await toolToClient[toolName].callTool({ + name: toolName, + }); + + console.log(toolResult); + + for (const client of allClients) { + await client.close(); + } +} + +// Start the example +main().catch((error: unknown) => { + console.error("Error running MCP Client Group example:", error); + process.exit(1); +}); + +async function spinUpServer( + name: string, + cb: ToolCallback, +): Promise { + const server = new McpServer({ + name: name, + version: "1.0.0", + }); + + server.tool(name, cb); + + server.resource("greeting", "greeting://hello", async (uri) => ({ + contents: [ + { + uri: uri.href, + text: `Hello from ${name}!`, + }, + ], + })); + + const transports = InMemoryTransport.createLinkedPair(); + await server.connect(transports[0]); + + return transports[1]; +} + +async function spinUpServers(): Promise { + const clientTransports = []; + clientTransports.push( + await spinUpServer("ping", async () => ({ + content: [{ type: "text", text: "pong" }], + })), + ); + + clientTransports.push( + await spinUpServer("pong", async () => ({ + content: [{ type: "text", text: "ping" }], + })), + ); + + // We deliberately spin up 2 servers with the same tool name to + // demonstrate dealing with that edge case in the sample. + clientTransports.push( + await spinUpServer("ping", async () => ({ + content: [{ type: "text", text: "pong2" }], + })), + ); + + return clientTransports; +} + +function simulatePromptModel(tools: Tool[]): string { + console.log(`Model was prompted with the following tools:`); + for (const tool of tools) { + console.log(` - ${tool.name}`); + } + return "ping"; +}