Skip to content
Practitioner10 min readUpdated September 2026

Tool Design And The Model Context Protocol

The quality of an agent is mostly the quality of its tools. How to design interfaces for a model rather than a human, why error messages are instructions, and what MCP standardises without solving.

Teams spend weeks tuning prompts and minutes wrapping their APIs. The ratio should be reversed. In almost every underperforming agent I have looked at, the prompt was adequate and the tools were the problem: names that meant nothing without the source code open, arguments requiring knowledge the model had no way to obtain, results that dumped a thousand lines of JSON when three fields mattered, and errors that said "400 Bad Request" to a component whose only means of recovery is reading that string.

A model in a loop does not have your documentation, your colleagues, your staging environment or your ability to guess. It has the tool names, the descriptions, the schemas, whatever is in context, and the text your system returns when something fails. That surface is the entire capability of the system. The model is a competent engineer on their first day, who cannot ask anyone anything, and who will confidently do something plausible if your interface is ambiguous.

So the discipline is easy to state and unglamorous to execute: build the tool layer as a deliberate product with a specific user in mind, rather than a thin pass-through to endpoints designed for a different consumer. What the agent may then do with it — permissioning, blast radius, audit — is a separate and equally important design, covered in designing agents with bounded authority.

Your existing API is not a tool set

The first instinct is to expose what you already have. You have a REST API, it is documented, it works, so wrap it and let the model call it. This produces a working demo and a system that plateaus at mediocre.

Your API was designed for a client developer who reads the docs once, writes integration code, tests it, and then executes the same correct call forever. The cost of ambiguity is one confused afternoon, paid once. A model pays that cost on every call, has no memory of previous confusion, and cannot test before committing. Interfaces merely survivable for a human are actively hostile to a model. Three mismatches show up constantly.

Endpoint granularity is wrong. Your API has thirty endpoints because that is a clean resource model. An agent needs perhaps six operations corresponding to things somebody actually wants done. Chaining five calls for one intent gives the model five chances to go wrong and fills the context with four intermediate results nobody needs.

Responses are sized for machines with parsers. A full object graph is fine for code that indexes into it. In a model's context it is a wall of mostly irrelevant tokens crowding out the reasoning you are paying for.

Errors are sized for log aggregators. A status code and a stack trace tell the model nothing about what to do differently, so it retries the identical call, invents an argument, or gives up and reports success.

Name and describe for a stranger

The tool name and description are the only things the model sees before choosing. Treat them as the primary interface, because they are.

Name the operation after the intent, in the vocabulary the task uses, not after your internal system. search_customer_orders is legible; qryOrdSvcV2 is not. Prefix families consistently. Avoid near-duplicate names differing by one word, because that is where selection errors concentrate.

The description should say what the tool does, when to use it, when not to use it, and what it returns. The third clause is the one everybody omits and the one that prevents the most errors. If two tools could plausibly apply, each description should point at the other.

search_orders
  Finds orders for one customer within a date range.
  Use when you have a customer id and need their order history.
  Do not use to look up a single known order id — use get_order.
  Returns up to 20 order summaries, most recent first, with a
  has_more flag. Narrow the date range if has_more is true.

That is longer than an API doc comment and earns its tokens several times over. Descriptions are prompt, at the exact moment the decision is made.

Granularity follows the task, not the resource

The right unit for a tool is one thing a person would ask for. If a common intent requires the model to orchestrate four calls in a fixed order, that order is known at design time and belongs in code. Collapse it into one tool and let the model spend its reasoning on the part that is genuinely uncertain.

This cuts both ways. A tool that does too much — one entry point with a mode argument selecting between eight behaviours — is equally bad, because the model must reason about the mode instead of the task, and because you cannot restrict authority usefully when everything comes through one door.

The test is whether you can describe the tool in one sentence without the word "or". If you cannot, split it. If describing a workflow takes four sentences with "then" between them, merge it. Bias towards read tools returning exactly what the next decision needs, and write tools that are narrow, specific and individually gateable.

Arguments the model can actually produce

Every argument must be something the model can obtain from the task, from context, or from a previous tool result. This sounds obvious and is violated constantly.

No opaque identifiers the model cannot look up. If a write tool needs an internal record id, a read tool must return it. Otherwise the model invents one, because inventing a plausible identifier is exactly what it is good at.

Prefer enums over free strings. A status argument constrained to a fixed set is a decision the schema makes for you. A free-text status is a guessing game whose losses you discover in production.

Flatten where you can. Deeply nested arguments produce more malformed calls than flat ones. If nesting is unavoidable, keep it shallow and describe every field.

Make required fields genuinely required. A field that is optional in the schema and mandatory in practice is a trap. The model omits it and gets an error it cannot interpret.

Use explicit units and formats. Not timeout but timeout_seconds. Not date but an ISO date with the format in the description. Ambiguous units are the most boring possible source of an incident.

Error messages are instructions

This is the highest-leverage change most teams can make and it takes an afternoon.

