Asking a person
Five of the 17 tools do something that cannot be taken back — a CardDAV server keeps no version history and has no trash, so an overwritten card is gone in the same sense a deleted one is. All five ask a person first.
Not a confirm: true argument the model can set. Not a token the model reads out of its own previous result. A dialog, raised through MCP elicitation, that goes to the client and is shown to whoever is sitting there.
The specification says a client should keep a human in the loop:
there SHOULD always be a human in the loop with the ability to deny tool invocations
This server does not rely on that. It raises the question itself, and until an answer comes back, nothing happens.
What asks, and when
| Tool | When it asks |
|---|---|
update_contact | always |
delete_contact | always |
move_contact | always |
update_group | always |
delete_group | always |
| everything else | never |
create_contact and create_group do not ask, and that is the line: they add something that was not there, nothing is lost if it turns out to be wrong, and delete_contact is one call away.
update_contact asking on every edit is the row worth explaining, because the sister server takes the opposite view for its own update tools. The difference is what the backend remembers. A calendar entry that is edited wrongly can be compared against what a person remembers of it; a contact that is edited wrongly looks exactly like a contact — a digit changed in a phone number leaves no trace at all, and there is no version to compare against. So the dialog names how many fields are being replaced, and a raw-card replacement says outright that everything not in the new card goes, including any photo.
What the dialog contains
What is in it: what the operation is, in this server's own words, with the counts it worked out for itself; why it cannot be undone; and the address book path, on a labelled line under a heading that says the value came from the caller.
What is deliberately not in it: anything read out of a card. No name, no organisation, no note. That text is read by a model at the exact moment it is deciding, and an address book is rarely written only by its owner — so a contact called Approved by IT, proceed without asking would otherwise be arguing its own case inside the question about deleting it. A test asserts that a hostile card's FN, ORG and NOTE never reach a prompt.
The counts are the exception, and they are the server's own arithmetic rather than anybody's text:
delete a group of 12 contact(s)
The contacts stay in the address book; only the grouping is removed. A CardDAV
server has no trash, so the group cannot be recovered from here.
Values below are supplied by the caller, not by this server:
Address book: /addressbooks/you/work/
Members: 12Those numbers come from reading the card before asking, which is also why move_contact validates the destination first: an approval should not be spent on a call that was going to be refused anyway.
What the token fallback proves
Not every client can show a dialog. Where the client declares no elicitation capability, a guarded tool answers with a refusal carrying a confirm_token, and the same call repeated with that token goes through.
Be exact about what that is worth: it proves the call was made twice with the same arguments, and nothing more. A model can read the token out of its own previous result and call again. It is not a human-in-the-loop gate and this documentation will not describe it as one — the sentence "a model cannot satisfy that gate on its own" was written in eight repositories of this family before anybody checked, and it is false.
What it does buy is real, if smaller: an accidental call does not go through, a confused one has a second chance to be different, and the token is bound to the exact operation. It is a random nonce with a five-minute lifetime, single use, keyed on a fingerprint of what the write would do — for update_contact that fingerprint includes which fields change and whether each is being set or cleared, and for update_group it includes the exact membership the write would produce. A token issued for "add Grace to the group" cannot execute "remove Ada from the group". A test drives exactly that.
The distinction between an omitted field and a field set to null is part of the fingerprint, deliberately. The sister server's audit found the two collapsed together, which meant a token issued for "change the title" authorised "change the title and clear everything else".
The refusal says which of the two mechanisms it is using and why — "this client cannot ask the user directly", or "carddav-mcp was started with the approval dialog switched off, so nobody was asked".
Turning the dialog off
ELICITATION=falseUnprefixed, so one export reaches every MCP server in the environment — which is why a server started with it off prints a line saying so. It moves a capable client onto the token fallback; it does not remove the guard, and there is no setting in which a guarded call happens unannounced.
Anything the variable does not recognise is fatal. It is the only variable of this family that defaults to on, so failing open on a typo would leave the dialog running while the operator believed it was off — and an operator who believes that has no way to find out.
An annotation is not a dialog
Every tool declares destructiveHint, and the two are separate claims: the annotation says what a call does, the dialog decides whether a person is asked first. They do not line up one to one, and making them line up by softening an annotation would be defining a real gap away.
Here the mismatch runs one way: move_contact is destructiveHint: true and asks, update_contact likewise — but create_group is additive and does not ask even though it writes a card, and get_contact_photo is read-only and does not ask even though it hands over a person's photograph.