diff --git a/mcp-client-typescript/index.ts b/mcp-client-typescript/index.ts index 30fd0be..e4d0fdb 100644 --- a/mcp-client-typescript/index.ts +++ b/mcp-client-typescript/index.ts @@ -7,6 +7,8 @@ import { import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import readline from "readline/promises"; +import { access } from "fs/promises"; +import { constants } from "fs"; import dotenv from "dotenv"; @@ -32,13 +34,23 @@ class MCPClient { this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" }); } - async connectToServer(serverScriptPath: string) { + async connectToServer(serverScriptPath: string, timeoutMs: number = 30000) { /** * Connect to an MCP server * * @param serverScriptPath - Path to the server script (.py or .js) + * @param timeoutMs - Connection timeout in milliseconds (default: 30000) */ try { + // Check if the server script file exists + try { + await access(serverScriptPath, constants.F_OK | constants.R_OK); + } catch { + throw new Error( + `Server script not found or not readable: ${serverScriptPath}` + ); + } + // Determine script type and appropriate command const isJs = serverScriptPath.endsWith(".js"); const isPy = serverScriptPath.endsWith(".py"); @@ -51,22 +63,42 @@ class MCPClient { : "python3" : process.execPath; - // Initialize transport and connect to server + // Initialize transport and connect to server with timeout this.transport = new StdioClientTransport({ command, args: [serverScriptPath], }); - await this.mcp.connect(this.transport); - - // List available tools - const toolsResult = await this.mcp.listTools(); - this.tools = toolsResult.tools.map((tool) => { - return { - name: tool.name, - description: tool.description, - input_schema: tool.inputSchema, - }; - }); + + // Wrap connection and tool listing with timeout + const connectionPromise = (async () => { + await this.mcp.connect(this.transport!); + + // List available tools + const toolsResult = await this.mcp.listTools(); + this.tools = toolsResult.tools.map((tool) => { + return { + name: tool.name, + description: tool.description, + input_schema: tool.inputSchema, + }; + }); + })(); + + await Promise.race([ + connectionPromise, + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + `Connection timeout: Server did not respond within ${timeoutMs}ms` + ) + ), + timeoutMs + ) + ), + ]); + console.log( "Connected to server with tools:", this.tools.map(({ name }) => name), @@ -80,6 +112,7 @@ class MCPClient { async processQuery(query: string) { /** * Process a query using Claude and available tools + * Implements a proper agentic loop that handles multiple tool calls * * @param query - The user's input query * @returns Processed response as a string @@ -91,53 +124,81 @@ class MCPClient { }, ]; - // Initial Claude API call - const response = await this.anthropic.messages.create({ + let response = await this.anthropic.messages.create({ model: ANTHROPIC_MODEL, max_tokens: 1000, messages, tools: this.tools, }); - // Process response and handle tool calls - const finalText = []; + // Agentic loop: continue until Claude stops requesting tools + while (response.stop_reason === "tool_use") { + // Add assistant's response to conversation history + messages.push({ + role: "assistant", + content: response.content, + }); - for (const content of response.content) { - if (content.type === "text") { - finalText.push(content.text); - } else if (content.type === "tool_use") { - // Execute tool call - const toolName = content.name; - const toolArgs = content.input as { [x: string]: unknown } | undefined; + // Collect all tool uses from this turn + const toolUses = response.content.filter( + (block) => block.type === "tool_use" + ); - const result = await this.mcp.callTool({ - name: toolName, - arguments: toolArgs, - }); - finalText.push( - `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`, - ); + // Execute all tool calls and collect results + const toolResults = await Promise.all( + toolUses.map(async (toolUse) => { + if (toolUse.type !== "tool_use") return null; - // Continue conversation with tool results - messages.push({ - role: "user", - content: result.content as string, - }); + try { + console.log( + `[Calling tool: ${toolUse.name} with args: ${JSON.stringify(toolUse.input)}]` + ); - // Get next response from Claude - const response = await this.anthropic.messages.create({ - model: ANTHROPIC_MODEL, - max_tokens: 1000, - messages, - }); + const result = await this.mcp.callTool({ + name: toolUse.name, + arguments: toolUse.input as { [x: string]: unknown } | undefined, + }); - finalText.push( - response.content[0].type === "text" ? response.content[0].text : "", - ); - } + // Format tool result according to Anthropic's API + return { + type: "tool_result" as const, + tool_use_id: toolUse.id, + content: JSON.stringify(result.content), + }; + } catch (error) { + console.error(`[Tool ${toolUse.name} failed: ${error}]`); + // Return error as tool result + return { + type: "tool_result" as const, + tool_use_id: toolUse.id, + content: `Error: ${error instanceof Error ? error.message : String(error)}`, + is_error: true, + }; + } + }) + ); + + // Filter out any null results and add tool results to conversation + const validToolResults = toolResults.filter((r) => r !== null); + messages.push({ + role: "user", + content: validToolResults, + }); + + // Get Claude's next response + response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); } - return finalText.join("\n"); + // Extract final text response + const textBlocks = response.content.filter( + (block) => block.type === "text" + ); + return textBlocks.map((block) => block.text).join("\n"); } async chatLoop() { diff --git a/mcp-client-typescript/package-lock.json b/mcp-client-typescript/package-lock.json index 8fabb30..ad93fa0 100644 --- a/mcp-client-typescript/package-lock.json +++ b/mcp-client-typescript/package-lock.json @@ -11,11 +11,11 @@ "dependencies": { "@anthropic-ai/sdk": "^0.36.3", "@modelcontextprotocol/sdk": "^1.25.2", - "dotenv": "^16.4.7" + "dotenv": "^16.6.1" }, "devDependencies": { - "@types/node": "^22.13.4", - "typescript": "^5.7.3" + "@types/node": "^22.19.3", + "typescript": "^5.9.3" }, "engines": { "node": ">=16.0.0" @@ -103,12 +103,12 @@ } }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-fetch": { @@ -399,9 +399,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1400,9 +1400,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1414,9 +1414,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/mcp-client-typescript/package.json b/mcp-client-typescript/package.json index 2f6ddce..e4107a1 100644 --- a/mcp-client-typescript/package.json +++ b/mcp-client-typescript/package.json @@ -13,13 +13,14 @@ "dependencies": { "@anthropic-ai/sdk": "^0.36.3", "@modelcontextprotocol/sdk": "^1.25.2", - "dotenv": "^16.4.7" + "dotenv": "^16.6.1" }, "devDependencies": { - "@types/node": "^22.13.4", - "typescript": "^5.7.3" + "@types/node": "^22.19.3", + "typescript": "^5.9.3" }, "engines": { "node": ">=16.0.0" - } + }, + "description": "See the [Building MCP clients](https://modelcontextprotocol.io/tutorials/building-a-client) tutorial for more information." } diff --git a/weather-server-typescript/package-lock.json b/weather-server-typescript/package-lock.json index 832c3e5..eed9771 100644 --- a/weather-server-typescript/package-lock.json +++ b/weather-server-typescript/package-lock.json @@ -9,13 +9,14 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.24.3" + "@modelcontextprotocol/sdk": "^1.25.2", + "zod": "^3.25.76" }, "bin": { "weather": "build/index.js" }, "devDependencies": { - "@types/node": "^22.19.2", + "@types/node": "^22.19.3", "typescript": "^5.9.3" } }, @@ -71,10 +72,11 @@ } }, "node_modules/@types/node": { - "version": "22.19.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", - "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -1005,6 +1007,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1056,9 +1059,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/weather-server-typescript/package.json b/weather-server-typescript/package.json index 49b6bba..57136d1 100644 --- a/weather-server-typescript/package.json +++ b/weather-server-typescript/package.json @@ -4,7 +4,7 @@ "main": "index.js", "type": "module", "bin": { - "weather": "./build/index.js" + "weather": "build/index.js" }, "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"" @@ -15,12 +15,13 @@ "keywords": [], "author": "", "license": "ISC", - "description": "", + "description": "See the [Quickstart](https://modelcontextprotocol.io/quickstart) tutorial for more information.", "devDependencies": { - "@types/node": "^22.19.2", + "@types/node": "^22.19.3", "typescript": "^5.9.3" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.24.3" + "@modelcontextprotocol/sdk": "^1.25.2", + "zod": "^3.25.76" } }