I once burned most of an afternoon convinced I was dealing with a stupid model. It kept reaching for the wrong tool out of a list I'd written myself, and the list looked fine. Of course it looked fine. I knew query meant a natural-language search string and not SQL. I knew which of three nearly identical tools to reach for, because I'd written all three. I knew the evaluate endpoint could mutate page state and not just read it.
None of that was in the schema.
An MCP server hands a model its tools through one call, tools/list, and what comes back is a name, a description, and an input schema. That's the whole brief. Everywhere you fill a gap from memory, the model fills it from token probabilities, and those don't match.
Three patterns from real servers, all invisible to the person who wrote them.
query means six things
The archived Google Maps MCP server defined a place-search tool:
{
"name": "maps_search_places",
"inputSchema": {
"properties": {
"query": { "type": "string", "description": "Search query" }
}
}
}To you, query means "coffee shops near Pike Place." To a model, query is one of the most overloaded nouns we have: SQL, a structured filter, a keyword bag, a street address. The description restates the name and adds nothing.
Rename it search_text. Then write the property description the way you'd write it for someone who has never opened your repo: "A natural-language place search — business name, category, or address. Example: 'coffee shops near Pike Place Market'." The example does more work than three sentences of prose would.
A tool that can wipe the page, described as "execute JavaScript"
The archived Puppeteer server:
{
"name": "puppeteer_evaluate",
"description": "Execute JavaScript in the browser console",
"inputSchema": {
"properties": {
"script": { "type": "string", "description": "JavaScript code to execute" }
}
}
}The handler passes that string to page.evaluate, which runs it in the page context. The script might read the text of a DOM node. It might also submit a form, clear localStorage, or delete every element on the page. One description covers all of it.
So a model picking this tool gets no signal that it's about to touch application state. Microsoft's Playwright server calls its equivalent browser_run_code_unsafe, which is blunt and does the job. MCP also offers destructiveHint and readOnlyHint annotations that a host can use to gate the call. An annotation is a claim about the tool, though, and for a tool that runs arbitrary code it's the parameter that decides whether a given call is destructive. The only honest annotation is the pessimistic one. Advisory and pessimistic still beats silent.
Three tools, one handler
OpenLink's ODBC server registers query_database, query_database_md, and query_database_jsonl. The first takes an optional format parameter; the other two hardcode a format and call the same handler. Their descriptions are nearly identical.
Nothing in that set gives a model grounds for choosing. And the general tool's format is an unconstrained string rather than an enum of ["json", "jsonl", "md"], so a model can pass "csv", get no error, and find out at runtime that its choice was quietly ignored. If the format-specific tools exist for convenience, say in each description when to prefer it. If they don't have a reason to exist separately, delete two of them.
Same bug, three times
In each case the author wrote a description that confirmed what they already knew instead of supplying what the model doesn't. "Search query." "JavaScript code to execute." Accurate, and useless to the only reader that matters at call time.
A 2026 preprint rewrote descriptions across 856 MCP tools, touched no code, and measured a median improvement of 5.85 percentage points in task success, along with regressions in some model-and-domain combinations. Rewriting descriptions moves behavior. It doesn't uniformly improve it, and that matters more than the headline number: you're editing an input to a system whose output you can't predict by reading, only by running.
So run it. Dump your tools/list payload, read it cold with the repo closed, and mark every place you supply the answer yourself. Those are the gaps the model is filling from somewhere else.
- Zod descriptions can vanish: The MCP TypeScript SDK's v2 migration guide warns that with Zod 4.0–4.1,
.describe()metadata can be lost during schema conversion, meaning a property description you wrote may never reachtools/list. - Anthropic's year-in-every-query fix: Anthropic reports that Claude's production web-search tool was appending "2025" to queries, biasing results, and the fix was changing the tool description rather than the code.
- MCP's annotation hints aren't guardrails: The current spec defines
readOnlyHint,destructiveHint, andopenWorldHintas explicitly untrusted annotations that should not be used as authorization controls, which means a host can use them for UI gating but not for security enforcement. - State enforcement can backfire: A September 2026 preprint found that deterministic gates improved a large model's pass rate from 0.39 to 0.54 on state-decidable policy, but degraded a smaller model's performance below baseline when the gate's state matcher was wrong.

