---
title: "What an agent actually needs from a tool"
description: "After a year of building MCP servers for real workflows, the tool surface matters more than the model behind it. Seven rules I now apply before a tool ships."
date: 2026-07-21
tags: ["agents", "mcp", "api-design"]
language: en
canonical: https://aissamirhir.com/blog/what-an-agent-needs-from-a-tool
source: aissamirhir.com
---
A tool is an API with the worst client you will ever have. The model does not read your docs. It does not retry with a smarter query after a stack trace. It gets one description, one schema, and whatever your tool returns, and from that it has to decide whether to call you again.

I have built MCP servers for order reconciliation, content pipelines and a few internal systems I cannot name. The model kept getting better underneath them. The failures that stayed were mine, and they were almost always in the tool surface. These are the rules that fixed them.

## 1. The description is the contract

The description is the only documentation the model will read, so it has to say three things: when to use the tool, when not to, and what comes back. A name is not enough.

```json
{
  "name": "orders_search",
  "description": "Find orders by customer email, order number or date range. Use this before orders_update to get the order id. Returns at most 50 orders, newest first, with a cursor for more. Does not return line items — call orders_get for one order's details.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "Email, order number, or free text." },
      "since": { "type": "string", "format": "date" },
      "cursor": { "type": "string" }
    },
    "required": ["query"]
  }
}
```

The sentence about `orders_get` prevents the most common failure I saw: the model searching again, with a different phrasing, hoping the line items would appear.

## 2. Small inputs, strict schemas

Every optional field is a decision the model has to make. Enums beat free text. Dates in one format. A `limit` with a hard cap on the server, whatever the input says. I generate the schemas from Zod so the validation and the description cannot drift.

```typescript
const SearchInput = z.object({
  query: z.string().min(2).describe('Email, order number, or free text.'),
  status: z.enum(['open', 'paid', 'shipped', 'cancelled']).optional(),
  since: z.string().date().optional(),
  cursor: z.string().optional(),
})
```

## 3. Return errors the model can act on

An exception is a dead end. A structured error with a hint is a next step.

```typescript
return {
  ok: false,
  error: 'ORDER_NOT_FOUND',
  hint: 'No order matches "KF-1042". Order numbers look like KF-<digits>; try orders_search with the customer email instead.',
}
```

The `hint` field changed behaviour more than any prompt I wrote. It turns a retry loop into a single corrected call.

## 4. Anything that writes takes an idempotency key

Agents retry. Networks flake. An `orders_refund` tool without an idempotency key will, eventually, refund twice. Require the key, store the result under it, and return the stored result when the same key comes back.

## 5. Paginate with cursors, and cap it

Offsets break when the data moves under the agent, and it always moves. Return a `cursor`, accept a `cursor`, and never return more than the cap even if asked. The description should mention the cap so the model plans for it.

## 6. One capability per tool, and reads never write

`orders_search` never mutates. `orders_update` never searches. Destructive tools are separate, named as such, and in my servers they require an explicit confirmation argument the model must set. Scoping this way also makes permissions legible: a read-only deployment simply does not register the write tools.

## 7. Evaluate it like software

A tool that works in a demo and fails on the tenth conversation is not done. I keep golden transcripts, replay them against every change, and score the outputs. [Assay](https://github.com/assay-ai/assay) is the small evaluator I maintain for this: it scores model output against a set of quality metrics so a regression shows up as a number, not a feeling.

> [!WARNING]
> Never return secrets in a tool result. Not a token, not a connection string, not a full customer record when the model asked for a name. The model will echo what it sees into the conversation, and from there into logs you do not control.

## The symptoms, in case you are debugging one now

| Rule broken | What you see in the transcript |
|---|---|
| Vague description | The model calls the wrong tool, or the right one for the wrong reason |
| Loose schema | Dates in three formats, limits of 10 000 |
| Thrown errors | The same call repeated with small rewordings |
| No idempotency key | Duplicate writes after a timeout |
| Offset pagination | Skipped or repeated records mid-run |
| Mixed read/write | A "search" that changed something |
| No evaluation | It worked yesterday |

None of this is exotic. It is API design, applied to a client that cannot read between the lines. The model is not the hard part; the surface you hand it is.
