Designing MCP tools that models actually call correctly
Your MCP server works in testing. Then you connect it to a real model and watch it fail in ways your handlers never see: the model ignores the server entirely, calls tools in the wrong order, or invents a parameter value that was never on offer. None of these are model bugs you can fix. All of them are design decisions you control. What follows is what held up in production, drawn from an MCP server I built that let agents set up third-party services for a user — demoed to partners and advertisers, and the seed of the company’s next product direction.
Server instructions decide whether your tools get called
The highest-leverage text you write is the server-level instructions field, not the tool descriptions. Tool descriptions tell the model what a tool does once the model has already decided your server is relevant. The instructions decide that first question. Two things belong there: concrete trigger situations, and the order to call your tools in. The shape looks like this:
Use this server when the user wants to try, set up, or
provision a third-party service — e.g. "set up a database
for this project" or "I need an email API."
Call sequence:
1. search_offers — find services matching the user's need
2. activate_offer — provision the selected serviceModels act on examples more reliably than on category descriptions. “Use this when the user wants to set up a service” is weaker than three example utterances that should trigger the server. And naming the call order up front removes the most common sequencing failure: the model calling the execute tool cold, with an identifier it guessed.
Filter server-side so the model cannot pick something unusable
In the system I built, the discovery tool returned only items that were actually actionable — every result it surfaced could be executed by the second tool. Options that existed in the catalog but could not be provisioned were filtered out before the model ever saw them. This closes off a whole failure class: the model cannot hallucinate a usable option from an unusable one, because the unusable ones are not in its context. Any validation you can do server-side before returning results is validation the model never gets a chance to fail.
Two tools, one hidden protocol
A discovery call followed by an execute call maps to how a model reasons: first find out what exists, then act on one of the results. Two tools beat one god-tool with a mode parameter, and they beat five granular tools that force the model to orchestrate your internal protocol.
That second half matters as much as the first. The execute tool in my system internally kicked off a provisioning job, polled for status, surfaced any human-input requests, redeemed a one-time token, and returned the result. Five distinct protocol steps. The model saw one synchronous call. Every step you expose as a separate tool is a step the model can skip, reorder, or retry incorrectly. Hide the protocol; expose the intent.
Tool results are prompt surface
A tool result goes straight into the model’s context as text. That makes it the best place to steer the next call, better placed than the system prompt, because it arrives at exactly the moment the model is deciding what to do next. Two habits follow from this. First, format identifiers so the model can lift them back out: my discovery tool returned markdown with each item’s ID set off clearly, so copying an ID into the execute call was a mechanical operation rather than an extraction problem. Second, end the result with an explicit next-step sentence — “to provision one of these, call the execute tool with its ID.” The model follows directions it just read.
Pass-through payloads travel better as strings
When a tool result contains structured data the model must hand back on the next call, an opaque JSON string round-trips more reliably than a nested object. Asked to reconstruct a nested parameter, models drop fields, rename keys, and coerce types. Asked to pass back a string they were given, they mostly pass back the string. In my system, user-input specifications moved between the two tools as a single JSON string parameter for exactly this reason. Reserve structured parameters for values the model is genuinely deciding; use strings for values it is merely carrying.
Return errors, don’t throw them
A thrown exception ends the turn. An error result — the MCP isError flag plus a human-readable message — stays in context, and the model reads it like any other tool output. My server returned errors as results with a plain-language explanation of what went wrong, and models routinely corrected the input and retried without any human involvement. Write the message for the model: state what was wrong and what a valid call looks like. An error result is a steering opportunity; an exception is a dead end.
When the caller is a model, return instructions
The strangest-looking design choice held up best. A successful execute call in my system did not return a raw payload of credentials and endpoints for the model to interpret. It returned a block of instructions — prose written for the model, with the live values interpolated into it: here is your API key, here is the endpoint, do this next. A payload requires the model to figure out what the fields mean and what to do with them. Instructions require it to follow directions, which is the thing models do best. If your tool’s consumer is an agent, the most useful return type is often a prompt.
Build the server per session
Construct the server instance with the caller’s credentials and context bound in at session setup, rather than resolving identity inside each tool handler. Two things fall out of this. Tools cannot be invoked with the wrong tenant’s identity, because the identity was fixed before any tool existed — there is no tenant parameter for the model to get wrong. And request-scoped values, like the client IP you need for downstream calls, are available wherever a handler needs them without threading them through every signature.
What to test
Handler unit tests tell you the tools work when called correctly. The failures above all happen before or between correct calls, so test with a real model connected. Three questions, tested separately: does the model pick the server up at all when a prompt should trigger it; does it pick the right tool when several could plausibly apply; and does it recover when a tool returns an error result. Each maps to a different artifact — server instructions, tool descriptions, and error formatting — so a failure tells you which text to rewrite. Run the prompts your users will actually type, not the ones your tools were designed around. The gap between those two sets is where integrations die.
Questions clients ask
How many tools is too many for one MCP server?
There is no hard limit, but selection accuracy drops as the count rises, because every tool description competes for the model’s attention on every turn. A discovery tool plus an execute tool covers a surprising range of products. Add a third tool when it has a genuinely distinct job, not to split one job into steps — steps belong inside a single tool.
Why won’t the model call my tool at all?
Usually the server never told the model when it is relevant. Tool descriptions say what a tool does; server-level instructions say when this server applies. Add concrete trigger situations — the actual user phrasings that should activate the server — and state the order to call tools in. If the model still skips it, your tool’s description likely overlaps with a built-in capability the model prefers.
Should tool descriptions be written for humans?
Write them for the model, and accept that humans will read them too. That means concrete trigger conditions, explicit parameter semantics, and a stated relationship to the other tools on the server (“call this after search_x”). Marketing language and human-oriented API prose are noise to the model and often cause the wrong tool to be picked.
How do I test tool selection?
Run realistic prompts against a real model with your server connected, not unit tests against the handlers. Check three things separately: does the model pick the server up at all for prompts that should trigger it, does it pick the right tool among several, and does it recover when a tool returns an error result. Each failure has a different fix — instructions, descriptions, and error formatting respectively.
Is your MCP server designed for the model that has to call it?
I audit MCP servers against exactly these failure modes: whether models pick the server up, pick the right tool, survive your error paths, and can act on your results. Fixed price, $2,000, one week, credited toward any follow-on implementation work.