---
title: "Read Your Tool Descriptions Like a Model Would"
canonical: "https://current.tinyfish.ai/issue/50/foundations/article/41621/read-your-tool-descriptions-like-a-model-would"
publication: "TinyFish"
publisher: "TinyFish"
language: "en-US"
guide: "https://current.tinyfish.ai/llms.txt"
format: "Practical Walkthrough"
author: "Sable Whitford"
author_url: "https://current.tinyfish.ai/team/201"
issue: 50
section: "foundations"
section_name: "Foundations"
published: 2026-10-08
updated: 2026-10-09
description: "Three real MCP tool schemas read the way a model reads them: ambiguous names, hidden side effects, and tools with no reason to exist separately."
word_count: 858
source_links: 9
---

# Read Your Tool Descriptions Like a Model Would

*By [Sable Whitford](<https://current.tinyfish.ai/team/201/markdown>)*

*October 8, 2026*

[Read on the website](<https://current.tinyfish.ai/issue/50/foundations/article/41621/read-your-tool-descriptions-like-a-model-would>)

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](https://modelcontextprotocol.io/specification/2026-07-28/server/tools). 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](https://github.com/modelcontextprotocol/servers-archived/blob/9be4674d1ddf8c469e6461a27a337eeb65f76c2e/src/google-maps/index.ts) defined a place-search tool:

```json
{
  "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](https://github.com/modelcontextprotocol/servers-archived/blob/9be4674d1ddf8c469e6461a27a337eeb65f76c2e/src/puppeteer/index.ts):

```json
{
  "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`](https://github.com/microsoft/playwright.dev/blob/main/mcp/introduction.mdx), which is blunt and does the job. MCP also offers [`destructiveHint` and `readOnlyHint` annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) 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](https://github.com/OpenLinkSoftware/mcp-odbc-server/blob/d9313b52ae86d72f537f0436c3aa9e63d3709848/src/main.ts) 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](https://arxiv.org/abs/2602.14878) 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.

<follow_up>

## Things to follow up on...

- **Zod descriptions can vanish:** The MCP TypeScript SDK's [v2 migration guide](https://github.com/modelcontextprotocol/typescript-sdk/blob/b022522089a0c8b632595c6e7b536453945ed5a9/docs/migration/upgrade-to-v2.md) warns that with Zod 4.0–4.1, `.describe()` metadata can be lost during schema conversion, meaning a property description you wrote may never reach `tools/list`.
- **Anthropic's year-in-every-query fix:** Anthropic [reports](https://www.anthropic.com/engineering/writing-tools-for-agents) 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`, and `openWorldHint` as [explicitly untrusted annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) 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](https://arxiv.org/abs/2609.25686) 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.
   
</follow_up>

## More

- [Foundations — Issue 50](<https://current.tinyfish.ai/issue/50/foundations/markdown>)
- [Issue 50](<https://current.tinyfish.ai/issue/50/markdown>)
- [Publication guide](<https://current.tinyfish.ai/llms.txt>)