The most alarming tool on a well-annotated MCP server is usually the one carrying the fewest annotations. That is the design, not an accident: silence here means assume the worst, and declaring nothing is how a careful server author says stop and ask.
Annotations are four optional booleans plus a title, so a client sitting between a model and a person can decide per call whether to run it or interrupt somebody.
The four hints
On the wire they are readOnlyHint, destructiveHint, idempotentHint and openWorldHint. SDKs spell them in their own idiom.
readOnlyHint — the tool performs no state changes. This is the hint that makes automation defensible: a call that cannot change anything is one you can afford to get wrong.
destructiveHint — whether a tool that does change things may perform destructive updates. False means additive only: it writes, but does not remove or overwrite what was there. Only meaningful for tools that are not read-only.
idempotentHint — repeated identical calls have no additional effect on the environment. Note the wording: a claim about the environment, not about what the second call returns.
openWorldHint — whether the tool's domain of interaction is open or closed. A web search is open-world; what comes back depends on entities the server does not control. Reading from a server's own store is closed.
A fifth field in the same object is not a boolean. Title is a display string a client can show instead of the programmatic name — arguably the most load-bearing of the lot, because a person reads it in the approval prompt.
Unspecified is not neutral
The defaults are the part people miss. Leaving a hint off is not "no comment" — each has a documented default, and they are pessimistic on purpose.
- read-only unspecified — clients assume it is not read-only
- destructive unspecified — clients should assume true
- idempotent unspecified — assume false
- open-world unspecified — assume true
An unannotated tool therefore declares: writes, may destroy, unsafe to retry, unbounded domain. Annotating is an act of narrowing — telling clients they may relax, and you had better be right.
What a client does with them
A client that keeps a person in the loop has one interesting decision per call: ask, or don't. Annotations are its only structured input, so approval policies get built out of them.
- read-only and idempotent — run it, show the result
- not read-only, destructive false — run it or ask, depending on the deployment
- destructive true or unstated — always ask, and show the title if there is one
- non-idempotent — never auto-retry a timeout, because a repeat could be a second write
That last rule is learned the hard way: a client that retries a timed-out call is fine against an idempotent tool and gambling against everything else. Human-in-the-loop review is easier when the tools say which calls are cheap.
Nine tools, annotated honestly
RelayLink's MCP server is small enough to read end to end.
All nine tools declare openWorldHint false. Since true is the default, that is an active statement on every one: nothing here reaches outside RelayLink's own data.
Five declare read-only and idempotent — check_inbox, list_drafts, thread_status, list_threads and list_contacts. The auto-approve candidates: nothing changes, and repeating costs nothing.
Two declare destructive false and stop there — get_package and draft_package. Neither claims read-only, which is the point, because both genuinely write. get_package inserts a read receipt, one of the two events that later let the sender's thread_status report a package as seen; draft_package inserts a draft row. Neither write removes or overwrites anything, which is what "additive updates only" means. Claiming read-only for either would have been a small, convenient lie.
One declares idempotent and little else — cancel_draft. Cancelling twice leaves the world where the first call left it, so the hint is accurate in the MCP sense — though the second call returns an error saying the draft was already cancelled. Effect on the environment, not shape of the reply.
One declares almost nothing — confirm_send, the tool that delivers. It sets openWorldHint false and a title, Send package (irreversible), and declines every other hint, so by the defaults above a client treats it as not read-only, destructive and non-idempotent: the strongest warning posture the vocabulary allows, achieved by staying silent. No tool there sets destructive true explicitly — the one that deserves it inherits it.
One wrinkle errs safe. confirm_send behaves idempotently on retry — repeat it against an already-sent draft and it returns the original identifiers, and nothing goes out twice — yet declares no idempotentHint. Under-claiming costs a redundant prompt; over-claiming costs a message nobody can recall.
Hints, not enforcement
None of this is a security control, and the official SDK documentation says so: the properties are hints, not guaranteed to describe behaviour faithfully, and clients should not make tool-use decisions based on annotations from untrusted servers. Nothing stops a hostile server stamping readOnlyHint true on a tool that deletes everything.
So a server enforces its own rules in code. RelayLink's send safety is not the word irreversible in a title; there is no single-call send at all. The server requires a draft created earlier by the same account, checks it belongs to the caller, and re-checks permission at confirm time. Strip every annotation off and that gate is untouched. The gap between a label and an enforced rule is most of what makes a server safe to connect, and why the approval step is the product.
Annotating a server you wrote
Two rules follow. Prefer under-claiming: an unnecessary prompt costs an interruption, a false read-only costs whatever the tool can reach. And write the title for the person, not the model — it appears when someone has to decide.
The vocabulary is coarser than reality. Four booleans cannot express "writes a row nobody would miss" or "idempotent in effect but not in reply", and both turn up inside a nine-tool server. When it cannot say the true thing, decline the flattering annotation rather than stretching it. If you are sizing up what a connected agent can do to you, start from the checklist rather than the hints.