AISuffer
intermediate Engineers

Claude Tool Use: How to Give Claude Access to Tools

Practical guide to Claude tool use — from basic concepts to building an agent with tools.

What Is Tool Use

Tool use is Claude’s ability to call external functions during response generation. Instead of just generating text, Claude can decide: “I need to look this up” or “I need to calculate this” — and call a function you provide. This is the core technology behind AI agents.

Without tool use, Claude is a smart text generator. With tool use, Claude becomes an agent that can act on the real world — search databases, call APIs, read files, send messages.

How It Works

The flow is simple:

  1. You describe available tools in JSON Schema format
  2. Send a request to Claude API along with tool descriptions
  3. Claude analyzes the user’s request and decides which tool to call (or none)
  4. Claude returns a tool_use response with the tool name and parameters
  5. You execute the call on your side and return the result
  6. Claude uses the result to form the final response
User: "What's the weather in London?"

Claude: tool_use → get_weather(city="London")

Your code: calls weather API → returns {temp: 15, condition: "cloudy"}

Claude: "It's 15°C and cloudy in London right now."

The key insight: Claude never executes code directly. It generates structured JSON, and your code handles the actual execution. This keeps you in control.

Defining Tools

Each tool needs three things: a name, a description, and an input schema. The description is critical — Claude uses it to decide when to call the tool.

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "search_web",
        "description": "Searches the internet for current information. Use this when the user asks about recent events, current data, or anything that requires up-to-date information.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "get_stock_price",
        "description": "Gets the current stock price for a given ticker symbol. Returns price in USD.",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {
                    "type": "string",
                    "description": "Stock ticker symbol, e.g. AAPL, GOOGL, MSFT"
                }
            },
            "required": ["ticker"]
        }
    }
]

Pro tip: Write descriptions as if you’re explaining to a smart colleague when to use the tool. Vague descriptions like “searches stuff” lead to poor tool selection. Specific descriptions like “searches the internet for current information — use when the user asks about recent events” work much better.

Making the API Call

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the current stock price of Apple?"}
    ]
)

Claude will analyze the message and decide whether to use a tool or respond directly.

Handling tool_use Responses

When Claude decides to use a tool, the response has stop_reason: "tool_use" and contains a tool_use block:

import json

def handle_response(response):
    for block in response.content:
        if block.type == "tool_use":
            tool_name = block.name
            tool_input = block.input
            tool_use_id = block.id

            print(f"Claude wants to call: {tool_name}({json.dumps(tool_input)})")

            # Execute the tool
            result = execute_tool(tool_name, tool_input)

            return tool_use_id, result

        elif block.type == "text":
            print(f"Claude says: {block.text}")
            return None, None

def execute_tool(name: str, input_data: dict) -> str:
    if name == "get_stock_price":
        # Your actual implementation here
        return json.dumps({"ticker": input_data["ticker"], "price": 198.50, "currency": "USD"})
    elif name == "search_web":
        # Your search implementation
        return json.dumps({"results": [{"title": "Result 1", "snippet": "..."}]})
    return json.dumps({"error": f"Unknown tool: {name}"})

Multi-Turn Tool Use (The Agent Loop)

Real agents don’t stop at one tool call. They chain multiple calls together. Here’s the full pattern:

def run_agent(user_message: str, max_turns: int = 10):
    messages = [{"role": "user", "content": user_message}]

    for turn in range(max_turns):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )

        # Append assistant response
        messages.append({"role": "assistant", "content": response.content})

        # If Claude is done — return the text
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return "Done."

        # If Claude wants to use tools — execute them all
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            # Return results to Claude
            messages.append({"role": "user", "content": tool_results})

    return "Max turns reached."

This loop is the heart of every Claude agent. Claude keeps calling tools until it has enough information to answer.

Error Handling

Tools can fail. Networks go down, APIs return errors, files don’t exist. Always return structured errors — Claude handles them gracefully:

def execute_tool(name: str, input_data: dict) -> str:
    try:
        if name == "get_stock_price":
            price = fetch_stock_price(input_data["ticker"])
            return json.dumps({"price": price})
    except ConnectionError:
        return json.dumps({"error": "Could not connect to stock API. Try again later."})
    except KeyError:
        return json.dumps({"error": f"Unknown ticker: {input_data.get('ticker', 'N/A')}"})
    except Exception as e:
        return json.dumps({"error": f"Unexpected error: {str(e)}"})

    return json.dumps({"error": f"Unknown tool: {name}"})

When Claude receives an error, it typically explains the issue to the user or tries an alternative approach. Don’t swallow errors silently — return them as structured data.

Streaming Tool Use

For real-time applications, you can stream tool use responses:

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Look up Apple stock price"}],
) as stream:
    for event in stream:
        if event.type == "content_block_start":
            if event.content_block.type == "tool_use":
                print(f"Calling tool: {event.content_block.name}")
        elif event.type == "text":
            print(event.text, end="", flush=True)

Streaming is essential for chat interfaces where you want to show Claude “thinking” in real time.

Claude vs OpenAI Tool Use

AspectClaude Tool UseOpenAI Function Calling
ProtocolNative + MCP (open standard)Proprietary
StateStateless (you manage messages)Stateless (or Assistants API for managed)
Parallel callsYesYes
StreamingYesYes
Tool descriptionsJSON SchemaJSON Schema
EcosystemMCP servers (1000+ community tools)Plugin ecosystem (limited)

The biggest advantage of Claude’s approach: MCP compatibility. Instead of writing custom tool integrations, you can connect pre-built MCP servers for GitHub, Slack, databases, and hundreds of other services.

Best Practices

  1. Write detailed tool descriptions — Claude picks tools based on descriptions, not names. A good description tells Claude exactly when and why to use the tool.

  2. Validate all input — Claude generates JSON parameters, but don’t blindly trust them. Validate types, check ranges, sanitize strings.

  3. Set iteration limits — always cap the agent loop (10-20 turns max). Runaway loops burn tokens and money.

  4. Return structured data — return JSON, not freeform text. Claude works better with structured tool results.

  5. Log everything — log every tool call with inputs and outputs. Essential for debugging when the agent misbehaves.

  6. Use tool_choice to control behavior:

    • "auto" — Claude decides (default)
    • {"type": "tool", "name": "search_web"} — force a specific tool
    • "any" — force Claude to use at least one tool
# Force Claude to use the search tool
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "search_web"},
    messages=[{"role": "user", "content": "Find MCP server news"}],
)

When NOT to Use Tool Use

Don’t overcomplicate things. You don’t need tools when:

  • The question can be answered from Claude’s training data
  • You’re doing text generation, summarization, or translation
  • A simple prompt handles the task

Tools add latency, cost, and complexity. Use them when Claude genuinely needs external data or actions — not as a default.

Next Steps