Tool Use and Function Calling Through MonoRouter
Claude's tool use works seamlessly through MonoRouter. Here's what we had to get right for tool calls, results, and multi-turn tool loops.
Tool use is one of Claude's most powerful features for developers. You define tools, Claude decides when to call them, and your code executes the actual function. This back-and-forth loop is the backbone of AI agents — and it works through MonoRouter without any changes to your code.
A Complete Tool-Use Example
Let's build a weather assistant that Claude can use to look up current conditions. This is a full, working example — you can copy it and run it.
Step 1: Define the Tools
import anthropic
import json
client = anthropic.Anthropic(
api_key="mr_live_abc123",
base_url="https://api.monorouter.dev/v1",
)
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city. Returns temperature in Fahrenheit and conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'San Francisco'"
},
"state": {
"type": "string",
"description": "Two-letter US state code, e.g. 'CA'"
}
},
"required": ["city"]
}
}
]
Step 2: Send the Request
messages = [
{"role": "user", "content": "What's the weather like in San Francisco and New York?"}
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(f"Stop reason: {response.stop_reason}")
# Output: Stop reason: tool_use
When Claude decides to call a tool, it returns a response with stop_reason: "tool_use" and one or more tool_use content blocks. In this case, Claude will request two parallel tool calls — one for each city.
Step 3: Execute the Tool and Return Results
def get_weather(city: str, state: str = None) -> dict:
"""Your actual implementation — call a weather API, scrape a page, whatever."""
# Simulated for this example
weather_data = {
"San Francisco": {"temp": 62, "conditions": "Foggy", "humidity": 78},
"New York": {"temp": 84, "conditions": "Sunny", "humidity": 55},
}
return weather_data.get(city, {"temp": 70, "conditions": "Unknown"})
def process_tool_calls(response):
"""Extract tool calls, execute them, and build tool_result messages."""
tool_results = []
for block in response.content:
if block.type == "tool_use":
# Execute the function
result = get_weather(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # Must match the tool_use block's ID
"content": json.dumps(result),
})
return tool_results
# Build the next message with tool results
tool_results = process_tool_calls(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
Step 4: Get Claude's Final Response
# Send the tool results back — Claude will synthesize a natural language answer
final_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(final_response.content[0].text)
# Output: Here's the current weather:
#
# **San Francisco**: 62°F and foggy with 78% humidity.
# **New York**: 84°F and sunny with 55% humidity.
The entire exchange — tool definitions, tool_use responses, tool_result messages, and the final answer — passes through MonoRouter unchanged. Every field is preserved: tool_use_id references, is_error flags, input schemas, everything.
The Full Agent Loop
In practice, you'll wrap this in a loop so Claude can call tools multiple times before giving a final answer:
def run_agent(user_message: str, tools: list, max_turns: int = 10):
messages = [{"role": "user", "content": user_message}]
for turn in range(max_turns):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
messages=messages,
)
# If Claude is done (no more tool calls), return the text
if response.stop_reason == "end_turn":
return response.content[0].text
# Otherwise, execute tool calls and continue the loop
tool_results = process_tool_calls(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
return "Max turns reached"
This is the same pattern used by Claude Code, Cursor, and LangChain under the hood. Each iteration is an independent API request, and MonoRouter routes each one through your session pool.
TypeScript Version
The same pattern in TypeScript using the Anthropic SDK:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: "mr_live_abc123",
baseURL: "https://api.monorouter.dev/v1",
});
const tools: Anthropic.Tool[] = [
{
name: "search_codebase",
description: "Search the codebase for files matching a pattern",
input_schema: {
type: "object" as const,
properties: {
query: { type: "string", description: "Search query or glob pattern" },
},
required: ["query"],
},
},
];
// The SDK handles the tool-use loop automatically with stream helpers
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools,
messages: [{ role: "user", content: "Find all TypeScript files that import React" }],
});
Parallel Tool Calls
Claude can request multiple tool calls in a single response — for example, looking up weather in two cities simultaneously. Your code executes them all (optionally in parallel) and sends the results back in a single message. MonoRouter handles this natively — there's nothing special about a message with multiple tool results.
The tool_use_id field is the key. Each tool result must reference the exact ID from the corresponding tool_use block. MonoRouter passes these IDs through unchanged, so matching always works.