CustomLabs
Tool design

The Agent Tool Interface.

Six surfaces, 24 named design rules, and five ways a tool interface fails. The tools and the context an agent works through are the product surface, and most "the agent isn't reliable" complaints are interface problems, not model problems.

Updated First published

26 min read

Markdown

An agent doesn't experience your system the way a person does. It experiences a list of tool names, a schema per tool, whatever context got assembled ahead of it, and whatever comes back from the call it decided to make. Every one of those is a design decision someone made, deliberately or by default, and a system that looks unreliable is very often a system whose interface was never designed at all.

This isn't a case for a specific protocol or a specific framework. MCPMCP is an open standard for connecting LLM applications to tools and data sources. standardizes how a tool call crosses a process boundary; it says nothing about how many tools you should expose, what to name them, or what a good response looks like. Those calls are still yours to make, on every surface below, whether the transport is MCP or a plain function call in your own codebase.

The six surfaces

Six surfaces, each blind to the next.#

Every surface controls one part of the interface and is blind to the rest. That's exactly why getting one right doesn't get you the others.

Diagram in six lanes, one per tool-design surface: tool catalog and naming; tool contracts; and tool responses. The other three are context assembly and budget; permissions and blast radius; and transport and operations. Each lane holds one node that names what that surface controls. No edges connect the surfaces; each layer is blind to a different failure. TOOL CATALOG & NAMING TOOL CONTRACTS TOOL RESPONSES CONTEXT ASSEMBLY & BUDGET PERMISSIONS TRANSPORT & OPERATIONS Which tools exist Shape of inputs What comes back Token budget Allowed to do Crosses a boundary
Six interface surfaces sit between an agent and its tools. Each one is blind to a different failure the next layer catches.
01

Tool catalog & naming

Controls: Which tools exist in a session, and what they are named.

Blind to: Whether an individual tool's schema is well designed once it's picked.

Get it wrong: Near-identical tools crowd the list and the model guesses between them.

02

Tool contracts (the schema)

Controls: The shape of a single tool's inputs.

Blind to: Whether the result that comes back is any good.

Get it wrong: A loose schema lets the model invent a plausible-looking argument instead of supplying a real one.

03

Tool responses (what comes back)

Controls: What the model actually reads on the way back from a call.

Blind to: Whether the call was the right one to make in the first place.

Get it wrong: A response built for a human in a debugger buries or drops the one thing the model needed.

04

Context assembly & budget

Controls: What actually occupies the token budget at each step: tool defs, retrieved content, history, the task.

Blind to: Whether any individual piece placed in context is trustworthy.

Get it wrong: The task itself is the first thing truncated, because it was assembled last with no reserved share.

05

Permissions & blast radius

Controls: What a tool call is actually allowed to do once it executes.

Blind to: Whether the call needed to happen at all, or was named and scoped sensibly.

Get it wrong: A shared, broad credential means every bad call inherits the reach of the most dangerous tool in the session.

06

Transport & operations

Controls: How a tool call crosses a process or team boundary, and whether anyone can reconstruct what happened afterward.

Blind to: Whether the tool on the other side of that boundary is well designed on the inside.

Get it wrong: A protocol ships with no operations story behind it. Nobody can say who called what, or trust the version they pinned.

The rule bank

24 rules, six surfaces.#

Filter by applicability (read-only, writes, autonomous), then copy the visible list as a Markdown checklist.

01 Tool catalog & naming

Every tool definition sits in context on every single turn, whether or not that turn needs it. A session with forty tools pays for forty schemas before the model reasons about anything real. The catalog layer decides which tools exist at all, what they're called, and how many a session actually loads. It's usually the first place a token budget quietly disappears. Get the count and the names right, and most of tool-selection accuracy follows for free. Get it wrong, and no amount of context engineeringContext engineering decides what occupies a model's context window at every step, and in what order. downstream fixes a model choosing between two tools it can't tell apart.

Why
A tool count that keeps growing widens the set of near-identical choices a model has to pick between on every turn. More tools raises the odds of a wrong pick, not the odds of a right one.
Build it
Group calls that share a workflow into one tool with a mode or action parameter: one `manage_ticket` tool with create/update/close, not three. Retire a tool outright once a broader one already covers its job.
Prove it
Track tool-choice accuracy on a scripted scenario set as the catalog grows (the trajectory layer on /evals/). Treat a drop as a signal to consolidate, not a signal to write a better prompt.
Why
Two tools named `search` from different systems are indistinguishable to a model reading a flat tool list. It has no way to know which `search` is Jira's and which is the internal wiki's.
Build it
Prefix every tool name with the system it belongs to (`jira_search`, `wiki_search`), so ownership is legible from the name alone, before the model ever reads the description.
Prove it
Grep the live tool list for a name collision or a missing prefix before every new integration ships. Add a lint check once the catalog is assembled from more than one source.
Why
A tool that reads under most calls and quietly writes under a specific parameter combination gets called with the read half's confidence and the write half's consequences.
Build it
Split any tool whose action depends on a parameter value into two tools, one read and one write. Give each its own name, its own schema, and its own permission grant.
Prove it
Audit the catalog for a single tool name backing more than one permission scope, and treat a match as a design defect to fix, not a convenience worth keeping.
Why
Every tool definition sits in the context window on every turn, whether or not that turn needs it. A forty-tool catalog is a permanent tax the task pays, regardless of which four tools it actually calls.
Build it
Cap the number of tools loaded per session. Select the subset a task needs at session start, via an intent router or a fixed profile per task type, instead of loading the entire catalog by default.
Prove it
Measure the token share tool definitions occupy at session start, track it next to the task's own tokens, and alarm when it crosses a stated budget. See budget-the-window below.

02 Tool contracts (the schema)

A tool's schema is the only conversation the model has with the tool before it makes the tool call. No comments, no onboarding doc, just field names, types, and whichever constraints the schema author bothered to encode. Leave that loose and the model fills the gap the way it fills any gap: by generating something plausible-shaped, whether or not a real value exists behind it. Tool callingTool calling lets a model request a structured action instead of only generating text. is only as reliable as this contract; 'the model hallucinated an argument' gets fixed here, with an enum or a validated pattern, not a better prompt.

Why
A parameter named `user` leaves open whether it wants an ID, an email, or a display name. The model fills that ambiguity with whichever it saw most in training, not whichever the tool actually expects.
Build it
Name every parameter for the specific thing it is: `user_id`, not `user`; `start_date_iso`, not `date`. State the unit or format in the name itself wherever one exists.
Prove it
Read every parameter name cold, with no description attached, and confirm it's unambiguous on its own. If it needs the description to disambiguate, rename it instead of documenting around it.
Why
A free-text field for a known, bounded value set invites the model to generate a plausible-looking value instead of picking a real one. That is the same completion behavior that produces a hallucinated ID.
Build it
Constrain every field with a small, fixed value set to an enum in the schema, and validate server-side that whatever comes through actually belongs to it. Never trust the schema alone to enforce itself.
Prove it
Track the invalid-argument rate on enum-constrained fields against free-text ones (the tool-call-argument-check on /evals/), and confirm the gap is large. If it isn't, the enum isn't constraining anything.
Why
A field marked optional that the tool actually needs just moves the failure downstream. The call succeeds at the schema layer and fails at execution, with a worse error and less context to explain why.
Build it
Mark `required` based on what the tool genuinely needs to act correctly, not on what's convenient to default. If a missing field always produces a degraded or wrong result, it's required.
Prove it
Sample recent execution failures and check how many trace back to a field that was optional in the schema but effectively mandatory in practice. Each one is a schema fix, not a validation fix.
Why
A tool description written for someone who already knows the underlying API assumes exactly the context the model doesn't have. Units, formats, and relationships between resources have to be stated, not implied.
Build it
Write the description the way you'd brief a new hire on their first day. State what the tool does, what a valid input looks like, what units and formats it expects, and how it relates to the tools around it.
Prove it
Hand the schema and description alone to someone unfamiliar with the system and ask them to predict a valid call; a description that requires guessing needs a rewrite.

03 Tool responses (what comes back)

What a tool sends back becomes the model's entire picture of what just happened. There's no side channel for 'this actually failed' unless the response format carries one. A raw database dump, a wall of UUIDs, an HTTP 200 wrapping an error string: none of these are wrong exactly. They're optimized for a human glancing at a debugger, not a model reasoning about the result next turn on a fixed token budget. That's what 'high-signal' actually buys on this layer: a model that notices a failure, instead of one that reports an absence of data as a fact.

