MCP Tool design Engineering

How to design MCP tools your agent will not misuse

Agents do not call the wrong tool because they are careless. They call it because the tool surface is ambiguous. Here are the design rules we apply to every MCP server we build: naming, required parameters, typed errors, server-side authorization, idempotency keys, and how to score selection accuracy before it reaches production.

APFlow
Blog · July 2026 · 9 min read
A monitor showing code in a dark room
Photo: Fernando Hernandez, Unsplash
TL;DR
  • ·Most wrong tool calls are an interface problem, not a model problem. Overlapping descriptions, optional parameter sprawl, and tools that mirror REST endpoints cause the majority of them.
  • ·Design each tool around a decision a human actually makes, make every parameter the operation needs genuinely required, and return structured errors with a suggested next action instead of stack traces.
  • ·Keep authorization and idempotency inside the server, never in the prompt, and score tool selection against a labelled fixture set in CI.

Your first MCP server will work in the demo. The agent calls the one tool you wrote, gets a clean result back, and you ship it. The trouble starts around tool number six, when the model has to choose. It calls search_orders where it should have called get_order, passes a customer name into a field that wanted an ID, retries a timed-out write three times, and creates three refunds. None of that is a model failure. It is an interface failure, and the interface is yours. What follows is the design discipline we apply to every MCP server we build: how to name tools, how to describe them, what to make required, what to return when things break, and how to prove the agent is choosing correctly before real users find out.

Why agents pick the wrong tool

A model picks a tool by reading names and descriptions, the same way a new engineer picks a function out of an unfamiliar SDK. It has no runtime, no type checker, and no way to try one and undo it. Every ambiguity you leave in the text becomes a coin flip at inference time. Three patterns cause most of the misfires. Overlapping descriptions: two tools whose opening sentences are near paraphrases, so there is nothing to separate them on. Optional parameter sprawl: a tool with eleven optional fields gives the model eleven chances to guess, and guesses land in your database. And REST mirroring: a server exposing GET /orders, GET /orders/id, and POST /orders/search as three sibling tools has published its routing table, not a set of capabilities. The model does not know your API conventions. It knows the words you wrote. Rewriting three descriptions is usually a bigger accuracy win than swapping the model.

Design for the decision, not the endpoint

Start from the sentence a human would say, not the route your API happens to expose. Nobody in operations thinks "GET /shipments with a status filter". They think "which of today's shipments are late, and who do I tell". That sentence is the tool. It might fan out to four internal calls, join two responses, and drop every field nobody needs, and that is correct: the server is where the work belongs, because each field you return costs context and each extra call costs a turn. Our test is simple. Can you name the tool with a verb and a noun a domain expert would recognize, and write one sentence about when to reach for it that never mentions HTTP. find_late_shipments passes. query_shipments does not, because "query" is a shape, not a decision. That gap is the difference between wrapping an API and building a capability, and it is most of why MCP is worth the effort at all.

~12 tools
The rough point at which selection accuracy starts to slide on a single server, measured across our own MCP deployments rather than any published benchmark. Past it we split the server by domain and let each client mount only the tools its task needs.

Descriptions that say when not to use the tool

A tool description is a prompt fragment you are writing on behalf of every agent that will ever mount your server. Write it in that spirit. The first sentence states what the tool does and which decision it serves. The second states preconditions: what must already be true before calling it. The third is the one nearly every server skips, the negative case. "Do not use this to look up a single order by ID; call get_order for that." Explicit exclusions are the cheapest accuracy fix available, because they resolve the exact ambiguity the model is stuck on. Keep verbs distinct across the surface: search, get, create, cancel, and never two of them meaning the same thing. If two tools need the phrase "similar to" in their descriptions to be told apart, you either have one tool with a parameter, or a boundary you have not drawn yet.

A tool definition, and the error it returns
{
  "name": "issue_refund",
  "description": "Refund part or all of a settled payment. Use when a customer is owed money back on a payment that has already cleared. Do NOT use to cancel an unsettled authorization: call void_authorization. Do NOT use for goodwill credits: call issue_credit_note.",
  "inputSchema": {
    "type": "object",
    "required": ["payment_id", "amount_minor", "reason_code", "idempotency_key"],
    "properties": {
      "payment_id":      { "type": "string", "pattern": "^pay_[a-z0-9]{16}$" },
      "amount_minor":    { "type": "integer", "minimum": 1 },
      "reason_code":     { "type": "string",
                           "enum": ["duplicate", "defective", "not_received", "goodwill"] },
      "idempotency_key": { "type": "string",
                           "description": "Caller-generated. Repeats return the first result." }
    },
    "additionalProperties": false
  }
}

// what it returns when it cannot proceed
{
  "error": {
    "code": "AMOUNT_EXCEEDS_REMAINING",
    "message": "Refund of 12000 exceeds the 4200 still refundable on this payment.",
    "remaining_minor": 4200,
    "retryable": true,
    "next": "Call issue_refund again with amount_minor of 4200 or less, or call request_approval."
  }
}

