Skip to content
Chapter 2 of 7 · ElicitationConfirmations and destructiveHint

11 min read

Confirmations and destructiveHint

On this page

Chapter 1's rule asks who owns a missing value, and a confirmation has no value in it. Nobody is being asked to supply a datum that only they hold; they are being asked whether an action they already requested should go ahead. The boolean coming back carries no information the server did not have — the answer is yes in the overwhelming majority of cases, and when it is no, what the user wanted was to stop a call that had already been authorised somewhere upstream.

That distinction matters because the two look identical from inside a handler. Both are a dialog that appears, an answer that comes back, and an operation that continues. What separates them is which question is being asked, and the answer decides which of three mechanisms is correct — only one of which costs your server a dialog.

Approval already has a mechanism, and it is not yours

Every host that runs tools has an approval story, because it had to have one before your server existed. The host is the application the user is sitting in that owns the client end of your connection, and it knows which tools the user has blanket-approved, which require a prompt each time, which are blocked by policy, and it keeps that state across the session and often across sessions. Your server sits behind that. By the time your tool handler runs, the host has already decided to permit this call, and a dialog your server opens afterwards is a second gate placed downstream of the real one.

That ordering is the whole argument, and it is worth walking rather than asserting. The host owns the transport, so nothing reaches your process except by the host choosing to send it. A tools/call frame arriving at your handler is therefore proof that the host's approval step has already run and already said yes. Your handler's first line executes after that decision, which means your dialog cannot prevent an approval — the approval has happened — and can only add a question about whether the approved thing should go ahead. A gate placed after the thing it is meant to gate is not a second layer of defence. It is a second opportunity to say no to something that has already been permitted, offered by the party that was about to carry it out.

What your server contributes to that decision is metadata, declared once at tool-definition time. Annotations are the channel topic 6.4 established for this, and the relevant ones here say what kind of effect the tool has:

python
@mcp.tool(
    annotations={
        "title": "Purge search index",
        "readOnlyHint": False,
        "destructiveHint": True,
        "idempotentHint": False,
        "openWorldHint": False,
    }
)
async def purge_index(index: str) -> dict:
    deleted = await search.drop_all(index)
    return {"index": index, "documents_deleted": deleted}

There is no dialog in that handler and there does not need to be one. The destructiveHint travels with the tool list at connection time, so the host has it before the model has even chosen to call anything, and hosts use it to decide how hard to gate: a read-only tool may run silently, a destructive one may always require an explicit allow-once regardless of what the user approved earlier. The confirmation still happens; it happens in the place that can actually stop the call and in the visual language the user has already learned from every other tool on that client.

Running the confirmation yourself gives up four things at once. The host's approval decision is recorded in its own audit trail and permission memory, and your boolean is not, so a user who chose always-allow last week is prompted again by you and an administrator reviewing what was approved sees only the host's record. Automated contexts — a continuous-integration run, a scheduled agent, an evaluation harness — have a policy for tool approval and no policy at all for an arbitrary server-side question. Your dialog is advisory in a way the host's is not, since the same server that is asking is the one that would carry out the deletion, so a bug or a compromise on your side defeats it entirely, while the host's gate sits outside your process. And a client that does not implement elicitation cannot answer your question at all, which is the failure the next chapter, on checking the capability first, is built around.

The choice is three-way

Putting confirmations back where they belong turns chapter 1's two options into three, and the three are distinguished by what is actually missing rather than by how the failure looks from inside the handler.

What is missingWhere it livesMechanismWorks without elicitation
Precise errora valuein something you or the model already holdtool result naming the field and its legal valuesyes
Elicitationa valueonly in the user's headelicitation/create with a flat schemano — needs a fallback
Host confirmationapproval, not a valuein the host's permission modeldestructiveHint on the tool definitionyes

The rows are not interchangeable and the middle one is the only one that costs a dialog you own. Reading the table in the other direction is a useful design exercise: for any interruption your server is about to cause, name which row it is in. If you cannot, you are usually looking at a fourth thing — an interruption that exists because the tool was designed to need it, which chapter 7, on operations that ask four times, takes apart.

A second example makes the sorting concrete. A send_campaign tool is about to email 41,000 recipients, and there are three candidate interruptions in it. "Are you sure you want to send to 41,000 people?" is row three, since the only content of the answer is permission and the host is already gating a tool whose destructive nature you declared. "Which sender identity should this go from?" is row one when the account has exactly one verified sender, because you can read that and should. "Send now, or at 09:00 in each recipient's local timezone?" is row two, because it selects between two different operations and nothing you hold records the answer. Three questions that look identical from inside the handler resolve to three different mechanisms, and only one of them costs a dialog.