Why
A response full of UUIDs gives the model a value it can pass to the next call, but nothing it can reason about. It can't tell two records apart the way a human would by name.
Build it
Include a human-readable name or label alongside every identifier a response returns. That gives the next reasoning step something legible to check its work against.
Prove it
Read a sample of raw tool responses cold and check whether the meaningful content is legible without a lookup. If every row is an ID and nothing else, add a name field.
Why
A full record dump forces the model to find the one relevant field itself, on every call, at the cost of every other field's tokens. The noise makes the actual signal easier to miss.
Build it
Return only the fields the next decision actually needs by default, with an explicit parameter to request more detail when a task genuinely calls for it. Stay narrow by default, and go wide only on request.
Prove it
Measure average response size in tokens against the share of fields a downstream step actually references (cost-per-completed-task on /evals/), and trim what nothing ever reads.
Why
An unbounded result set either blows the context budget on one call, or gets silently truncated with no signal that anything was cut. Either way, the model doesn't know it's looking at a partial answer.
Build it
Set a sane default page size on every list-returning tool. When a response is truncated, say so explicitly, and state exactly how to narrow the next call: a filter to add, a cursor to pass.
Prove it
Deliberately trigger a truncated response and confirm the model, reading only what came back, can construct a correctly narrowed follow-up call without being told how.
Why
A stack trace or a generic 500 tells the model something failed and nothing about what to do next. It has no route to a fix, so it either repeats the identical call or gives up.
Build it
Return a specific, actionable error, such as 'no matching record for order 4471, check the order still exists,' instead of an implementation-level exception. Give write-scoped tools a distinct error path from read-scoped ones.
Prove it
Deliberately fail a call and confirm the returned error alone is enough for the model to correct its next attempt. If it just repeats the same call, the error didn't teach anything.

04 Context assembly & budget

Every tool definition, every retrieved document, and every prior turn competes for the same fixed context window. Nothing forces that competition to resolve in the task's favor. Left unmanaged, the stable stuff (system instructions, tool schemas) drifts toward the front, where a single reordered field busts prompt caching. Meanwhile the volatile stuff (this turn's actual question) gets buried or evicted first when something has to go. Budgeting context is a decision made once, on purpose, not a truncation strategy improvised the day the window fills up.

Why
A single volatile token near the front of a prompt busts prompt caching for the entire prefix behind it: a timestamp, a reordered tool list, a per-request ID. This happens even though the request looks basically unchanged to a human reading it.
Build it
Order every context assembly the same way on every call: static system instructions and tool definitions first, retrieved content next, whatever changes per turn last. Never reorder tools between calls in the same session.
Prove it
Check your actual cache-read share against provider usage data, not against how stable the prompt looks, and treat a low hit rate as an ordering bug to find.
Why
Without a stated allocation, tool definitions, retrieved content, and history all expand to fill whatever room is available. Whichever one grows fastest quietly starves the others, usually the task, since it's assembled last.
Build it
Write down a token allocation per slice (instructions, tool defs, retrieved content, history, task) before the system ships. Alert when any slice crosses its share, instead of discovering it from a truncated task.
Prove it
Instrument actual token spend per slice on a sample of real sessions and compare it against the written budget. A slice that's silently 3x its allocation is the thing to fix before it causes an overflow.
Why
Compacting only once the window is already full means the eviction happens under pressure, with whatever's oldest or least structured cut first. That's usually exactly the state a long-running agent needed to keep.
Build it
Define the compaction rule in advance: summarize turns older than N steps, or once a slice crosses a stated share of budget. Run it on that schedule, not as a reaction to a budget already exceeded.
Prove it
Force a long run past the point compaction should trigger, and confirm the agent's task state and constraints survive intact. If it forgets its own goal afterward, the rule cut the wrong thing.
Why
A model reads one undifferentiated stream of tokens. Nothing marks 'this is an instruction' apart from 'this is retrieved text,' so a document that reads like a command is functionally indistinguishable from one.
Build it
Mark retrieved content and tool output as data wherever the format allows it: a distinct field, or a wrapping delimiter the system prompt explicitly names as untrusted. Never concatenate it directly into the instruction stream.
Prove it
Run a labelled set of injection attempts planted inside retrieved content and tool responses through the full tool-enabled path (the injection-red-team-set check on /evals/). Track resistance as its own number.

05 Permissions & blast radius

A tool call that can write, send, or delete is a different category of risk than one that can only read. Treating them the same means one flat credential, one shared identity, and no distinction in the catalog. Every prompt injection, every hallucinated argument, and every over-eager retry then inherits the full blast radius of the most dangerous tool in the session. This layer doesn't stop a bad call from being attempted. It decides how far a bad call can actually reach before something requires a human or a rollback.

