Wiring up nodejs claude messages api function calling requires more than a single SDK call—you have to loop tool results back into the conversation until Claude stops emitting tool_use blocks. This tutorial builds a minimal TypeScript agent that calls local functions for weather and arithmetic using the Anthropic Messages API and the official SDK.
Prerequisites
- Node.js 18 or newer (fetch is global)
- An Anthropic API key (
ANTHROPIC_API_KEYin env) - Familiarity with TypeScript and async/await
Initialize a project and install deps:
mkdir claude-tools && cd claude-tools
npm init -y
npm install @anthropic-ai/sdk
npm install -D typescript tsx
Add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true
},
"include": ["*.ts"]
}
Set your key:
export ANTHROPIC_API_KEY="sk-ant-..."
Define the tool schemas
Claude needs JSON Schema for each tool. We declare two: get_weather and calculate.
const tools = [
{
name: "get_weather",
input_schema: {
type: "object",
properties: {
city: { type: "string", description: "City name, e.g. 'Berlin'" }
},
required: ["city"]
}
},
{
name: "calculate",
input_schema: {
type: "object",
properties: {
expression: { type: "string", description: "e.g. '3 * (4 + 5)'" }
},
required: ["expression"]
}
}
] as const;
Implement local handlers
The SDK does not execute code. You map tool_use blocks to real functions:
function getWeather(city: string): string {
// stub: real impl would call an API
const fake: Record<string, number> = { berlin: 12, paris: 18, tokyo: 24 };
const t = fake[city.toLowerCase()] ?? 20;
return `${city}: ${t}°C`;
}
function calculate(expr: string): string {
// safe-ish eval for demo only
const sanitized = expr.replace(/[^0-9+\-*/().\s]/g, "");
try {
const result = Function(`"use strict"; return (${sanitized})`)();
return String(result);
} catch {
return "error";
}
}
Send the first request
Build the conversation and call client.messages.create. Use claude-3-5-sonnet-20241022 (a real model id).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
let messages: Anthropic.MessageCreateParams["messages"] = [
{ role: "user", content: "What's the weather in Berlin and what is 3 * (4 + 5)?" }
];
const first = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
tools: tools as unknown as Anthropic.Tool[],
messages
});
console.log(JSON.stringify(first.content, null, 2));
Expected checkpoint output (abridged):
[
{
"type": "text",
"text": "I'll check both for you."
},
{
"type": "tool_use",
"id": "toolu_01A",
"name": "get_weather",
"input": { "city": "Berlin" }
},
{
"type": "tool_use",
"id": "toolu_01B",
"name": "calculate",
"input": { "expression": "3 * (4 + 5)" }
}
]
Run the tool loop
Append the assistant message, execute each tool, and return results as a user message with tool_result blocks.
messages.push({ role: "assistant", content: first.content });
const toolResults = first.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")
.map((block) => {
let content = "";
if (block.name === "get_weather") {
content = getWeather((block.input as any).city);
} else if (block.name === "calculate") {
content = calculate((block.input as any).expression);
}
return {
type: "tool_result" as const,
tool_use_id: block.id,
content
};
});
messages.push({ role: "user", content: toolResults });
const second = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
tools: tools as unknown as Anthropic.Tool[],
messages
});
console.log(second.content.find(b => b.type === "text")?.text);
Expected final text:
The weather in Berlin is 12°C, and 3 * (4 + 5) equals 27.
Full agent loop
Wrap it in a while to handle multi-step chains:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const model = "claude-3-5-sonnet-20241022";
let messages: Anthropic.MessageCreateParams["messages"] = [
{ role: "user", content: "Weather in Paris then add 10 to that temperature." }
];
for (let i = 0; i < 5; i++) {
const res = await client.messages.create({
model,
max_tokens: 1024,
tools: tools as unknown as Anthropic.Tool[],
messages
});
messages.push({ role: "assistant", content: res.content });
const uses = res.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
);
if (uses.length === 0) {
console.log(res.content.find(b => b.type === "text")?.text);
break;
}
const results = uses.map((u) => {
const input = u.input as any;
const content =
u.name === "get_weather" ? getWeather(input.city)
: u.name === "calculate" ? calculate(input.expression)
: "unknown tool";
return { type: "tool_result" as const, tool_use_id: u.id, content };
});
messages.push({ role: "user", content: results });
}
This loop terminates when Claude returns no tool_use blocks. In production, add timeout and error handling.
Notes on routing and fallback
If you front the Messages API with an OpenRouter-class gateway such as n4n.ai, you get one OpenAI-compatible endpoint for 240+ models and automatic fallback when a provider is degraded, but the nodejs claude messages api function calling loop above stays identical—only the client base URL changes.
Keep tool schemas strict. Claude will obey required fields; missing inputs fail fast. For latency, stream responses and parse tool_use deltas incrementally.
That’s the whole pattern: define tools, send, loop on tool_use, return tool_result.