Mid · 8 chapters
Tools over MCP
Declaring them, behavioral annotations (read-only vs destructive vs idempotent hints), and how they reach the model's context
MCP does not repair poor tool contracts; weak schemas, misleading annotations, and opaque errors still cause unsafe calls and silent failures in production.
Read these first
What you will know
Declare a tool on a server and have a model call it end to end
Declare it on the server, list it from the client, and watch the model call it end to end.
Use behavioural annotations to mark a tool read-only or destructive
Annotations let a client know a tool is read-only or destructive, which drives confirmation prompts.
Run a client trace that distinguishes read-only, destructive, and idempotent annotations and records how each affects confirmation or retry policy
These flags are advisory metadata; treating them as authorization or assuming every client enforces them can turn retries into duplicate writes.
Show what the client actually forwards to the model
The client forwards name, description and schema; your implementation stays invisible.
Diagnose a tool that appears but is never chosen
A tool that lists but never gets chosen has a description problem, exactly as with any other tool.
Chapters
- 1
Declaring and calling a tool
Declaring a tool on an MCP server means giving it three things: a name, a description, and a JSON Schema for its input. The client discovers it through tools/list, renders those three things into the request it sends the provider, and a selection by the model comes back as tools/call carrying arguments shaped by that schema; your handler runs and returns content the host places into the conversation. Five hops, three of which you own — and the whole path is short enough to drive from one thirty-line script, which is the fastest way to make every later symptom locate itself.
- 2
The name, description and schema
A forty-line docstring, precise type hints and a careful implementation are invisible: the client forwards the tool's name, description and input schema as it chose to render them, and the model sees nothing else. Moving the safety rails out of the handler and into the declaration takes a description from roughly 60 tokens to 190, which costs about two cents across a forty-turn session with prompt caching in play and buys a model that has been told about the seven-day recovery window. Porting a tool from your agent's own definitions onto an MCP server improves no part of that contract, because a vague description travels unchanged.
- 3
Behavioural tool annotations
The readOnly, destructive, idempotent and openWorld hints tell a host what kind of operation a tool is, so it can decide about confirmation dialogs and retries. A client that reads a destructive hint and does nothing with it is fully compliant, and so is one that never looks at annotations at all — the specification tells clients to treat annotations from untrusted servers as untrusted, because the party making the safety claim is the party whose behaviour is in question. The defaults matter as much as the values: an unannotated tool reads as writing, destructive, non-idempotent and open-world, so leaving them off a read-only lookup has a cost.
- 4
Enforcement in the tool handler
One server, three hosts, three different confirmation policies — and one of those hosts, a CI job driving a bare client, shows no dialog at all, which is compliant behaviour. A server that needs an operation gated cannot borrow another application's UI to do it: it has to establish who is calling from a token it validates itself, check the audience claim so a token minted for a different server is rejected at parse time, resolve arguments before comparing them, and ask its own policy store before the side effect happens. That is what an authorization ticket has to contain to be honestly closed across three hosts.
- 5
Protocol errors and tool errors
MCP carries failure in two channels read by two different parties: a JSON-RPC error replaces the result and is handled by the client, while a result marked isError: true travels into the conversation where the model reads it. Turning every exception into the first means a lookup for a missing invoice is re-issued with the same id seven times, burning roughly 29,000 input tokens and 31 seconds before the model tells the user something false. The dividing line is whether your tool logic ran and produced a verdict — and the wording of a verdict is prompt text, so it should say what is true, name the next action, and say when repeating is pointless.
- 6
Idempotency keys for tool calls
Tag charge_card as idempotent and a careful host will retry a failed call on the strength of it; the customer is charged £240 twice and the defect is the annotation, not the host. Removing the flag fixes one row of a seven-row table — host retry policies that ignore annotations, transport replays, a model re-issuing after an ambiguous error, a second host and your own retry wrapper are all untouched. Duplicate suppression lives in a key the server derives from the caller, the tool name and the canonicalised arguments, recorded before the side effect and carrying the outcome afterwards, or it does not exist.
- 7
Descriptions and tool selection
A tool that appears in tools/list, is forwarded by the host, and is selected three times in four hundred sessions has not been throttled by anything — it is losing a comparison on description against whatever else is connected. The artefact that settles it is the provider request the host sent: if your entry is in the tools array with the text you wrote, the protocol did its job. Three words against forty-six is not a close comparison, and a rewrite whose every clause closes a specific gap took selection from 3 to 247 of 291 answerable questions, with the explicit comparison against the neighbour carrying most of it.
- 8
Consolidating a tool surface
Each near-neighbour makes every other description work harder, because a description must distinguish itself from thirty-three siblings rather than from a handful — and eleven of a generated server's tools were about orders alone. Default annotations across all of them are evidence that nobody designed the surface, not the cause of the misrouting. When a host hard-codes four of your tool names it has done your consolidation for you, badly, and told you which four matter. Consolidating 34 endpoint wrappers into 9 designed tools took first-choice accuracy from 61% to 89% over 120 replayed tasks and the tool array from 6,460 tokens to 2,180.
External resources
- Tool Annotations as Risk Vocabulary: What Hints Can and Can't Doblog.modelcontextprotocol.io
Official post defining the four hints, their defaults, and their limits.
- Build an MCP servermodelcontextprotocol.io
Shows how to declare MCP tools, annotate behavior, and expose them in model context.
- Testing MCP Tool Annotations: readOnlyHint, destructiveHint, openWorldHintsunpeak.ai
How to verify clients actually honour the hints you declare.
- Tool Annotations as Risk Vocabulary: What Hints Can and Can't Do | Model Context Protocol Blogblog.modelcontextprotocol.io
This is a substantive MCP explainer covering the ToolAnnotations interface, behavioral hints, defaults, trust limitations, and security implications, though it may not fully explain how tool definitions enter the model’s context.
- MCP server: A step-by-step guide to building from scratchcomposio.dev
Shows MCP server construction, tool declarations, behavioral annotations, and how tools enter model context.
- MCP | Tools, Resources, Prompts | Do you know the difference?J-HAYER
Draws the line between model-invoked tools and the other primitives.
- Build and Ship Any MCP Server in MINUTES (Full Guide)Cole Medin
Shows building MCP servers, declaring tools, annotating behavior, and exposing them in context.