Why
A single set of credentials covering both read and write access means every read call carries the blast radius of the write it was never going to make. A hallucinated argument or a successful injection inherits permissions it didn't need.
Build it
Grant read access by default, and make any write a genuinely separate tool with its own, narrower credential. Never use one shared scope that happens to allow both.
Prove it
Audit the credential behind every tool and confirm a read-only tool's credential literally cannot perform a write at the provider level.
Why
A write-scoped call that times out can get retried by the harness, by a bounded-loop budget, or by the model itself. Either it mutates twice, or the retry itself becomes the source of runaway spend.
Build it
Generate a unique idempotency key per intended write, and pass it through every retry of that same logical call. The receiving system then recognizes a repeat and no-ops it, instead of executing it again.
Prove it
Deliberately retry a write call with the same key, and confirm the second attempt is a no-op. Then confirm a genuinely new call with a new key still goes through.
Why
An agent that can act autonomously has no backstop once a bad decision reaches an irreversible tool (a refund, a delete, a message sent externally). The call executes exactly as fast as a good one would.
Build it
Require an explicit human confirmation before any tool call that can't be cleanly undone, at minimum until the surrounding controls have an established track record. Gate it in the harness, not as a suggestion in the prompt.
Prove it
Script an adversarial scenario that tries to make the agent issue a refund or send a message it shouldn't, and confirm the checkpoint actually fires. That's the tool-abuse-scenarios check on /evals/.
Why
A tool that deletes, sends, or reaches an open-world resource looks identical to a safe read in a flat tool list with no distinguishing marker. Nothing stops it being treated with the same casualness as a lookup.
Build it
Add an explicit annotation to every tool definition that's destructive, irreversible, or reaches outside a closed, known set of resources. That lets the harness and any reviewer filter on it mechanically, rather than reading every schema by hand.
Prove it
Grep the live catalog for every write-capable or open-world tool and confirm each one actually carries the annotation. An unmarked destructive tool is a defect to fix before the next session loads it.

06 Transport & operations

The moment a tool call crosses a process boundary (a team's API, a vendor's MCP server, a different product entirely), it stops being a function call. It becomes a contract with an operations story: who's allowed to call it, what happens when its shape changes, and whether anyone can reconstruct what happened after the fact. MCP standardizes the wire format for that crossing. It does not standardize the governance around it. Most of what goes wrong at this layer is an org that shipped the protocol and skipped the operations.

Why
Standing up a server, a transport layer, and an auth story for a tool that's only ever called from one process adds real operational surface. That surface guards a boundary that doesn't exist yet.
Build it
Keep a tool in-process, as a plain function call inside your own codebase, until it genuinely needs to cross a team or product boundary. Reach for MCP specifically at the point that boundary becomes real.
Prove it
Before standing up an MCP server for a new tool, name the specific other team or product that will call it. If the honest answer is 'nobody yet,' the boundary hasn't arrived.
Why
A tool's schema that changes shape with no version marker breaks every agent that already pinned to the old shape. It happens silently, the moment the new version deploys, and there's no compiler to catch it.
Build it
Version every tool contract explicitly, and keep the old version live through a stated deprecation window. Require agents to pin a version, rather than always resolving to whatever is newest.
Prove it
Ship a deliberate breaking change behind a new version and confirm an agent still pinned to the old one keeps working through the entire deprecation window.
Why
A shared service account behind every agent in a fleet makes it structurally impossible to answer 'which agent did this' after the fact. It also means one agent's scope is every agent's scope.
Build it
Give every agent its own credential, scoped to only what that agent's tools actually need, vaulted rather than embedded in a prompt or a config file checked into source.
Prove it
Pick any tool call in a trace and confirm you can name the specific agent identity that made it. It should come straight from the credential, not the shared account or the underlying human.
Why
Without a call-level log, a regression in cost, latency, or reliability is just a vague complaint about the agent getting slower. There's no specific call, argument, or version to trace it back to.
Build it
Log tool, arguments, outcome, latency, tokens, and cost on every call, structured and queryable. That's the same trace-first discipline the handbook's operate stage applies to the rest of the system.
Prove it
Pick a random recent session and reconstruct its full tool-call trace, cost included, from the logs alone. If any call is missing, or its cost can't be attributed, the logging has a gap.
Five ways a tool interface fails

These aren't model problems.#

Each one is a cause on this page's surfaces above, and a named symptom already documented elsewhere on this site.

Catalog creep

