Penholder ← penholder.ai

Governing Agent Writes over MCP: Tool Annotations, Elicitation, and the Write-Gate

Reference · Updated 26 August 2026

The Model Context Protocol gives an agent three things at the write boundary: tool annotations (readOnlyHint, destructiveHint) that describe what a tool does, elicitation (the server pauses and asks the user mid-session), and — if you run one — a gateway that can block a tool call before it fires. All three are useful. None of them gate the commit to your system of record. Annotations are advisory hints the spec itself says clients must treat as untrusted and are "not enforcement." Elicitation asks whoever is holding the client session, then disappears when the session ends. A gateway gates the call, not the write. When an MCP tool writes to a database you'd get paged over, the durable checkpoint belongs one layer down — at the write, in the record itself.

What MCP gives you at the write boundary

None of these are strawmen — each is a real, well-designed part of the protocol, and for most tools they're the right amount of control. The point isn't that they're weak; it's that they answer a different question than "may this specific write commit?"

Tool annotations — readOnlyHint, destructiveHint, idempotentHint, openWorldHint

Metadata a server attaches to a tool so a client can label it and decide whether to prompt: a tool marked readOnlyHint: true can skip a confirmation; one marked destructiveHint: true can raise one. A tool with no annotations is assumed destructive, non-idempotent, and open-world — the safe default. They are the emerging risk vocabulary for agentic systems, and worth setting on every tool you publish.

Answers: what does this tool claim to do? (advisory metadata, per tool)
Elicitation — elicitation/create

A server pauses mid-session and asks the user for input: Form mode (structured input against a JSON schema) or URL mode (for sensitive input that must not pass through the client). Standardized, so any compliant client renders the prompt. This is genuinely preventive — it stops before the action and hands control to a human.

Answers: does the person at the client approve? (session-scoped, at the client)
An MCP gateway / proxy

A policy layer between client and servers that can authenticate, filter, and block tool calls — enforce which tools are reachable, scan arguments, apply per-client consent. Real enforcement, and the right place for network and auth policy. What it gates is the tool call: "may this invocation proceed?", decided at the proxy.

Answers: may this tool call fire? (per-call, at the proxy)

Annotations are hints — and the spec says so

The most common mistake is treating destructiveHint as a guardrail. It isn't one, and the protocol is explicit about it. From the MCP maintainers' own writing: annotations are "not guaranteed to faithfully describe tool behavior, and clients must treat them as untrusted unless they come from a trusted server." More bluntly: "An untrusted server can lie. A server can claim readOnlyHint: true and delete your files anyway." And on relying on them for safety: "They aren't enforcement. If you need a guarantee that a tool can't exfiltrate data, that's a job for network controls or sandboxing, not a boolean hint."

So annotations improve the client's UX and give humans a vocabulary for risk — keep setting them. But "the write is safe" is a hard guarantee, and a hint is a soft signal. The spec's own guidance is to keep your actual safety guarantees in deterministic controls, not in metadata the server self-reports.

Elicitation asks the session — not the record's owner

Elicitation is the closest MCP-native thing to an approval, and it's good. But two properties matter once a write lands in a system of record. First, it is session-scoped: the pause lives in the client ↔ server session and disappears when the session ends. Kill the client, restart the agent, and the pending question is gone — nothing durable is holding the write. Second, it asks whoever is operating the client, in the moment, with the payload the server chose to show. That's often the same agent operator — not the person accountable for the data. A finance or ops owner who should sign off on a change to reported revenue is not in that loop.

A gateway gates the call, not the commit

A gateway is real enforcement, and you should run one for auth and policy. But it sits at the tool-call layer: it decides whether an invocation may proceed, using the arguments as presented. It does not re-read the target row at decision time, doesn't distinguish a correct UPDATE from a destructive one within an allowed tool, and its record is a log of calls tied to the proxy — not an independent, tamper-evident account of what committed to the record. The MCP security guidance itself flags the failure modes at this layer — the confused deputy and token pass-through — where a correctly-privileged proxy is made to act on someone else's behalf.

Where the write-gate sits: behind the server, at the commit

A write-gate doesn't compete with MCP — it sits downstream of it. However the write arrives (an MCP tool, a LangGraph node, a direct connector), it lands as a durable PENDING proposal in the record's own governance layer, and a human approves a specific before → after value at the database, not a tool-call payload. (For the full mechanic — intercept, hold, approve, commit fail-closed, record — see The Write-Gate Pattern.) Because the checkpoint lives in the record rather than the session, four things follow:

Side by side

Across the axes that matter once an MCP tool's write reaches a system of record:

