The most important thing to understand about Claude's tool use: Claude doesn't call your functions. You call your functions, when Claude tells you to.
This distinction matters for how you design tool-using agents. Let me show you how it actually works, then build something real.
The execution model
Here's the basic loop:
1. You send messages + tool definitions to Claude
2. Claude responds with either:
a. A text response (done)
b. A tool_use block (Claude wants data)
3. If tool_use: you execute the function, send the result back
4. Claude incorporates the result and continues
5. Repeat until Claude returns a final text response
Claude never has network access. It never calls your API. It just returns structured JSON telling you what it would like you to run and with what arguments.
import anthropic
client = anthropic.Anthropic()
# Define what tools Claude can "use"
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. 'Denver, CO'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
]
# Initial message
messages = [{"role": "user", "content": "What's the weather in Denver?"}]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(response.stop_reason) # "tool_use"
print(response.content)
# [TextBlock(text="I'll check the weather for you."),
# ToolUseBlock(id='toolu_01...', name='get_weather', input={'location': 'Denver, CO'})]
Claude returned a tool_use block. Now you execute the actual function:
# Execute the function Claude asked for
def get_weather(location: str, unit: str = "fahrenheit") -> dict:
# Your real weather API call here
return {
"location": location,
"temperature": 72,
"unit": unit,
"condition": "Partly cloudy",
"humidity": 45,
}
# Find tool use blocks in the response
tool_calls = [block for block in response.content if block.type == "tool_use"]
# Execute each tool call
tool_results = []
for tool_call in tool_calls:
if tool_call.name == "get_weather":
result = get_weather(**tool_call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": str(result),
})
# Send results back to Claude
messages = [
{"role": "user", "content": "What's the weather in Denver?"},
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results},
]
final_response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(final_response.content[0].text)
# "The weather in Denver, CO is currently 72°F and partly cloudy, with 45% humidity."
Building a real agent: multi-step research
The power shows up when you give Claude multiple tools and let it decide how to chain them. Here's a research agent that can search, read pages, and summarize findings:
import anthropic
import requests
from bs4 import BeautifulSoup
client = anthropic.Anthropic()
tools = [
{
"name": "search_web",
"description": "Search the web for information on a topic. Returns a list of results with titles and snippets.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"num_results": {"type": "integer", "description": "Number of results (max 5)", "default": 3}
},
"required": ["query"]
}
},
{
"name": "read_page",
"description": "Fetch and read the content of a web page. Use after searching to get full article content.",
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to fetch"},
"max_chars": {"type": "integer", "description": "Max characters to return", "default": 5000}
},
"required": ["url"]
}
},
{
"name": "save_note",
"description": "Save an important finding or note for the final report.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"},
"source": {"type": "string", "description": "URL or source of this finding"}
},
"required": ["title", "content"]
}
}
]
def execute_tool(name: str, inputs: dict) -> str:
if name == "search_web":
# Replace with your search API (Serper, Brave, SerpAPI, etc.)
results = your_search_api(inputs["query"], inputs.get("num_results", 3))
return str(results)
elif name == "read_page":
try:
resp = requests.get(inputs["url"], timeout=10, headers={"User-Agent": "Mozilla/5.0"})
soup = BeautifulSoup(resp.text, "html.parser")
# Remove scripts, styles, navigation
for tag in soup(["script", "style", "nav", "header", "footer"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
max_chars = inputs.get("max_chars", 5000)
return text[:max_chars]
except Exception as e:
return f"Error fetching page: {e}"
elif name == "save_note":
# Save to your notes system
notes.append(inputs)
return f"Note saved: {inputs['title']}"
return "Unknown tool"
def run_agent(task: str, max_iterations: int = 10) -> str:
"""Run the research agent until it produces a final answer."""
messages = [{"role": "user", "content": task}]
notes = []
for i in range(max_iterations):
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
# Add assistant response to history
messages.append({"role": "assistant", "content": response.content})
# Done — Claude has a final answer
if response.stop_reason == "end_turn":
text_blocks = [b for b in response.content if b.type == "text"]
return text_blocks[-1].text if text_blocks else ""
# Execute tool calls
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for tool_call in tool_calls:
print(f" → Calling {tool_call.name}({tool_call.input})")
result = execute_tool(tool_call.name, tool_call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
return "Max iterations reached"
# Use it
result = run_agent(
"Research the top 3 MCP servers for developer productivity in 2026. "
"Find real examples, check their GitHub stars, and summarize what makes each valuable."
)
print(result)
Claude will:
- Search for MCP servers
- Read the top result pages
- Save notes on key findings
- Search for more specific information as needed
- Return a synthesized report
You didn't write any of that logic. Claude decided the research strategy.
Parallel tool calls
Claude 3.5+ can make multiple tool calls in a single response when they're independent. This is critical for performance:
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Compare the weather in Denver, NYC, and LA"}],
)
# Response may contain 3 tool_use blocks simultaneously
tool_calls = [b for b in response.content if b.type == "tool_use"]
print(len(tool_calls)) # 3 — Claude called get_weather 3 times in parallel
# Execute them all, return all results
tool_results = []
for tool_call in tool_calls:
result = get_weather(**tool_call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": str(result),
})
In your executor, you can run these in parallel with asyncio or ThreadPoolExecutor since they're independent.
Forcing tool use
Sometimes you want Claude to always use a specific tool, not just when it decides to:
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"}, # Force this tool
messages=messages,
)
Or force any tool use (Claude must use at least one tool):
tool_choice={"type": "any"}
Or let Claude decide (default):
tool_choice={"type": "auto"}
Error handling in tool results
You can return errors to Claude and it will adapt:
try:
result = execute_tool(tool_call.name, tool_call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": f"Error: {str(e)}",
"is_error": True, # Tell Claude this was an error
})
Claude will see the error and decide whether to retry with different arguments, use a different tool, or acknowledge the failure in its response.
Token efficiency tips
Tool calls consume tokens. Here's how to keep costs down:
1. Tighten your tool schemas — Only include the parameters Claude actually needs. Extra fields in descriptions cost tokens.
2. Return structured data, not prose — {"temp": 72, "unit": "F"} is cheaper than "The temperature is currently 72 degrees Fahrenheit".
3. Cap tool output size — Add a max_chars parameter to any tool that reads external content.
4. Use claude-haiku-4-5 for simple tool routing — If the tool call logic is simple (just picking which API to call), Haiku handles it well at a fraction of the cost.
5. Pre-filter tool results — If your search returns 10 results but Claude only needs metadata, strip the full content before returning.
What to build with this
The agent pattern above handles 80% of practical use cases. The tools that make agents most powerful in practice:
- Code execution — let Claude run Python snippets to calculate things
- Database queries — read-only SQL access against your data
- File system access — read and write files within a sandboxed directory
- API integrations — Stripe, Notion, GitHub, Slack — anything with an API
- Web scraping — fetch and parse any public page
The MCP protocol standardizes this pattern so tools are reusable across different Claude integrations. You define a tool once as an MCP server and it works in Claude Desktop, Claude Code, and your own apps.
The Whoff Agents MCP servers are pre-built examples of this — Crypto Data MCP, Security Scanner, Workflow Automator — each exposing a set of tools that Claude can use to do real work with real data.
The Anthropic API docs have a complete tool use reference at docs.anthropic.com. The examples in this post are simplified — production code should add retry logic, token budget tracking, and rate limit handling.
Top comments (0)