Merge pull request #147 from olaservo/fix-ts-client-tool-use
Fix tool use in TypeScript client, mirroring #141
This commit is contained in:
commit
eafc692b88
1 changed files with 66 additions and 34 deletions
|
|
@ -1,7 +1,10 @@
|
||||||
import { Anthropic } from "@anthropic-ai/sdk";
|
import { Anthropic } from "@anthropic-ai/sdk";
|
||||||
import {
|
import {
|
||||||
|
ContentBlockParam,
|
||||||
MessageParam,
|
MessageParam,
|
||||||
Tool,
|
Tool,
|
||||||
|
ToolResultBlockParam,
|
||||||
|
ToolUseBlock,
|
||||||
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
|
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
|
||||||
|
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
@ -13,6 +16,7 @@ import dotenv from "dotenv";
|
||||||
dotenv.config(); // load environment variables from .env
|
dotenv.config(); // load environment variables from .env
|
||||||
|
|
||||||
const ANTHROPIC_MODEL = "claude-sonnet-4-5";
|
const ANTHROPIC_MODEL = "claude-sonnet-4-5";
|
||||||
|
const MAX_TOOL_TURNS = 10;
|
||||||
|
|
||||||
class MCPClient {
|
class MCPClient {
|
||||||
private mcp: Client;
|
private mcp: Client;
|
||||||
|
|
@ -89,52 +93,62 @@ class MCPClient {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Initial Claude API call
|
let response = await this.anthropic.messages.create({
|
||||||
const response = await this.anthropic.messages.create({
|
|
||||||
model: ANTHROPIC_MODEL,
|
model: ANTHROPIC_MODEL,
|
||||||
max_tokens: 1000,
|
max_tokens: 1000,
|
||||||
messages,
|
messages,
|
||||||
tools: this.tools,
|
tools: this.tools,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process response and handle tool calls
|
const finalText: string[] = [];
|
||||||
const finalText = [];
|
|
||||||
|
|
||||||
for (const content of response.content) {
|
for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) {
|
||||||
if (content.type === "text") {
|
const toolUses: ToolUseBlock[] = [];
|
||||||
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 (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({
|
const result = await this.mcp.callTool({
|
||||||
name: toolName,
|
name: toolUse.name,
|
||||||
arguments: toolArgs,
|
arguments: toolArgs,
|
||||||
});
|
});
|
||||||
finalText.push(
|
toolResults.push({
|
||||||
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`,
|
type: "tool_result",
|
||||||
);
|
tool_use_id: toolUse.id,
|
||||||
|
content: result.content as ToolResultBlockParam["content"],
|
||||||
// Continue conversation with tool results
|
|
||||||
messages.push({
|
|
||||||
role: "user",
|
|
||||||
content: result.content as string,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get next response from Claude
|
|
||||||
const response = await this.anthropic.messages.create({
|
|
||||||
model: ANTHROPIC_MODEL,
|
|
||||||
max_tokens: 1000,
|
|
||||||
messages,
|
|
||||||
});
|
|
||||||
|
|
||||||
finalText.push(
|
|
||||||
response.content[0].type === "text" ? response.content[0].text : "",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: response.content as unknown as ContentBlockParam[],
|
||||||
|
});
|
||||||
|
messages.push({ role: "user", content: toolResults });
|
||||||
|
|
||||||
|
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");
|
return finalText.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,20 +160,38 @@ class MCPClient {
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
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 {
|
try {
|
||||||
console.log("\nMCP Client Started!");
|
console.log("\nMCP Client Started!");
|
||||||
console.log("Type your queries or 'quit' to exit.");
|
console.log("Type your queries or 'quit' to exit.");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const message = await rl.question("\nQuery: ");
|
let message: string;
|
||||||
if (message.toLowerCase() === "quit") {
|
try {
|
||||||
|
message = await rl.question("\nQuery: ", { signal: ac.signal });
|
||||||
|
} catch {
|
||||||
break;
|
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 {
|
} finally {
|
||||||
|
process.off("SIGINT", onSigint);
|
||||||
|
rl.off("close", onClose);
|
||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue