MCP tool annotations explained
Tool annotations are optional hints a server attaches to a tool to tell clients how it behaves. Clients use them to decide things like whether to ask the user before running a call.
The four hints and their defaults#
| Hint | Meaning when true | Default if missing |
|---|---|---|
readOnlyHint | The tool does not modify its environment. | false |
destructiveHint | The tool may perform destructive updates. When false, it only makes additive updates. Only meaningful when readOnlyHint is false. | true |
idempotentHint | Calling it again with the same arguments has no further effect. Only meaningful when readOnlyHint is false. | false |
openWorldHint | The tool interacts with an open world of external entities, like web search. False means a closed domain, like a local memory store. | true |
There's also an optional title annotation for display.
The defaults are cautious, so be explicit#
A tool with no annotations is treated as possibly destructive, not idempotent and open-world. That's safe, but it means a harmless read-only tool may trigger the same confirmation prompts as a delete. Mark read-only tools readOnlyHint: true, and give write tools an honest destructiveHint.
Hints are not a security control#
The schema reference is blunt about this: annotations are hints, not guaranteed to be accurate, and clients should never make tool-use decisions based on annotations from untrusted servers. The tools spec says clients MUST treat annotations as untrusted unless they come from trusted servers. So annotations help well-behaved clients give a better experience. They don't replace access control on your server.
Examples#
{
"name": "search_orders",
"description": "Search the shop's orders by customer email or order number. Returns up to 25 matches.",
"inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "Email or order number"}}, "required": ["query"]},
"annotations": {"readOnlyHint": true, "openWorldHint": false}
}{
"name": "refund_order",
"description": "Refund an order in full to the original payment method. Cannot be undone.",
"inputSchema": {"type": "object", "properties": {"order_id": {"type": "string", "description": "Order number"}}, "required": ["order_id"]},
"annotations": {"readOnlyHint": false, "destructiveHint": true, "idempotentHint": true, "openWorldHint": true}
}The refund is idempotent if your API refuses a second refund of the same order. If it doesn't, set idempotentHint: false.
Converting from OpenAPI? The converter suggests hints from each HTTP method, and the linter flags tools whose names don't match their hints. For the other fields of a tool, see the tool schema reference.