All Articles

MCP Tools Agents Can Actually Use: Design Rules for Production Servers

The quickest way to build an MCP server is to wrap an existing API: one tool per endpoint, arguments copied from the OpenAPI spec, the JSON response returned as-is. It works in a demo. That mapping needs evaluation: similar tool names, oversized results and unhelpful errors can make an otherwise functional API difficult for a model to use.

The problem is that a REST API is designed for programmers who read the docs once and write code. A tool is read by a model on every call, and the model has to pick it from a list, fill in the arguments and interpret the result in context, with no chance to ask questions. Anthropic’s Writing effective tools for agents is the best long treatment of this. These are the rules I’d apply to any server before it goes to production.

1. One tool per task, not per endpoint

An agent that needs a customer’s open orders shouldn’t have to call list_customers, get_customer, list_orders and get_order_status in sequence. Every extra call is another chance to pick the wrong tool or pass the wrong ID, and more tokens.

Design tools around what the agent is trying to do:

  • find_customer(query) returns the few best matches with their IDs, and searches by name, email or order number, so the agent doesn’t need to know which.
  • get_open_orders(customer_id) returns only the orders that are open, each with the fields relevant to support work.

Fewer, more capable tools also make selection easier. Past a few dozen tools in context, choosing between them becomes a problem in its own right. If you need many, group them by prefix (billing_, shipping_) so the names carry structure.

2. Return what the agent needs, not what the API has

A raw API response can contain fields the task does not use: internal IDs, timestamps in three formats, nested metadata, null fields. All of it costs tokens and attention, on this call and every later turn in the conversation.

  • Return a curated set of fields with clear names. status: "shipped" beats st: 3.
  • Prefer names and labels over opaque IDs, but include the ID the agent will need for the next call.
  • Paginate or truncate large results, and say that you did: “Showing 20 of 312 results; narrow by date range to see more.” A silently truncated list reads as a complete one.
  • Offer a detail or format argument for when the agent genuinely needs more.

The 2026-07-28 MCP tools specification moved tool inputSchema and outputSchema to full JSON Schema 2020-12. Use outputSchema with structured results, so clients can validate what comes back and the model gets a predictable shape.

3. Write errors the model can act on

400 Bad Request tells a model nothing. It will retry with the same arguments or give up. Write errors the way you would for a colleague:

{
  "isError": true,
  "content": [{
    "type": "text",
    "text": "date_from must be ISO 8601 (YYYY-MM-DD). Got '07/15/2026'. Example: 2026-07-15."
  }]
}

Say what was wrong, what was expected, and how to fix it. For permission errors, say that it’s a permission problem and that retrying won’t help, so the agent escalates instead of looping.

4. Make arguments hard to get wrong

  • Use enums for anything with a fixed set of values. The model can’t invent a status that isn’t in the schema, and validation catches it if it tries.
  • Give each argument a description that says what it’s for, with an example of the format.
  • Avoid overloaded parameters, such as a filter string with its own query syntax. Separate typed arguments are easier for a model to fill in correctly.
  • Validate on the server regardless. The schema is guidance to the model, not a guarantee.

5. Treat annotations as hints, never as controls

MCP tools can carry annotations: readOnlyHint, destructiveHint, idempotentHint and openWorldHint. Clients use them to decide, for example, whether to ask the user before a call. Set them accurately. They’re useful.

But they’re self-reported by the server and not enforced by anything. The annotation alone does not verify that a tool is read-only. Security has to come from the server’s own authorization: the credentials the server uses, scoped to what its tools should be able to do. If a server has tools that only read, give it credentials that can only read. Then a wrong annotation, or a compromised server, can’t turn a read into a write.

6. Treat tool results as untrusted input

Whatever a tool returns goes straight into the model’s context. If a tool fetches a web page, reads an email or returns a support ticket, it may be returning text written by an attacker, including text that looks like instructions. The server can help by:

  • clearly delimiting external content in results, and labelling where it came from;
  • stripping or escaping content that isn’t needed for the task (HTML, hidden text);
  • never including secrets or credentials in results, even ones the underlying API returns.

Delimiting doesn’t make injection impossible. The prompt-injection post covers why the stronger defence is limiting what an agent that has read untrusted content can then do.

7. Test tools with an agent, not just with unit tests

Unit tests check that a tool works. They don’t show whether a model can use it. Write a dozen realistic tasks, run an agent against the server, and read the transcripts: which tool did it pick, what arguments did it pass, how did it recover from errors? Start with a small reviewed sample, then expand around failures and important tasks. Ten transcripts can expose an interface problem; they do not establish its prevalence or prove the tool is usable in general.

Where this stops

Good tool design reduces the mistakes a well-intentioned agent makes. It doesn’t bound what a manipulated or confused one can do. That’s the job of the server’s authorization and credentials, and of approval gates in front of irreversible actions. Design tools for the model, and scope permissions for the worst case.