One tool call, end to end
tool_use · name, id, inputtool_result · id, contentThe definition, and what each field is actually for
A tool definition has three required fields — name, description and input_schema — plus an optional strict flag that makes the arguments conform to the schema exactly. The mechanics of the round trip are 2.3's subject: stop_reason of tool_use, one tool_result per tool_use_id. What belongs here is the design of the definition itself, because that is what the model actually reads when deciding what to do.
The description is the most under-invested field in most codebases. Anthropic's engineering guidance on writing tools is blunt about the return on it: even small refinements to tool descriptions can yield dramatic improvements, and the framing to use is explaining the tool to a new team member who has all the general competence and none of your context. Make implicit knowledge explicit — what the tool does, when to reach for it, when not to, what it returns, and any precondition. Name parameters unambiguously: user_id rather than user, because the second one invites an email address.
The same tool, described twice
Weaktext
name: search
description:
Searches for stuff.
input_schema:
query: string
id: stringStrongtext
name: orders_search
description:
Find orders by customer
email or order number.
Use for “where is my
order”. Does NOT return
refunds — use
refunds_search for
those. Returns up to 20
orders, newest first.
input_schema:
customer_email: string
order_number: string
limit: integer (max 20)The schema is a second description, read by both the model and your validator. Use enums where the value set is fixed, mark genuinely required fields required, and set additionalProperties: false so nothing unexpected arrives. Note the limits that 6.3 covers: minimum, maximum and pattern are not enforced, so a tool that accepts a quantity still has to range-check it in code. With strict: true you get the declared shape reliably; you never get the declared meaning for free.
Tool set construction
The most common mistake in a first tool integration is to publish one tool per API endpoint. It feels complete and it performs badly, because the agent then has to compose four calls to answer one question, burning context and getting one of the four wrong. The guidance is to build tools around workflows rather than endpoints: a schedule_event tool instead of separate list_users, list_events and create_event; a get_customer_context that returns recent transactions and notes together instead of three retrieval tools the agent must stitch.
The counterweight is ambiguity. Anthropic's context-engineering guidance names bloated tool sets — too much functionality, or ambiguous decision points about which tool to use — as a specific failure mode, and the tools guidance states plainly that more tools do not always lead to better outcomes: a few thoughtful tools aimed at high-impact workflows beat comprehensive coverage. Where you do have many related tools, namespace them with a consistent prefix — asana_search, jira_search, or by resource, asana_projects_search — so that the name itself disambiguates.
| Symptom | Likely cause | Fix in the tool layer |
|---|---|---|
| Agent picks the wrong tool | Two descriptions overlap | Say what each is not for; namespace the names |
| Agent chains four calls for one answer | Tools mirror endpoints | Consolidate into a workflow tool |
| Agent calls with a missing argument | Ambiguous parameter name | Rename (user_id, not user); mark it required |
| Context fills with tool output | No limits on the response | Paginate, filter, add a concise response_format |
| Agent hallucinates record ids | Opaque UUIDs in responses | Return human-meaningful names where you can |
That last row is worth dwelling on. Resolving arbitrary alphanumeric UUIDs into semantically meaningful language significantly improves precision in retrieval tasks by reducing hallucination — a model can carry “Northwind Logistics, invoice March 2026” through a reasoning chain far more reliably than b7f3…. And on size: implement pagination, filtering and truncation with sensible defaults. The tools guidance mentions a response_format enum letting the agent ask for a concise or detailed reply, which cut token consumption by roughly two thirds in their Slack example, and notes that Claude Code caps tool responses at 25,000 tokens by default. A tool that can return a megabyte is a context-engineering problem waiting to happen — 6.1's subject, created here.
Client tools and server tools
The objective's client-versus-server distinction is about who executes the tool, and it is entirely mechanical. A client tool runs in your application: Claude returns a tool_use block, your code executes it and sends back a tool_result. Every custom tool you write is a client tool, and so are the Anthropic-schema client tools such as the memory, bash, text editor, computer use and browser use tools — Anthropic defines the schema, you still run them.
A server tool runs on Anthropic's infrastructure. You enable it and the results come back in the response; there is no tool_result for you to construct. Web search, web fetch, code execution, the advisor tool, the tool search tool and the MCP connector are in this group. Some carry usage-based pricing beyond tokens — web search is charged per search, code execution by compute time. One edge case is worth remembering: if Claude calls server tools in parallel with client tools, you handle execution for the client ones in that same round trip.
Who runs it, and what that changes
Client tools — you execute
- Any custom tool; memory, bash, editor, computer, browser
- Reaches your database, your VPC, your credentials
- You own errors, retries, timeouts and approval
- You must return a
tool_resultpertool_use_id
Server tools — Anthropic executes
- Web search, web fetch, code execution, advisor, tool search
- No execution code and no
tool_resultto build - Some priced per use on top of tokens
- Cannot reach systems only your network can see
Agentic harness dispatch
The harness is the code between the model and your systems, and dispatch is its core job: take a tool_use block, find the handler for that name, validate the input, run it, and turn whatever happened into a tool_result. Four rules make a dispatcher production-grade.
- Dispatch from a registry, never from `eval` or dynamic attribute lookup. An unknown tool name should produce an error result, not an exception and not an accidental call into your codebase.
- Validate the input against the schema before executing, and treat validation failure as a tool error the model can see and correct.
- Catch everything the handler can raise. A tool that throws takes down the loop; a tool that returns an error lets the agent recover.
- Return one `tool_result` per `tool_use` block, matched by `tool_use_id`, even for the calls that failed — a missing result is a malformed conversation.
HANDLERS = {"orders_search": orders_search, "refund_create": refund_create}
def dispatch(block): # block is one tool_use block
result = {"type": "tool_result", "tool_use_id": block.id}
handler = HANDLERS.get(block.name)
if handler is None: # hallucinated or stale tool name
return {**result, "content": f"Unknown tool: {block.name}", "is_error": True}
try:
args = SCHEMAS[block.name].validate(block.input) # your own validation
if needs_approval(block.name, args) and not approved(block):
return {**result, "content": "Awaiting human approval.", "is_error": True}
return {**result, "content": handler(**args)}
except ValidationError as e:
# Actionable: tells the model exactly what to fix and retry.
return {**result, "content": f"Invalid arguments: {e}", "is_error": True}
except Exception as e:
# Never leak internals; never let it escape the loop.
log.exception("tool failed", tool=block.name)
return {**result, "content": f"{block.name} failed: {safe(e)}", "is_error": True}The error path deserves its own emphasis, because it is where the objective's “error handling” lives. A failed tool is reported by returning a tool_result with is_error set to true and an explanation in content — not by raising, and not by silently substituting a default. Claude handles that gracefully: it can retry with corrected arguments, try a different tool, or tell the user it could not do the thing. What it cannot do is recover from information it never received. The rule of thumb is that error text should be actionable: “no customer found with email x@y.com — try orders_search with an order number” teaches the next attempt, where “Error 500” teaches nothing.
Controlling and approving tool calls
tool_choice controls whether the model may call a tool at all. auto is the default and lets it decide; any forces it to call something; {"type": "tool", "name": …} forces one specific tool; none forbids tools for that request. auto also accepts disable_parallel_tool_use, which makes the model call tools one at a time instead of issuing several in one response. Forcing a tool costs slightly more system-prompt overhead than auto, and the presence of the tools parameter at all adds a few hundred tokens before your own schemas are counted.
Prompt wording nudges tool use — the documentation gives graded examples, from “use the tools to investigate before responding” through to “always call a tool first” — but the guidance is explicit that when you need a guarantee you set tool_choice rather than relying on the prompt. That principle generalises to the whole task statement, and it is the heart of approval patterns: what must not happen is not a prompting problem.
Claude Code's permission system is the reference implementation to reason from. It states the principle directly: permission rules are enforced by Claude Code, not by the model, and instructions in a prompt or CLAUDE.md shape what Claude tries to do but do not change what is allowed. Rules are allow, ask and deny entries in settings, written as a tool name with an optional specifier such as Bash(npm run *) or Read(./.env). Permission modes set the default posture — default prompts on first use of each tool, acceptEdits auto-accepts file edits, plan explores without editing, bypassPermissions skips prompts and is for isolated containers only. And a PreToolUse hook can evaluate a call programmatically before the prompt appears, with deny rules taking precedence over anything a hook allows.
Deciding what needs a human
- Read-only, scopedAllowno prompt; log it
- Reversible writeAllow, then auditundo path must exist
- Spends or notifiesAsk a humanapproval before execution
- DestructiveDeny in policynot reachable by the agent
Two patterns implement the middle column in an API application. Gate inside the handler: the dispatcher checks a policy before executing, and returns an “awaiting approval” tool result so the agent can explain the wait to the user, as in the code above. Split the tool: refund_request writes a pending row that a person releases, and there is no refund_execute in the tool set at all. The second is stronger, because the capability the agent lacks cannot be talked into existence. Either way the safest tool set is the smallest one that does the job — least privilege, which 7.2 develops.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Publishing one tool per API endpoint | Build tools around the workflows the agent performs, so one call answers one question. |
| Letting a handler raise, so a tool failure ends the conversation | Catch it and return a tool_result with is_error and an actionable message, so the agent can recover. |
| Returning raw UUIDs and unbounded payloads from tools | Return human-meaningful identifiers, paginate, and offer a concise response format. |
| Relying on the system prompt to stop a dangerous tool call | Enforce it outside the model — permission rules, an approval gate in the dispatcher, or simply not offering the tool. |
| Giving an agent a general-purpose shell or query tool for convenience | Offer specific, scoped tools; a general tool is unbounded privilege that no description can narrow. |
You should now be able to
- Write a tool definition whose name, description and schema tell the model when to use it and when not to.
- Construct a tool set around workflows, namespaced to remove ambiguity, sized for the job.
- Distinguish client tools from server tools and say which is possible for a given integration.
- Implement harness dispatch with registry lookup, input validation and total error containment.
- Return tool errors as
is_errorresults with actionable text, and classify model, tool and infrastructure failures. - Choose an approval pattern by reversibility and blast radius, and enforce it outside the model.