The boundary between rows two and three is sharper than it first looks, and it is worth pinning down with a purge. Are you sure? is row three, because the only information in the answer is permission. Change the question to prod-catalog has 2.4M documents and a nightly reindex scheduled in 40 minutes. Purge now, purge after the reindex, or purge only documents older than 90 days? and it moves to row two, because the three answers lead to three different operations and nothing in your system or the model's context establishes which the user wants. The test is whether a yes and a no are the only possible answers. When they are, the host's gate already covers it. When the answer selects among genuinely different outcomes, you are collecting a value that happens to have consequences attached, and chapter 1's ownership question applies as normal.

Where the annotation does not reach

Annotations are hints, and the specification says so plainly. A host is free to ignore destructiveHint entirely, to prompt for everything, or to prompt for nothing because the user turned confirmations off. That is a real limitation and it produces a real objection: if the host might not confirm, surely the server should, so that something confirms.

The objection is tempting for a reason worth naming, because the reason is structural rather than careless. A server author can read their own handler and cannot read the host's configuration, so the only lever visibly under their control is the one inside their process, and a lever you can see feels like the only lever there is. The move that follows — implement the gate where I can see it — would be right if the two gates were independent. They are not: the server-side dialog is opened by the same process that performs the deletion, so any fault that would make the deletion happen when it should not is a fault that can also make the dialog return true, or not appear at all. A check that fails in the same direction as the thing it checks adds no independent evidence, which is the general form of the rule and the reason a host's gate, sitting outside your process, is worth more than a better-worded one inside it.

The objection also mistakes what the annotation is for. It is a declaration of the tool's nature, and the decision of how to gate on that declaration belongs to whoever configured the client, who knows things your server does not — whether this is a sandbox or production, whether the user is an administrator, whether the run is interactive at all. A user who has disabled confirmations on a client they control has made a choice about their own environment, and a server that reimposes a dialog is overriding that choice for every user of every client in order to protect against one configuration.

What the annotation genuinely does not cover is authorization, and it is easy to slide from one to the other because both feel like permission. Whether this user is allowed to purge this index is a question your server must answer itself, from the identity attached to the session, before it does anything — and it must answer it whether or not a dialog appeared, because a confirmation from a user without the right to purge is worth nothing. A confirmed action by an unauthorised user is still an unauthorised action, and if your authorization check is the dialog, you have no authorization check. Elicitation, host confirmation and access control are three separate concerns that happen to share a moment in time.

The other place this reasoning does not reach is a genuinely irreversible operation with a natural undo window, where the better design is neither a question nor a gate. A purge that soft-deletes and retains for 24 hours, and returns 2,400,118 documents marked for deletion; restore with restore_index before 2026-04-14T09:12Z, removes the need to be certain in advance. That result goes back to the model, the model tells the user, and the user has a day to change their mind rather than three seconds to read a modal. Confirmation dialogs are a way of buying certainty at the moment of action; reversibility is a way of not needing it, and where the operation permits it, reversibility is the stronger design. It is also the only one of the three that keeps working when the client is a scheduled job.

The step under that claim is about what a dialog can physically carry, and it is worth making explicit. Certainty about a destructive action requires the facts bearing on it — how much is being deleted, what else is scheduled against it, what depends on the result — and a boolean field carries none of them. Everything the purge dialog knew had to be compressed into one sentence of prose, read once, under time pressure, in a window covering the thing the user was looking at. The undo window inverts that constraint by moving the decision to a point where the outcome is a fact rather than a prediction: the count is in the tool result, the command that reverses it is in the same string, and the user has a day in which to notice the number was larger than they expected. A confirmation asks somebody to be right in advance about a quantity nobody has shown them.

The rule underneath all of this is that a dialog should carry information in both directions. An elicitation does: the server learns something it could not otherwise know, and the answer changes what the operation does. A confirmation dialog opened by a server does not, because the permission it collects was collected upstream by a party with more context, a durable record of the decision, and the ability to enforce it. What your server knows that the host does not is what kind of operation this is, and the annotation is the channel for exactly that.

The purge team, having read this far, would move their confirmation into the annotation and delete the elicitation. That still leaves them with the report they have not explained: a partner integration where the tool did not double-prompt, did not fail, and did not return — it hung until the agent run timed out. The next chapter finds the cause in a message that arrived before any tool was ever called.

Elicitation — read the current version at https://daviddominguez.dev/learn/en/elicitation/confirmations-and-destructivehint

Back to top