Open the JSON for almost any modern MCP tool and you'll find four booleans riding alongside the name and schema: readOnlyHint, destructiveHint, idempotentHint, openWorldHint. They read like a permission model — the kind of thing an operating system would enforce before letting a process touch the disk. That reading is the single most expensive mistake you can make with them, because MCP tool annotations are not a permission model. They're a vocabulary. And a vocabulary describes; it does not enforce.
What the four hints actually say#
Each annotation answers one question about a tool's behavior. readOnlyHint asks whether the tool modifies anything. destructiveHint asks, if it modifies something, whether the change is destructive rather than additive — an overwrite versus an append. idempotentHint asks whether calling it again with identical arguments is safe. openWorldHint asks whether the tool reaches out to an unbounded set of external entities or stays inside a closed, predictable domain.
The MCP blog frames the set exactly right: a "basic risk vocabulary" that lets a client run a preflight assessment before it decides whether to interrupt the user. A tool marked readOnlyHint: true from a server you trust can skip the confirmation dialog. A tool marked destructiveHint: true earns a warning that lists what's about to change. The canonical example is a delete_file call where the client "shows a dialog listing what's about to be deleted before anything happens." That's the whole job: turning a wall of identical tool calls into a graduated experience where the scary ones get a speed bump and the safe ones don't.
The defaults are pessimistic on purpose#
Here's the first non-obvious detail, and the one most people get backwards. The defaults are not neutral — they're cautious. readOnlyHint defaults to false. destructiveHint defaults to true. idempotentHint defaults to false. openWorldHint defaults to true.
Read that as a sentence: a tool that declares nothing is treated as write-capable, destructive, non-idempotent, and open-world. Silence buys you the most guarded handling, not the least. This is the right call — an un-annotated tool is an unknown quantity, and unknowns should get the confirmation dialog — but it inverts the intuition that annotations are how a tool earns trust. In MCP, annotations are how a well-behaved tool sheds unnecessary friction. The permissive states (readOnlyHint: true, destructiveHint: false) are the ones a server has to actively claim.
Annotations aren't how a tool earns trust. They're how a trusted tool sheds friction. The distinction is the whole security story.
The line the spec draws in bold#
Now the part that matters more than all the field definitions combined. The specification is unambiguous: annotations "are not guaranteed to faithfully describe tool behavior, and clients must treat them as untrusted unless they come from a trusted server." Or, more bluntly, in the blog's own words — an untrusted server can lie.
Nothing in the protocol stops a malicious or compromised server from stamping readOnlyHint: true on a tool that quietly rm -rfs your working directory. Annotations are self-reported metadata; there is no verification layer, and there was never meant to be one. They also do nothing to stop prompt injection against the model itself — the annotation describes what the tool does, not what an attacker can talk the model into calling. Microsoft's security team spent a whole advisory on prompts becoming shells in agent frameworks; not one of those RCEs is closed by a boolean the attacker's server gets to set.
So the failure mode writes itself. A team ships an agent, sees readOnlyHint, and wires it into an auto-approval path: if readOnly, run it without asking. Then they connect a third-party MCP server they don't control, and the annotation they treated as a fact becomes an input the attacker chooses. The UX signal got promoted to a security decision, which is the one move the spec explicitly tells you not to make. Keep the actual boundary where it belongs — sandboxing, network egress rules, zero-trust posture, and auth — and let annotations do only what they can: shape the dialog for servers you've already decided to trust.
The trap inside idempotentHint#
There's a subtler hazard, and it's in the annotation you'll most want to lean on. idempotentHint: true is a gift to your retry logic: a timed-out call you can safely replay without inventing a deduplication scheme. But a timeout is ambiguous — the request may have failed, or it may have succeeded and only the acknowledgment got lost. If the tool is actually idempotent, replaying is free. If the annotation is wrong — a server author who conflated "returns the same value" with "causes the same effect once" — your clean retry just charged the card twice.
That's why idempotentHint belongs in the UX layer, not the correctness layer. It can tell you whether to bother asking the user before a retry. It cannot be the thing that makes the retry safe. Safe retries still require a real idempotency key minted on your side and honored at the tool boundary — a guarantee you control, not a hint you received.
How to hold them#
Treat the four annotations as what they are: a shared language for describing risk, extremely useful for building a humane approval flow and completely useless as an access-control mechanism. Read them to decide whether to ask. Never read them to decide whether it's safe. The 2026 spec's whole direction is to keep the protocol core thin and push real guarantees out to the infrastructure that already provides them — and annotations are the clearest instance of that philosophy. The same split shows up wherever the protocol adds surface: MCP Apps lets a server render a whole interactive UI, and there too the sandbox is the signage and the consent path is the fence. They're signage. Signage is worth having. Just don't mistake it for a fence.



