AI coding

Writing Tool Documentation a Model Reads Correctly

Antonio Evans9 min read

A close-up of a JSON tool schema on a dim screen, with five highlighted keys: preconditions, units, enum, side_effects, and errors.
Key points

Key points

  1. 1A tool description is a contract the model reads before the call, and it needs six explicit fields: preconditions, units, enumerations, side effects, a per-error-code meaning, and a recovery step.
  2. 2Descriptions of tool behavior in an MCP listing are metadata supplied by the server, and the specification tells hosts to treat them as untrusted unless the server is trusted.
  3. 3Constraining a schema at the point of generation removes the whole class of parse and missing-field failures that free-form output produces, so the cheapest fix is a tighter schema rather than a better error message.
  4. 4Structured, machine-readable error codes with recovery hints are what let an agent retry intelligently instead of looping on the same broken payload.

Why the pre-call contract beats the post-call error

Most teams debug a tool integration from the inside out. The agent sends a call, the call fails, and someone improves the error message. That work is worth doing, but it is the wrong default. An agent that fails on a call has already spent a round trip, a slice of its step budget, and whatever context it burned deciding to call that tool. An agent that never makes the malformed call spends nothing.

The separation matters because the two artifacts have different audiences and different lifetimes. The tool definition is assembled into the model's context on every call, alongside the system message and the user message, and it is re-sent each turn (the Prompt Engineering Guide). The schema is the static part of that prefix. The error message is returned after generation and consumed only in the recovery path. If the only place a constraint exists is the error, the model learns it by failing, one budget unit at a time.

The Model Context Protocol formalizes where the contract lives. Each tool is identified by a name and includes metadata describing its schema, and clients discover that metadata through a tools/list request before any tools/call happens (the MCP tools specification). The server controls exactly what the model sees, which means the six fields below belong in that listing, not in a wiki page a human reads.

There is a trust caveat worth stating plainly. The MCP specification's security section requires that descriptions of tool behavior, such as annotations, "should be considered untrusted, unless obtained from a trusted server," because tools represent arbitrary code execution (the MCP specification). So the contract you write is a contract the model and the host both have reason to scrutinize. Write it as checkable fact, not as marketing copy.

Field one: preconditions as checkable facts

A precondition is only useful to a model if it can be evaluated against something the model knows or can look up. "The user should have permission" is not checkable. "The caller must hold scope mcp:tools, otherwise the call returns 403 with code INSUFFICIENT_SCOPE" is checkable, and it mirrors how authorization scopes are advertised in the MCP authorization flow (the MCP authorization tutorial).

A practical test: for each precondition you write, ask who evaluates it and when. There are only three acceptable answers. The model evaluates it before calling, the schema makes the violation unrepresentable, or the server enforces it and the error code names the enforcement. Anything else is decoration.

Work through a file-reading tool. The ambiguous version reads "Reads a file from the repository." The checkable version reads:

  • The repository must be checked out and the working directory must be the repository root.
  • path must be relative; absolute paths are rejected with ABSOLUTE_PATH_REJECTED.
  • The file must exist at the time of the call, or the tool returns FILE_NOT_FOUND with the resolved path it tried.
  • Binary files return BINARY_NOT_SUPPORTED rather than garbage content.

Four lines, four facts a model can act on. The Zuplo analysis of agent-callable APIs makes the same point from the REST side: prose documentation is useless to an agent, which needs machine-readable schemas describing what an endpoint does, when to use it, what preconditions must be met and what side effects it produces (Zuplo).

Field two: units, named and bounded

Units are the single most common silent corruption in tool calls, because a wrong unit still validates as the right type. A schema that says {"type": "integer"} for a timeout accepts 500 whether the caller meant 500 milliseconds or 500 seconds. Nothing fails loudly. The call just times out or fires too early.

Name the unit in the property description and, where you can, pair it with a bound:

{
  "timeout_seconds": {
    "type": "integer",
    "minimum": 1,
    "maximum": 300,
    "description": "Request timeout in seconds, not milliseconds. Default 30. Values above 300 are rejected."
  }
}

The bound does double duty. It stops absurd values, and the description's phrase "not milliseconds" forecloses the specific confusion that would otherwise survive the type check. The same treatment applies to bytes versus megabytes, ISO 8601 versus epoch, and percentages versus fractions. The machine learning tool design survey lists tight schemas with explicit format patterns among the practices that make invalid states impossible rather than merely discouraged (Machine Learning Mastery).

A worked example makes the cost visible. An agent session that needs 20 tool calls, at a per-call reliability of 95 percent, has a probability of at least one invalid output of 1 minus 0.95 to the 20th power, which is 64 percent. At 99 percent per call, the same session still fails 18 percent of the time (Siddhant Khare's agent handbook). Compounding is why a unit error on a frequently called tool is so expensive: it is not one failure, it is a failure probability you pay on every step of every session.

Field three: enumerations instead of descriptions

When a parameter has a small set of legal values, enumerate them. The reason is structural: an enumeration eliminates a class of plausible but invalid outputs, and validation failures surface at the tool boundary instead of as cryptic downstream errors (Machine Learning Mastery).

Compare these two property definitions for the same parameter.

ApproachProperty definitionWhat the model can produce
Prose only"type": "string", description: "Task priority, usually low, medium or high.""high", "High", "urgent", "P2", "blocker"
Enumerated"type": "string", "enum": ["low", "medium", "high"]"low", "medium", "high"

The prose version invites the model to helpfully map "urgent" onto your scale. The enumerated version makes the mapping unrepresentable and forces the decision elsewhere, ideally in your prompt, where it can be stated once and reviewed. The OpenAI structured outputs documentation summarizes why this matters: with schema enforcement you no longer need to worry about the model omitting a required key or hallucinating an invalid enum value (OpenAI).

Two caveats keep enumeration from backfiring. First, do not over-constrain: a schema that accepts only values your current data happens not to contain will reject valid future requests, one of the common mistakes in structured output adoption (James M.). Second, an enumeration is a wire format, not a taxonomy. If the model must frequently choose between eight near-synonymous values, the problem is the tool's design, not the enum. The agent-tool-design literature recommends one tool per responsibility rather than an action parameter that multiplexes behaviors, because a multi-action tool forces the model to solve the routing problem before it can solve the task (Machine Learning Mastery).

Field four: side effects, stated before they happen

Side effects are the part of the contract that protects the user, not just the integration. The MCP specification's trust and safety principles require hosts to obtain explicit user consent before invoking any tool, and to show clear UI indicating what each tool does before authorization (the MCP specification). A host can only render a meaningful consent prompt if your description says what will change.

Write side effects as a list of verbs with their blast radius:

  • Writes: creates or overwrites the file at path; parent directories are not created.
  • Charges: none.
  • Notifications: none.
  • Reversible: yes, if backup_existing is true and the previous content is written to path.bak.

And when a tool is not idempotent, say so, with the mechanism that makes retries safe. The monday.com API documentation is a clean model here: send a unique Idempotency-Key with a mutation, and a retry with the same key returns the cached response with an Idempotency-Replayed: true header instead of creating a duplicate, with cached responses expiring after 30 minutes (monday.com). That single sentence, placed in the tool description, tells the model both that retrying is safe and how to make it safe.

The failure this prevents is not the dramatic one. It is the half-done workflow: the contact record is created and the subscription call fails, leaving thousands of inconsistent records with no error anyone acted on (Composio). If the description states the order of writes and which one is the commit point, the model can plan around it instead of discovering it.

Field five and six: error codes with a meaning and a recovery step

Every error code in your tool should get two sentences in the description: one line for what it means, one for what to do next. That is the difference between an error that halts an agent and an error that corrects it. The guidance from agent-facing error design puts it sharply: every error must tell the agent what to do next, because the error message is documentation read at runtime rather than before it (Szymon Pacholski).

A comparison table is the most compact way to ship this.

CodeMeaningRecovery step
VALIDATION_FAILEDA field violated its declared constraint; the response names the field.Fix the named field and resend; do not retry unchanged.
FILE_NOT_FOUNDThe path does not exist at call time.List the parent directory before retrying.
INSUFFICIENT_SCOPECredentials lack the required scope.Stop; ask the user to re-authorize. No retry will succeed.
RATE_LIMITEDQuota exhausted; response carries a Retry-After value.Wait the indicated time, then retry once.
IDEMPOTENCY_CONFLICTA request with this key is in flight.Wait the Retry-After interval and read the cached result.

The last two rows come straight from published behavior. monday.com returns 409 Conflict with a Retry-After header when a request with the same idempotency key is already being processed, and its guidance says not to retry after a rate limit with an idempotency key at all, because the original request was never processed and there is no duplicate risk (monday.com). Those two facts in a table are worth more to a model than a page of prose.

The distinction to keep clear is interface misuse versus semantic misuse. A controlled study of schema-first tool APIs found that schema conditions reduced interface misuse, meaning violations of the input contract, but did not reduce semantic misuse, meaning calls that were schema-valid yet wrong for the task (arXiv). Your error table is a tool for the first category. For the second, the fix is a narrower tool, not a longer error.

A worked rewrite: one tool, before and after

Here is a fictional but representative rewrite of a billing tool description. The first version is the kind of thing that passes code review and fails in production.

charge_customer: Charges a customer. Takes an amount and a currency.

Now the same tool as a six-field contract:

{
  "name": "charge_customer",
  "description": "Charge a stored payment method once. Preconditions: customer_id must reference an active customer with a saved payment method; the account must not be in collections. Amount is in the currency's minor unit (cents for USD). Side effects: creates one charge, sends one receipt email, and is idempotent per idempotency_key for 30 minutes. Errors: CUSTOMER_INACTIVE (stop, no retry), NO_PAYMENT_METHOD (ask user to add one), CARD_DECLINED (stop, surface reason), DUPLICATE_CHARGE (a charge with this key already exists, return it instead of retrying).",
  "inputSchema": {
    "type": "object",
    "properties": {
      "customer_id": {"type": "string", "description": "Customer identifier, prefix cus_."},
      "amount_minor": {"type": "integer", "minimum": 50, "description": "Amount in minor units of currency. 2500 means 25.00 USD. Not dollars."},
      "currency": {"type": "string", "enum": ["usd", "eur", "gbp"], "description": "ISO 4217 lowercase code."},
      "idempotency_key": {"type": "string", "description": "UUID generated once per intended charge. Reuse on retries, never regenerate."}
    },
    "required": ["customer_id", "amount_minor", "currency", "idempotency_key"]
  }
}

Count what changed. Units are named twice, once in prose and once in the property description. The enumeration replaces a sentence of guidance. Side effects enumerate a write, an email, and a deduplication window. Each error code carries a recovery instruction, and two of them explicitly say "stop," which is the most valuable instruction a description can give a model in a retry loop.

The research on this practice is uneven, and it is worth saying so. The controlled study cited above found that schema conditions reduced interface misuse without improving end-task success in its constrained local inference setting, and its authors describe that as a pilot result (arXiv). The honest reading: a terse contract reliably removes malformed calls, and it does not compensate for weak planning. That is a reason to write the contract and also a reason not to expect it to fix everything.

Rolling the contract out across a toolset

Once one tool is written this way, the work becomes consistency. Three habits keep a toolset coherent.

First, keep the tool list deterministic and stable. The MCP specification recommends returning tools in a deterministic order so clients can cache the list reliably and prompt cache hit rates stay high when tools are in the model's context (the MCP tools specification). Reordering tools for cosmetic reasons is a real cost.

Second, treat the tool list as a token budget. Tool definitions are part of the context on every call, which means they consume tokens and affect cost and latency, so they should be concise (the Prompt Engineering Guide). The six-field contract is tight by design: it adds facts, not adjectives. If a description needs a paragraph of history to make sense, the tool is doing too much.

Third, audit for the failure modes that look fine in a demo: unfiltered API exposure, silent partial success, and overlapping tool names that make selection a coin flip (Machine Learning Mastery). Overlapping names are a description problem. If get_user and fetch_account both plausibly handle a lookup, the model's choice is arbitrary, and no amount of error-code detail repairs an arbitrary choice.

A practical audit takes an afternoon. Dump your tools/list output, walk each entry against the six fields, and mark any field that is missing or unverifiable. Then run one representative session per tool and log every error code the model actually encountered. Codes that never appear are candidates for deletion. Codes that appear but have no recovery sentence are the ones to fix first.

If your toolset spans text, image and audio models and you want one place to run the resulting agent, Quanta Inference exposes every model type through a single OpenAI-compatible API key, which keeps the contract work portable across vendors (https://quantaintelligence.ai/inference). For teams that would rather start from a working expert than assemble one, Quanta Experts reads your context and returns finished deliverables (https://quantaintelligence.ai/agents).

FAQ

FAQ

Where exactly should the contract live, the description string or the schema?

Both, with a division of labor. The schema should carry everything mechanically enforceable: types, bounds, enumerations, required fields. The description string carries what the schema cannot express: preconditions, side effects, and the error-code table. In MCP, all of this travels in the tool listing the model reads before the call (the MCP tools specification), so there is no separate documentation step and nothing to fall out of date.

How long should a tool description be?

Long enough to carry the six fields and no longer. Tool definitions sit in context on every call and consume tokens on every request (the Prompt Engineering Guide), so length is a recurring cost. The test is whether each sentence is a fact a reader could check: a unit, a bound, an enumeration, a side effect, an error meaning. Sentences that describe the tool's virtues get deleted.

Does a stricter schema fix semantic errors, where the model calls the right tool for the wrong reason?

No. A controlled study found that schema conditions reduced interface misuse, meaning contract violations, but did not reduce semantic misuse, meaning calls that were valid but unproductive for the task (arXiv). Semantic problems usually trace to tool scope and naming, so the fix is splitting an overloaded tool or renaming an ambiguous one rather than adding more constraints to the schema.

What is the smallest useful error-code table?

The codes each tool can actually return, one line of meaning, and one line of recovery, with an explicit "stop, do not retry" wherever retrying cannot help. Published idempotency documentation shows the value of that last part: retrying a rate-limited mutation with an idempotency key is explicitly the wrong move, while retrying after a timeout is the right one (monday.com). If your table does not distinguish those cases, the model will discover the difference by burning budget.

How do I know the description is working?

Measure interface misuse directly: the count of calls rejected for schema or contract violations per session. That is the metric schema work moves, and it is separate from task success, which the same study found dominated by semantic and budget constraints (arXiv). Track it before and after a rewrite of one tool. A falling misuse count on that tool, with task success unchanged, is the expected and useful result.

Sources

Sources

  1. Function Calling in AI Agents | Prompt Engineering Guide
  2. Tools
  3. Specification
  4. Understanding Authorization in MCP
  5. How to Design APIs That AI Agents Can Actually Use
  6. AI Agent Tool Design: What Works and What Doesn't
  7. Ch. 30: Structured Outputs & Function Calling
  8. Structured model outputs | OpenAI API
  9. Structured Outputs: When Your AI Needs to Follow a Schema
  10. Idempotency
  11. AI Agent Security: Why Reliability is the Missing Defense ...
  12. Error Handling for AI Agents: A Different Standard Than ...
  13. Schema First Tool APIs for LLM Agents: A Controlled Study ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles