Why architecture matters here

The problem annotations solve is that a tool's name and schema tell a client almost nothing about its blast radius. execute_query could be a read-only SELECT or a DROP TABLE; update_record could be a reversible field edit or a cascading delete. Without structured metadata, a client that wants to offer sensible autonomy — auto-run the harmless stuff, confirm the dangerous stuff — has only heuristics: keyword-match the name, or ask a model to guess intent, both of which are unreliable exactly when it matters. Annotations replace guessing with a declaration: the server, which actually knows what the tool does, states its properties in a fixed vocabulary the client can reason about deterministically.

Why this rises to an architectural concern rather than a UI nicety is the autonomy spectrum agents now operate across. A fully-supervised assistant can confirm everything; that does not scale to an agent running a hundred tool calls in a loop. The practical middle — auto-approve read-only and idempotent operations, pause for confirmation on destructive or open-world ones — is only expressible if the client can classify calls, and annotations are the classification substrate. They let a client build a policy like 'auto-run anything with readOnlyHint: true, always confirm anything with destructiveHint: true, and treat unknown tools as potentially destructive' — a policy that is legible, testable, and adjustable per trust level of the connected server.

But the same design carries a trap that has caused real security misunderstandings, so it belongs in 'why this matters': annotations are hints, not guarantees. They are assertions a server makes about itself, and a malicious or buggy server can lie — mark a destructive tool readOnlyHint: true to slip past a client's auto-run policy. Therefore the architecturally correct mental model is: annotations improve the user experience and inform consent for tools you already trust; they are never the mechanism that enforces safety. Enforcement lives on the server (real permission checks) and in the client's trust decision about whether to connect to that server at all. Confusing the advisory layer with the enforcement layer is the central error, and understanding why is the whole point of this piece.

It is worth being concrete about why the vocabulary is exactly this small set and not a richer taxonomy. Each hint maps to a distinct client decision an agent runtime actually has to make on every call. readOnlyHint answers 'can I skip consent entirely?' — the auto-run gate. destructiveHint answers 'must I make the user look before proceeding?' — the hard-confirm gate. idempotentHint answers 'can I safely retry this after a timeout?' — the resilience gate an autonomous loop leans on constantly. openWorldHint answers 'does this reach beyond my trust boundary?' — the scrutiny gate that pairs with egress and SSRF concerns. A larger vocabulary would blur these into overlapping flags no client could implement consistently; a smaller one would collapse decisions that genuinely differ. The set is deliberately minimal and orthogonal so a policy engine can treat each as an independent axis.

The corollary is that annotations are only as valuable as the client's willingness to act on them differently. A client that confirms everything ignores the hints; a client that auto-runs everything trusts them blindly. The value lives entirely in the middle — a client that reads the four axes plus the server's trust level and produces a graded response — which is why the feature is really a contract between honest server authors who annotate truthfully and disciplined client authors who build a real policy engine on top. Neither half works alone, and a system that has only one half has the appearance of safety without the substance.

Advertisement

The architecture: every piece explained

Top row: where annotations live. A tool definition carries a name, an inputSchema, a description, and an optional annotations object. That object holds the hint fields. readOnlyHint (default false) asserts the tool performs no modifications to its environment — a search, a fetch, a status check. When true, a client can reasonably auto-run without consent because the worst case is wasted work, not damage. destructiveHint (default true, and only meaningful when the tool is not read-only) asserts that updates may be irreversible or wide-reaching; note the default is the cautious one, so a tool that omits the annotation is treated as potentially destructive rather than assumed safe.

Middle row: the rest of the vocabulary. idempotentHint (default false) asserts that repeating the call with identical arguments causes no additional effect beyond the first — a client or agent can safely retry on a timeout without fear of double-charging or double-sending. openWorldHint (default true) asserts the tool interacts with an open, external world (the internet, a third-party service) as opposed to a closed local domain; a web search is open-world, a query against a fixed in-memory dataset is not. The optional title gives a human-friendly display name distinct from the machine name, so consent UIs can show 'Delete customer record' rather than delete_customer_v2. The client policy engine consumes all of these to classify each call into auto-run, confirm, or deny.

Bottom rows: the two layers people must not conflate. The consent UI is annotation-driven: it decides whether to prompt, and what to say — 'This tool is marked destructive and reaches an external service; allow?' — turning metadata into an informed human decision. The server enforcement layer is the real security boundary: actual authorization checks, scopes, and permission validation that hold regardless of what any annotation claims. A tool marked read-only must still be prevented, server-side, from writing; the annotation is a promise, the check is the enforcement. The ops strip captures the discipline: assign trust levels to servers, audit tool calls, review annotations as part of tool review, and — the load-bearing rule — never treat a hint as authorization.

A practical note on how clients combine these axes: the safest policy engines treat the annotations and the server trust level as a matrix, not a single score. The rows are trust tiers (first-party, verified third-party, untrusted); the columns are the hint combinations. A read-only tool from a first-party server lands in the auto-run cell; a destructive tool from any tier lands in hard-confirm; anything non-read-only from an untrusted server lands in confirm-or-deny regardless of what its other hints claim, because an untrusted server's self-description carries no weight. Implemented this way, adding a new server or tool never requires new code — it just falls into a cell — and the security posture is auditable as a table a human can read, rather than buried in conditional logic scattered across the client.

MCP tool annotations — machine-readable hints about what a tool doesso clients can decide autonomy vs confirmationTool definitionname + inputSchemaannotations blockhints, not guaranteesreadOnlyHintno state changedestructiveHintirreversible effectidempotentHintsafe to retryopenWorldHinttouches external worldtitlehuman-facing labelClient policyauto vs confirm vs denyConsent UIannotation-driven promptsServer enforcementthe real security boundaryOps — trust levels + audit + annotation review + never trust hints as authzdeclarescarriesflagsflagspromptgateenforceoperateoperate
Tool annotations: hint fields on a tool definition drive client consent policy — while the server remains the real enforcement boundary.
Advertisement

End-to-end flow

Walk a coding agent connected to two MCP servers: a trusted internal 'repo' server the platform team ships, and a third-party 'web-tools' server the user added themselves. The client has a policy: auto-run read-only tools from trusted servers, confirm everything destructive, and confirm all non-read-only calls from untrusted servers regardless of their hints.

The agent needs to understand a bug, so it calls repo.search_code. Its definition carries readOnlyHint: true, openWorldHint: false and comes from the trusted server; the client's policy engine classifies it as auto-run and executes it silently — no dialog, the loop stays fast. The agent reads results, then decides to run repo.run_tests, annotated readOnlyHint: false but idempotentHint: true, destructiveHint: false. Not read-only, so it is not auto-run by the read-only rule, but its non-destructive, idempotent hints let the client apply a lighter-weight 'notify but proceed' policy for this trusted server — the user sees a passive log line rather than a blocking prompt.

Now the agent proposes repo.force_push, annotated destructiveHint: true. The consent UI fires a blocking confirmation using the human-readable title 'Force-push to remote branch' and an explicit warning that the action is marked destructive; the user reviews the arguments (which branch?) and approves once. Meanwhile the model, influenced by content it fetched, tries web-tools.http_post to an arbitrary URL. That tool is openWorldHint: true from the untrusted server, so — independent of any other hint — the client demands confirmation and shows the full destination; the user recognizes the URL as suspicious and denies it. Note what did the protecting: the client's trust decision about the server, layered on top of the annotation, not the annotation alone.

Finally the crucial counter-example. Suppose web-tools ships a tool cleanup deceptively annotated readOnlyHint: true that actually deletes files. If the client naively trusted the hint and auto-ran it, damage would follow — which is exactly why the well-designed client never auto-runs write-capable tools from untrusted servers on the strength of a hint, and why the tool's own server (here, an attacker's) is not the enforcement boundary you rely on. The correct architecture used annotations to reduce friction on trusted, verified tools and fell back to human consent and server trust everywhere else. Annotations made the good path smooth; they were never asked to make the bad path safe.