Looks like: Two or three tools in the same session do nearly the same thing under slightly different names. The model picks whichever one it saw more often in training, not whichever one the task actually needed.

Guardrail: Audit the catalog for near-duplicates before adding a new tool, and merge or retire the older one instead of shipping a fourth way to do the same call.

Failure mode: Tool argument hallucination

Context crowd-out

Looks like: Tool definitions and retrieved results together eat most of the window before the model reaches the actual question. The task itself is the first thing truncated when something has to give.

Guardrail: Budget context per slice (instructions, tool defs, retrieved content, task) and alarm when a slice crosses its share, instead of discovering the task got dropped after the fact.

Failure mode: Context overflow drops the task

Cheerful failure

Looks like: A tool call returns HTTP 200 with an empty or malformed body. The model treats the absence of data as a fact worth reporting, not a failure worth flagging.

Guardrail: Give every response a typed status field the model can branch on: ok, empty, or error. A failure then has to be represented as one, not inferred from a body that happens to be blank.

Failure mode: Silent tool failure

Retry storm

Looks like: A write-scoped tool call times out, gets retried automatically, and the mutation it was supposed to make either happens twice or the retry itself becomes the thing burning budget.

Guardrail: Put an idempotencyAn idempotent operation produces the same result no matter how many times it runs. key on every write call, so a retried request is provably a no-op, not a second mutation. Cap retries explicitly, instead of leaving the ceiling unbounded.

Failure mode: Retry-amplified spend

Instructions in the payload

Looks like: A tool's response includes retrieved text that reads like an instruction. The model, with no structural way to tell data from command, follows it.

Guardrail: Treat everything a tool returns as data, never as instruction, and scope every tool to least privilege so a followed instruction still has nowhere dangerous to go.

Failure mode: Injection via retrieved content
What this is built from

Verifiable, not claimed.#

No invented benchmark numbers, no client names. Just what's already documented on this site.

Sources

  1. Model Context Protocol - MCP Specification: Tools (2026-07-28)

    The specification text our tool-design and catalogue rules are checked against. Retrieved 2026-08-24.

  2. JSON Schema - JSON Schema, Draft 2020-12

    The schema specification a tool description must satisfy under our rule bank. Retrieved 2026-08-24.

Not sure which surface is causing it

A Ship Audit checks the tool interface an agent actually runs against (catalog, contract, permissions, and the rest) and tells you which surface is the live risk, not just which one is theoretically incomplete.

Questions

Before you write the schema.#

What teams ask us before they scope a tool catalog.

01 How many tools is actually too many?

There's no fixed number. The failure mode is near-identical tools crowding the model's choice, not a raw count. Audit for overlap before adding a new one, and cap what loads per session to what the task needs.

Link to this answer: How many tools is actually too many?
02 Do we need MCP, or is in-process enough?

Stay in-process until a tool genuinely crosses a team or product boundary. MCP earns its cost, a server, a transport layer, and an auth story, once another team or product actually needs to call the same tool. Standing it up earlier is operational surface with no boundary to justify it.

Link to this answer: Do we need MCP, or is in-process enough?
03 What's the single highest-leverage rule to start with?

Read-only by default, paired with a typed status field on every response. Together they close off the two failure modes that do the most damage early: an over-scoped credential, and a silent failure the model reports as fact.

Link to this answer: What's the single highest-leverage rule to start with?
04 How does this relate to the Eval Stack?

The Eval Stack tells you whether the system works. This page is the interface the agent works through to get there. A contract check on /evals/ is scored against a tool contract built to the rules above. The two are complementary, not overlapping.

Link to this answer: How does this relate to the Eval Stack?
05 Isn't this just prompt engineering?

No. Prompt engineering shapes what you ask the model; this is the surface the model acts through once it decides to do something. A well-designed tool catalog survives a mediocre prompt; a great prompt on top of a badly designed tool interface still fails on the tool call.

Link to this answer: Isn't this just prompt engineering?
06 What's context engineering, and how is it different from prompt engineering?

Context engineering is deciding what occupies the token window at every step (tool defs, retrieved content, history, the task itself) and in what order. Prompt engineering is one input to it, not the whole discipline. See the context surface above.

Link to this answer: What's context engineering, and how is it different from prompt engineering?
Write the contract down

A tool that reports success on a call that actually failed sends a model chasing the wrong fix. This pattern defines a typed contract that catches a malformed call before the model ever sees it.

Source: https://customlabs.io/tool-design/

navigate select esc close