ControlEnforced, not advisory?At the system-of-record commit (not the tool call)?Outlives the MCP session?Fail-closed on changed baseline?Independent tamper-evident record?
Tool annotations (destructiveHint)No — spec says treat as untrustedNo — metadata on the toolN/A — static metadataNoNo
MCP elicitationYes — server-driven pauseNo — tool-call layerNo — session-scopedNoNo
MCP gateway / proxyYes — blocks the callNo — gates the callNo — per-callNoPartial — logs the call
Write-gate (behind the server)YesYesYesYesYes
MCP-native            client ─▶ [gateway: may this call fire?] ─▶ server ─▶ tool
                                          [elicitation: ask the session]
                                          annotations: readOnlyHint (advisory)
                                                              │
                                                              ▼  writes to DB — ungated at the commit

write-gate            server ─▶ tool ─▶ write ─▶ [ PENDING in the system of record ]
                                                   │  the data owner reviews before → after
                                         approve ─▶ commit (fail-closed on conflict)
                                         reject  ─▶ nothing lands
                                                   │
                                         every step ─▶ tamper-evident, independent log

When MCP's own controls are enough

Most of the time they are, and a write-gate would be over-engineering. Set accurate tool annotations, run a gateway for auth and policy, and use elicitation for the in-session confirmation — and stop there — when: the tool's writes aren't to a system of record you'd get paged over; an ephemeral pause is fine (a lost session just means the agent re-asks); the person at the client is the right approver; and no one needs an independent, tamper-evident record of the decision. That covers a large share of MCP tools. Don't put a commit-boundary control on a read-only lookup or a scratch-space write.

When you need a write-gate behind the server

The write-gate earns its place on a narrow band — MCP tools whose writes hit a record where the tool-call layer is the wrong place to decide:

A write-gate doesn't replace MCP's controls; it sits under them. Keep the gateway and elicitation for the call and the in-session pause, and add a write-gate for the commit to the system of record. And if you're comparing this to framework human-in-the-loop more broadly — LangGraph, the OpenAI Agents SDK, HumanLayer — see interrupt() vs a write-gate.

FAQ

If I set destructiveHint: true on my write tool, isn't that governing the write?

No — it labels the tool so a client can choose to prompt, but the MCP spec says clients must treat annotations as untrusted and that they "aren't enforcement." The write still commits the moment the tool runs. Set the annotation (it's good hygiene and a shared risk vocabulary), but don't mistake self-reported metadata for a gate on the commit.

Isn't MCP elicitation basically an approval step?

It's the closest MCP-native thing to one, and it's genuinely preventive. But it's session-scoped — the pause disappears when the session ends — and it asks whoever is operating the client in the moment, not necessarily the person accountable for the data. A write-gate holds the proposed write durably in the record and routes it to the data owner, and it re-checks live state at approve time.

I run an MCP gateway. Doesn't that already enforce policy?

For auth, tool allow-listing, and argument scanning, yes — keep it. But it gates the tool call: it decides whether an invocation may proceed, using the arguments as shown. It doesn't re-read the target row, can't tell a correct update from a destructive one inside an allowed tool, and its log is tied to the proxy. Pair it with a write-gate when the tool writes to a system of record.

Does a write-gate require MCP?

No — that's the point. It sits at the system-of-record commit, so it governs the write no matter how it arrives: an MCP tool, a LangGraph or OpenAI Agents SDK run, or a direct connector. One gate at the write, honored across every transport.

Where should annotations, gateways, and a write-gate each live?

Use all three at their own layers. Annotations describe tools; a gateway enforces auth and policy on calls; elicitation handles in-session confirmation; and a write-gate holds the commit to a system of record — durable, fail-closed, independently recorded. They compose; they don't substitute.

Penholder

Penholder is a write-gate. Put it at the system of record your MCP tools write to — Postgres, MySQL, a warehouse (BigQuery, Snowflake, Redshift, ClickHouse), SQL Server, Oracle, or a governed spreadsheet — and an agent's write lands PENDING and commits only after a human approves: fail-closed on conflict, with a tamper-evident log. Framework- and transport-agnostic, at the write boundary. See how it works →

Sources

  1. Model Context Protocol Blog — Tool Annotations as Risk Vocabulary: What Hints Can and Can't Do ("clients must treat them as untrusted unless they come from a trusted server"; "They aren't enforcement"; a server can claim readOnlyHint: true and act destructively anyway).
  2. Model Context Protocol — Elicitation specification (elicitation/create, Form and URL modes, session-scoped).
  3. Model Context Protocol — Security Best Practices (confused-deputy and token pass-through at the proxy layer) and the 2026-07-28 specification.
  4. Stacklok — Tool annotations are becoming the risk vocabulary for agentic systems (gateways enforcing on annotations; hints vs enforcement).
  5. The provenance point — AI Incident Database, Incident 1152 (an agent deleted a production database and misreported what it had done).