Required parameters should be genuinely required

Optional parameters are where agents improvise. If the schema says a field is optional, the model treats it as a suggestion, and it will either omit something you needed or invent something plausible to fill the hole. So anything the operation cannot correctly execute without goes in the required list with no default. Anything with a safe default gets that default applied server side and stays out of the schema entirely. Whatever is left optional should be a small closed set, and enums beat free text every time. A status field typed as a string invites "pending", "Pending", and "awaiting_approval" from the same model on the same day. Typed as an enum of four values it cannot be wrong. Same for money and dates: take integer minor units and ISO 8601 strings, reject everything else at the boundary, and say why. Validation you skip does not disappear. It reappears as a support ticket about a refund that was off by two decimal places.

Return errors the agent can recover from

When a tool fails, the agent has to decide what to do next, and a stack trace tells it nothing useful. Worse, raw exceptions push your internals into the model context and sometimes into a user-facing answer. Return a structured error instead: a stable machine-readable code, a short human-readable message, whatever state the agent needs in order to correct itself, and an explicit flag for whether a retry could possibly help. Codes should be a closed vocabulary you can grep for and count in your logs. The field that changes behaviour most is the last one, a suggested next action: "call get_payment to read the remaining balance", or "this needs human approval, call request_approval". Without it a capable model will improvise a recovery path, and improvised recovery is how an agent ends up calling four wrong tools in a row. Errors are part of your tool surface, not something that falls out of your HTTP client.

Authorization belongs in the server, not the prompt

Never let the system prompt be your access control. Anything expressed as instructions can be argued with, and anything a user can type reaches the model. The server holds the credentials, resolves the caller from the session, and checks permission on every single call, exactly as it would for a human user. Two rules follow. Scope credentials per server and per environment, because a server that reads tickets should not carry a token that can also move money; the agent that mounts it will eventually try. And tools that cross a real threshold, moving money, changing access, sending anything to the outside world, should not execute on the model's say-so alone. Put a confirmation step between decision and execution. Log every call with caller, arguments, result, and timestamp, because when an agent does something surprising the first question is always what it actually called. The rest of that ground is covered in the security mistakes that turn agents into incidents.

Agents retry, so writes must be idempotent

Agents retry far more than humans do. A timeout, a truncated stream, or a supervisor deciding a step did not complete, and the same create_invoice call goes out twice inside a second. Every write tool needs an idempotency key: a caller-generated parameter that the server stores alongside the result of the first successful execution, so a repeat with the same key returns that stored result instead of doing the work again. Make it required rather than optional, because an optional idempotency key is an absent one. For the same reason, prefer tools that declare a desired end state over tools that apply a delta. set_status is safe to run twice; advance_status is not. Duplicate side effects are the most common production failure we see in agent systems, and they are almost entirely a tool design problem. The underlying patterns are the same ones behind idempotent integrations generally.

How to measure tool selection accuracy

You cannot improve what you do not score, and tool selection is scoreable. Treat the surface like any other interface with tests: a fixture set of realistic requests, a labelled expectation for each one, and a number that moves when somebody edits a description. Run it on every change to a tool schema. Descriptions read like documentation, but they behave like code.

  1. 1 Collect real requests
    Take fifty to a hundred phrasings of what people actually ask, pulled from support logs, from the team, and from transcripts of your own testing. Include the ambiguous ones and the ones the agent should refuse. Fixtures written by the same person who wrote the tools only confirm what that person already believes.
  2. 2 Label the expected call
    For each request, record the tool that should be called and the arguments it should be called with. Two scores fall out of that: selection accuracy, meaning did it pick the right tool, and argument accuracy, meaning did it fill the fields correctly. They fail for different reasons and get fixed in different places.
  3. 3 Score against the real mount
    Run the actual client against the actual server, exposing only the tools that client will mount in production. Selection is context dependent, so a score measured against a different tool set is somebody else's score, not yours.
  4. 4 Read the near misses
    Every wrong selection points at a specific pair of descriptions. Fix the text, rerun, and confirm the fix did not break a neighbouring case. In our own servers, most regressions came from a description edited to rescue one example that quietly broke two others.
  5. 5 Gate merges on the score
    Wire the eval into CI and fail the build on a drop. A schema or description change shipped without an eval run is a change with unknown blast radius, and the blast radius of a bad description is every conversation that server touches.
50 fixtures
The smallest eval that has ever caught a real regression for us: fifty recorded requests with the correct tool and arguments labelled by hand. That is a weekend of work, and it pays for itself the first time somebody edits a description.
The takeaway

Your MCP server is a user interface whose only user is a language model. Everything that makes an API pleasant for a careful human, precise names, honest errors, required fields that are actually required, matters more here, because this caller cannot ask you what you meant. Write the descriptions before the implementation. If you cannot describe a tool in three sentences that make clear when to use it and when not to, the tool is wrong, not the model.

Share

Put one workflow into production.

A 15-minute call, then a real assessment of what an agent can run on your own servers.

Book a scoping call →
Keep reading