Governing Agent Writes over MCP: Tool Annotations, Elicitation, and the Write-Gate
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?"
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.
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)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:
- It's enforcement, not a hint. The write physically does not commit until approved. Nothing the server self-reports about a tool changes that.
- It survives the session and the transport. Restart the client, swap MCP for a cron job, and the held write is still in the system of record for a human to act on.
- It re-checks live state and fails closed. If the target row changed between propose and approve, a stale baseline is rejected rather than silently overwriting the newer value — something no tool-call gate sees.
- It's recorded independently. The proposal, the decision, and the commit are hash-chained in a tamper-evident log you don't have to take the agent's — or the gateway's — word for.
Side by side
Across the axes that matter once an MCP tool's write reaches a system of record:
| Control | Enforced, 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 untrusted | No — metadata on the tool | N/A — static metadata | No | No |
| MCP elicitation | Yes — server-driven pause | No — tool-call layer | No — session-scoped | No | No |
| MCP gateway / proxy | Yes — blocks the call | No — gates the call | No — per-call | No | Partial — logs the call |
| Write-gate (behind the server) | Yes | Yes | Yes | Yes | Yes |
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:
- The tool writes to a production database, warehouse, or ledger that must outlive the session and whichever transport proposed the write.
- Approval must bind to the actual commit and catch a baseline that changed between propose and approve — not just green-light an invocation.
- A non-engineer who owns the data (finance, ops, risk) approves the row change, not a JSON tool payload shown in a session.
- An auditor needs an independent, tamper-evident maker/checker record the acting agent and the gateway cannot rewrite. (How this maps to OWASP, NIST, and the EU AI Act: the compliance mapping.)
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
- 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: trueand act destructively anyway). - Model Context Protocol — Elicitation specification (
elicitation/create, Form and URL modes, session-scoped). - Model Context Protocol — Security Best Practices (confused-deputy and token pass-through at the proxy layer) and the 2026-07-28 specification.
- Stacklok — Tool annotations are becoming the risk vocabulary for agentic systems (gateways enforcing on annotations; hints vs enforcement).
- The provenance point — AI Incident Database, Incident 1152 (an agent deleted a production database and misreported what it had done).