Giving an AI model access to an API does not automatically give it a useful tool.
An API is usually designed for software written by people who already know the system. A model sees a name, a description, an input schema, and whatever result comes back. From that contract, it must decide whether the tool fits the task, construct valid arguments, interpret the response, and choose what to do next.
Weak contracts turn ordinary ambiguity into agent failures.
A tool named update_record accepts a free-form object. Which record? Which fields? Does an omitted value stay unchanged or become empty? Can the call be retried? What happens if the record changed after the agent read it?
The model may still produce a valid call. It may even succeed. That does not mean it did the right thing.
Designing tools for models is interface design under uncertainty.
Start With One Clear Job
A useful tool should make its purpose obvious before the model reads the parameters.
Compare:
manage_project
Performs an action on a project.
with:
create_project_task
Creates one task in an existing project after the project and assignee have been resolved to IDs. Does not send notifications or change other tasks.
The second definition tells the model what the tool does, when it fits, and what it does not do.
This matters most when several tools look similar. search_customers, get_customer, and update_customer_email create a clearer decision surface than one customers tool with an action string and a large union of unrelated fields.
Names should describe observable operations. Descriptions should explain:
- when to use the tool
- the important preconditions
- the side effect it causes
- what it deliberately does not do
- which nearby tool to use instead when the boundary is easy to confuse
Anthropic's current troubleshooting guidance makes the same distinction: when a model chooses the wrong tool, differentiate tools by when to use them, not only what they do. OpenAI's function-calling guide likewise defines the description as the place for details on when and how to use a function.
The description is not decorative documentation. It is part of the model's decision environment.
Make Invalid States Hard to Express
Once the model selects a tool, it has to build the arguments.
The schema should narrow that job.
Prefer an enum such as priority: low | normal | high over “enter a priority.” Use an integer with a meaningful range instead of a numeric-looking string. Mark required fields as required. Reject unexpected fields. Describe units, formats, and identifiers where the field appears.
If a task needs a project ID, say whether the value is an internal UUID, a human-facing key such as WEB-42, or either. If a timestamp must include a timezone, encode and describe that requirement. If an empty string means something different from an omitted field, do not leave the distinction implicit.
OpenAI and Anthropic both support strict schema enforcement for tool calls. The current MCP tool specification also defines tool inputs with JSON Schema and can define structured outputs. These features reduce malformed calls. They do not prove that a well-formed call matches the user's intent.
The practical rule is:
Use the schema to enforce shape, and use application logic to enforce meaning.
An enum can prevent urgent-ish. It cannot determine whether this task is truly high priority. A string format can reject a malformed account ID. It cannot prove that the account belongs to the current customer.
Keep schemas as small and flat as the job permits. Deeply nested objects, fields with similar names, and multiple ways to express the same operation increase the model's argument-construction burden. Complexity is sometimes necessary. It should represent domain complexity, not an API's historical accidents.
Separate Looking From Acting
Models often need to resolve a target before changing it.
That is easier and safer when read tools and write tools have distinct contracts:
search_projects(query)returns a small list of candidates.get_project(project_id)returns the current project and its version.create_project_task(project_id, title, assignee_id, expected_project_version)performs one bounded write.
This sequence gives the model evidence before authority.
It also gives the surrounding system useful control points. Search can be broadly available. A write can require confirmation. The UI can show the resolved project, assignee, and proposed task before execution.
Do not rely on the model to turn a vague name into the right record inside a destructive tool. If delete_customer(name) silently chooses the first fuzzy match, the problem is the interface, not merely the model.
This separation complements sandboxing and tool permissions. A sandbox limits where an agent can act. A precise tool limits what one action means.
Return Evidence, Not a Victory Sentence
A tool result becomes the model's next observation.
Success is rarely enough.
A useful write result might include:
{
"status": "created",
"task_id": "TASK-1842",
"project_id": "PROJECT-17",
"title": "Confirm launch checklist",
"assignee_id": "USER-9",
"version": 1,
"created_at": "2026-08-20T11:15:00Z"
}
This lets the model confirm the target, cite the new identifier, and avoid claiming that some unreported side effect occurred.
Read tools should return the fields needed for the next decision, not an unbounded dump of the backing database. Paginate large collections. Put machine-readable facts in stable fields. Keep human-readable explanations available, but do not force the model to extract every state transition from prose.
The current MCP tool contract supports optional output schemas and structured content. That is useful because the consumer can validate the result it receives. The same principle applies outside MCP: if later decisions depend on a field, give that field a stable name and type.
Errors Should Teach the Next Move
An error is not only a failure report. In an agent loop, it is guidance for recovery.
Compare:
Request failed: 400
with:
{
"error": "invalid_assignee",
"message": "USER-9 is not a member of PROJECT-17.",
"recoverable": true,
"suggested_next_tool": "list_project_members",
"project_id": "PROJECT-17"
}
The second result distinguishes a correctable input problem from a service outage or policy denial. It gives the model a bounded next step without pretending the recovery is automatic.
Useful error contracts distinguish at least:
- invalid input
- missing or ambiguous target
- permission or policy denial
- version conflict
- rate limit or temporary dependency failure
- internal failure with no safe automatic retry
Do not include secrets, stack traces, or private implementation details merely to make an error verbose. Return the information the caller needs to decide whether to correct, retry, ask, or stop.
Traces should preserve these tool results and recovery decisions. That is the connection to giving an agent a flight recorder: observability shows where the interface confused the model, while better tool design removes the confusion at its source.
Make Retries Safe—or Make Them Explicitly Unsafe
Agents retry. Networks time out. A model may not know whether a tool completed before the connection failed.
For consequential actions, support an idempotency key or another deduplication mechanism. If the same request is submitted twice with the same key, the system should return the original result instead of creating a second payment, message, ticket, or deployment.
Where idempotency is impossible, state that clearly and require a fresh read or human confirmation before retrying.
Version checks help too. An expected_version field lets the server reject an update when the underlying record changed after the agent read it. The agent can then retrieve the new state rather than overwrite someone else's work.
Some actions need a compensating operation: cancel a queued job, archive a mistakenly created task, or reverse a reversible transaction. “The model can call the API again” is not a recovery design.
Stability Matters More Than Cleverness
Tool contracts become part of prompts, traces, tests, and saved sessions. Quietly changing a field name or result shape can break behavior even when the backend API still works.
Prefer stable, boring interfaces. Add fields compatibly. Version genuinely breaking contracts. Keep aliases or adapters during migrations where practical. Record the tool version in traces so a failure can be reconstructed against the interface the model actually saw.
Do not expose every backend endpoint just because it exists. A small set of task-shaped tools is often more usable than a complete mirror of an internal API. More choices create more opportunities for overlap, argument mistakes, and unnecessary calls.
This does not mean every tool must be tiny. It means each tool should have one coherent reason to exist.
Evaluate the Contract, Not Just the Model
Tool-use failures are often blamed on the model first.
Sometimes the model is the problem. Sometimes two descriptions overlap, the schema permits nonsense, the result omits the identifier needed for the next step, or every error looks identical.
Build evaluations around the interface:
- Does the model choose the right tool—or correctly choose no tool?
- Does it resolve the target before acting?
- Are required arguments correct, not merely valid?
- Does it recover from an actionable error?
- Does it stop on a policy denial or non-retryable failure?
- Does a timeout produce a duplicate action?
- Can the final answer cite the actual tool result?
Use real traces to create regression cases, after removing sensitive data. Test the same tasks when a schema, description, provider, or model changes. That keeps tool quality tied to outcomes rather than a few polished demonstrations.
A Practical Review Checklist
Before giving a model a new tool, ask:
- Can a reader tell when to use it from the name and description?
- Is its job distinct from nearby tools?
- Are inputs constrained to the smallest useful shape?
- Are identifiers, units, formats, and side effects explicit?
- Does the server validate authorization and business rules?
- Does the result provide evidence for the next decision?
- Do errors say whether to correct, retry, ask, or stop?
- Are retries idempotent or clearly unsafe?
- Can changed state be detected before overwriting it?
- Is the contract versioned and covered by evals?
A powerful model can compensate for a weak interface some of the time. That is not a property to build around.
The durable path is to make the right action easy to describe, the wrong action hard to express, the result easy to verify, and failure safe to recover from.
That is what turns an API endpoint into a tool a model can actually use.
