The server was done. tools/list returned all six tools. The inspector lit up green on every one. I wired it into a real chat, asked the kind of question it was built for, and the model reached for the wrong tool. Then it made something up instead of calling the right one at all.
Nothing was broken. The protocol was fine. The tools ran fine when I called them by hand. The problem was quieter than a bug: a tool the model can technically see, and a tool the model actually uses at the right moment, are two different things.
Here are the four ways that gap shows up, and how to close each one.
Trap 1: a vague description is a dead tool
The model doesn't pick tools by their name. It picks by their description. That's the whole game, and it's the thing people skip.
A tool called get_data with the description "fetches data" is invisible in practice. The model has no idea when to reach for it versus the five other tools that also, in some sense, fetch data. So it guesses, or it skips.
Compare these two:
// the model shrugs at this one
{
"name": "get_data",
"description": "Fetches data"
}
// the model knows exactly when to use this one
{
"name": "search_orders",
"description": "Find orders by customer email or order ID. Returns status, items, and ship date. Use this before answering any question about a specific order — do not guess from memory."
}
Write the description for the model, not for a docs page. Say when to call it. Say what comes back. Say how it differs from the neighbor tool that looks similar. That last part matters more than people expect — most "the model called the wrong tool" problems are really "two descriptions that sound the same."
Trap 2: a fat response poisons the context
A tool returned four thousand tokens of raw JSON to answer "how many open orders are there?" The number was in there. So was everything else. The model waded through the dump, half-answered, and then the next few tool calls came back sloppy because the context was now full of junk it didn't need.
Your tool's return value isn't a database export. It's an input to the model's next thought. Every token you hand back is a token it has to read on every following turn.
So return what's needed to make a decision, not everything you have. If the question is a count, return a count. If it's a list, paginate it and say there's more. Add a summary field the model can lean on instead of parsing the raw rows. A tool that answers tightly keeps the whole conversation sharp. A tool that dumps drags everything after it down with it.
Trap 3: the schema lies about what it needs
Your input schema is a contract. When it's wrong, the model holds up its end and the server still breaks.
The usual version: a field is really optional, but the schema doesn't mark it, so the model dutifully invents a value to fill it. Or a field can be null in practice, but nothing says so, so the model sends a string and your handler throws. Then the tool 500s with a stack trace the model can't do anything with, and it either retries the same broken call or gives up.
Two fixes, and you need both. Make the schema tell the truth — mark what's actually required, mark what's nullable, add a one-line description on each field the way you did for the tool itself. And validate the input on the way in, so that when something's still off, you return a plain-English error the model can read and correct, not a language-runtime traceback.
Trap 4: one tool fails and the whole agent stalls
Five tools, and number three throws an unhandled exception. If that bubbles up as a hard error, the agent loop can just stop. One flaky dependency and the model is stuck, staring at a failure it has no way to route around.
Catch the error inside the tool and hand it back as something the model can read:
{ "error": "Order service timed out. Try again, or ask the user to retry in a minute." }
Now the model has options. It can retry, try a different tool, or tell the user plainly. A readable error keeps the agent moving. A thrown exception freezes it. Same failure underneath — completely different experience on top.
How to actually test this
Here's the trap in the trap: the inspector being green tells you the tools run. It tells you nothing about whether the model chooses them right. Those are separate questions, and only the second one matters to a user.
So test the second one:
- Run a real conversation, not a manual tool call. Watch which tool the model reaches for at each step. Wrong pick? That's a description problem, almost every time.
- Log every tool call the model makes — name and arguments. The arguments show you what the model thought your schema wanted, which is often not what you thought it said.
- Throw an ambiguous request at it, the kind that could match two tools, and see if it lands on the right one. If it coin-flips, your two descriptions aren't distinct enough yet.
That loop — real chat, watch the choices, fix the descriptions, run it again — is the work. The protocol is the easy part. Getting the model to use your tools the way you meant is the part that takes iterations.
I build MCP servers and wire up Claude Code setups for teams that want the model to actually use their tools, not just expose them — and this is where most of the time goes, long after tools/list is green.
So, honest question: what's the one MCP tool the model keeps refusing to call for you — and did rewriting its description finally fix it, or is it still sitting there ignored? Tell me what it does. I've got a guess about the description.
Top comments (0)