From 1a0ad1cc80f262b9e9ad8314be5039acbed8b684 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 30 May 2026 11:47:53 -0700 Subject: [PATCH] Fix tool use in TypeScript client, mirroring #141 Apply the same Messages API tool-use fixes that #141 made to the Python client (mcp-client-python/client.py) to mcp-client-typescript/index.ts: - Reply to each tool_use with a proper tool_result block carrying tool_use_id, and append the assistant's full content (preserving the tool_use block) instead of sending the result as a plain user string. - Restructure processQuery into a turn-level loop so parallel tool calls in one response and chained tool calls across turns both work; pass tools on every follow-up request. - Add MAX_TOOL_TURNS = 10 cap with a "[Stopped after N tool-use turns]" notice to prevent unbounded tool-use loops. - Fix chatLoop to exit cleanly on EOF (Ctrl-D) and SIGINT (Ctrl-C) via an AbortController wired to readline close, instead of hanging or throwing on stdin EOF. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp-client-typescript/index.ts | 100 ++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/mcp-client-typescript/index.ts b/mcp-client-typescript/index.ts index 6c368bd1..8137963a 100644 --- a/mcp-client-typescript/index.ts +++ b/mcp-client-typescript/index.ts @@ -1,7 +1,10 @@ import { Anthropic } from "@anthropic-ai/sdk"; import { + ContentBlockParam, MessageParam, Tool, + ToolResultBlockParam, + ToolUseBlock, } from "@anthropic-ai/sdk/resources/messages/messages.mjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; @@ -13,6 +16,7 @@ import dotenv from "dotenv"; dotenv.config(); // load environment variables from .env const ANTHROPIC_MODEL = "claude-sonnet-4-5"; +const MAX_TOOL_TURNS = 10; class MCPClient { private mcp: Client; @@ -89,52 +93,62 @@ 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 = []; + const finalText: string[] = []; - 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; + for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) { + const toolUses: ToolUseBlock[] = []; + for (const block of response.content) { + if (block.type === "text") { + finalText.push(block.text); + } else if (block.type === "tool_use") { + toolUses.push(block); + } + } + + if (toolUses.length === 0) { + return finalText.join("\n"); + } + + const toolResults: ToolResultBlockParam[] = []; + for (const toolUse of toolUses) { + const toolArgs = toolUse.input as { [x: string]: unknown } | undefined; + finalText.push( + `[Calling tool ${toolUse.name} with args ${JSON.stringify(toolArgs)}]`, + ); const result = await this.mcp.callTool({ - name: toolName, + name: toolUse.name, arguments: toolArgs, }); - finalText.push( - `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`, - ); - - // Continue conversation with tool results - messages.push({ - role: "user", - content: result.content as string, + toolResults.push({ + type: "tool_result", + tool_use_id: toolUse.id, + content: result.content as ToolResultBlockParam["content"], }); + } - // Get next response from Claude - const response = await this.anthropic.messages.create({ - model: ANTHROPIC_MODEL, - max_tokens: 1000, - messages, - }); + messages.push({ + role: "assistant", + content: response.content as unknown as ContentBlockParam[], + }); + messages.push({ role: "user", content: toolResults }); - finalText.push( - response.content[0].type === "text" ? response.content[0].text : "", - ); - } + response = await this.anthropic.messages.create({ + model: ANTHROPIC_MODEL, + max_tokens: 1000, + messages, + tools: this.tools, + }); } + finalText.push(`[Stopped after ${MAX_TOOL_TURNS} tool-use turns]`); return finalText.join("\n"); } @@ -146,20 +160,38 @@ class MCPClient { input: process.stdin, output: process.stdout, }); + // rl.question() doesn't reject on stdin EOF on its own; wire its close + // event to an AbortSignal so EOF (Ctrl-D) and SIGINT both unblock it. + const ac = new AbortController(); + const onClose = () => ac.abort(); + rl.on("close", onClose); + const onSigint = () => rl.close(); + process.once("SIGINT", onSigint); try { console.log("\nMCP Client Started!"); console.log("Type your queries or 'quit' to exit."); while (true) { - const message = await rl.question("\nQuery: "); - if (message.toLowerCase() === "quit") { + let message: string; + try { + message = await rl.question("\nQuery: ", { signal: ac.signal }); + } catch { break; } - const response = await this.processQuery(message); - console.log("\n" + response); + + if (message.toLowerCase() === "quit") break; + + try { + const response = await this.processQuery(message); + console.log("\n" + response); + } catch (e) { + console.log(`\nError: ${e instanceof Error ? e.message : String(e)}`); + } } } finally { + process.off("SIGINT", onSigint); + rl.off("close", onClose); rl.close(); } }