When a tool fails, the string you return is read by a component whose entire recovery strategy is reading that string. Write it as an instruction to a colleague about to try again: what went wrong, why, and what a valid next action would be.

Compare. "Error: invalid date range" causes a retry with another invalid range. "The date range is invalid because end_date is before start_date. Provide end_date on or after start_date. You supplied start 2026-05-01 and end 2026-04-01." causes a corrected call. Three categories deserve distinct treatment.

Malformed call. Say which argument, why it failed, what the constraint is, and echo what was supplied. Recovery is usually immediate.

Valid call, no result. Distinguish it clearly from failure and say what to try instead — a broader range, a different identifier, a related tool. An empty result that looks like an error produces pointless retries; an error that looks like an empty result produces silent wrong answers, which is worse.

Not permitted, or not possible. Say so terminally, so the model stops rather than exploring. "This action requires human approval and has been queued" beats a generic denial, because it tells the loop the correct next move.

Also make every mutating tool idempotent under a caller-supplied key. Agents retry far more than client code does — on timeouts, on parse failures, on their own suspicion that something did not take. The mechanics are in designing agents with bounded authority.

What MCP does, and what it does not

The Model Context Protocol standardises how a model-facing application discovers and calls tools, and how it reads resources, across a client-server boundary. Instead of every framework inventing its own plugin shape, a server declares its tools and their schemas, and any compliant client can use them. Servers can be local processes or remote services.

What it genuinely gives you is worth having. Integrations become reusable across clients and frameworks rather than being rewritten per stack. The transport, discovery and schema negotiation are solved, which is unglamorous work you no longer own. There is an ecosystem, so the connector to the system you use may already exist. It draws a clean process boundary, which is a natural place to put sandboxing, credential handling and policy enforcement.

What it does not give you is more important to be clear about, because expectations here have run ahead of reality.

It does not design your tools. A badly named, badly scoped, badly described tool is exactly as bad over MCP as it was in your framework. The protocol carries your interface; it does not improve it.

It does not solve authorisation. The protocol describes how calls are made, not who is allowed to make them or with what authority. That remains yours to enforce, in code, at the tool boundary.

It does not protect you from prompt injection. A server returning content from the outside world returns whatever that content says. Connecting more sources through a standard interface makes the ingestion easier, not the content safer.

It does not manage context budget. Every connected server's tools occupy space in every request, and verbose results land in the same context everything else shares.

It does not version your semantics. Two servers can expose a tool with the same name and different behaviour, and nothing stops a server's behaviour changing under you.

Treat an MCP server as you would any third-party dependency with access to your systems: pinned, reviewed, scoped, monitored, owned by somebody.

Registries, versioning and the cost of too many tools

Once you have more than a handful of tools, two problems appear together.

The first is governance. You need a registry: what exists, who owns it, what authority it requires, its schema, which version is current, and which agents may use it. Without one you get duplicates with divergent behaviour and no way to answer "what can our agents actually do", which is the first question in any security review. Versioning matters more than for a normal API, because your consumer does not read changelogs. Changing an argument's meaning while keeping its name is a silent behavioural change across every agent using it. Additive changes are safe; renames and semantic changes need a new tool name and a deprecation window with usage monitoring.

The second problem is context. Every tool in scope costs tokens in every request and adds a candidate to every selection decision. Selection accuracy degrades as the candidate set grows, particularly when tools overlap, and past some point adding a capability makes the whole system worse. There is no universal number, but the direction is reliable: a curated dozen beats an indiscriminate hundred.

SymptomLikely causeMove
Picks the wrong tool among similar onesOverlapping names or descriptionsMerge, rename, add explicit "do not use when"
Calls a tool then immediately calls another to fix itGranularity too fineCollapse the sequence into one tool
Retries the same failing callError string is not an instructionRewrite the message with the constraint and a next action
Fabricates identifiersNo lookup path for a required argumentAdd a read tool, or accept a natural key
Runs out of context mid-taskVerbose results, oversized tool listTrim result payloads, scope the tool set per task
Stops early claiming successEmpty results indistinguishable from completionDistinguish no-result from done in the response

The strongest single control is to scope the tool set per task rather than exposing everything to everything. A triage agent does not need the refund tool in its schema. Selecting the available set at dispatch improves accuracy, cuts cost and narrows authority at once, which is unusual and worth taking.

What to do on Monday

Open the schema your agent actually receives and read it as a competent engineer seeing this system for the first time, with no other source of information. Every place you would have to guess is a place your agent is guessing, several times a day, and losing.

Then do the error audit. Collect the tool errors from the last week, rank by volume, and rewrite the top five as instructions containing the constraint violated and a valid next action. Measure task success before and after. This is the cheapest improvement available anywhere in the system.

Finally, count your tools and cut the set each agent sees to what its task needs. Check that every write tool takes an idempotency key, that every required identifier has a read tool producing it, and that every tool has one named owner. If you are adopting MCP, treat each server as a dependency with a review, a pin and a scope — the protocol standardises the plumbing, and the quality of what flows through it is still entirely yours.