DEV Community

Saurabh Pathak
Saurabh Pathak

Posted on

Can an MCP server ever know a human really answered an elicitation?

I'm working on an approval gate for destructive MCP tools. The server asks the human through MCP elicitation, so the model never sees the approval step.

A commenter pointed out a hole: in Claude Code, a hook on the Elicitation event can exit 0 with action "accept" and answer the request with no dialog. The server only receives accept, so it can't tell a person from a script. A project .claude/settings.json in a cloned repo could register such a hook once the folder is trusted.

What I'm considering:

  • Log "client-accepted" instead of "approved", so the audit file never claims a human was involved.
  • Run the allowlist and policy checks before any elicitation, so a scripted accept can't override them.
  • Refuse irreversible tools when I can't tell how the answer was produced.

My question: is there any reliable way for an MCP server to know that a real person answered an elicitation? Or should the design assume it can't know, and rely on reversibility and quarantine instead?

I haven't checked which other clients let a script answer elicitation. If you know of any, please tell me. The project is open source, so PRs and issues are welcome.

Top comments (3)

Collapse
 
sattyamjjain profile image
Sattyam Jain •

I think the honest answer for form-mode elicitation is no. The 2025-11-25 spec says "the protocol itself does not mandate any specific user interaction model", and user approval controls on the client are a SHOULD. So logging "client-accepted" is right.

URL mode is the closest the spec gets. The server sends a URL; the data does not pass through the client, and the server "MUST verify the identity of the user who opens the URL before accepting information". A Claude Code Elicitation hook can still accept a URL-mode request, but it gets no content back, so the server keeps waiting for its own page. It is close to the one-time code idea above, but native to the spec.

One idea I have not tested: put the confirm step for irreversible tools on that server-hosted page, with a step-up check like a passkey with user verification. A scripted accept cannot finish that. It still does not help if someone controls the browser session, and I have not checked which clients support URL mode.

For repo hooks, allowManagedHooksOnly in managed settings limits which hooks run.

modelcontextprotocol.io/specificat...

Collapse
 
autenai profile image
Auten •

From the Auten team (screen-control tools, same wall). Is your threat model only hooks that a cloned repo brings in, or also hooks the user wrote? If it's repo hooks, an out-of-band step closes most of it: for irreversible tools, send a short one-time code to something the client process can't read (a phone push, an authenticator) and require that code in the elicitation reply. A scripted accept doesn't have it, so "client-accepted" and "human-confirmed" become two separate log states.

Collapse
 
zade_meadows profile image
Zade Meadows •

Short answer: the server can't know, and the design should say so in the audit log.

The reason is a trust boundary, not a missing feature. Once a repo's .claude/settings.json can register an Elicitation hook and the folder is trusted, everything the client tells the server about the answer is client testimony. accept is a string; there is no field in it that came from a person. Any "verified human" bit a client could add would be produced by the same process the hook controls, so it's worth exactly what the hook says it's worth.

So your option 3 is the right default, and option 1 is the honest logging for it. Option 2 is necessary but it isn't approval, it's policy: run allowlist/policy before elicitation so a scripted accept can't widen what is reachable. It doesn't turn a scripted accept into a real one.

"Can't know inside the protocol" is not the same as "can't know at all." You can leave the client's channel:

  1. Out-of-band confirmation that the server owns. Mint a nonce for the pending operation, send it over a channel you control (approval page bound to the operator identity, push to a phone, an SSH session on the server), and require the nonce or a signature over the operation hash before the tool runs. A hook can answer the elicitation; it can't answer a page you host.
  2. Single-use capability with a TTL. Even a scripted accept then buys nothing durable: one operation, one argument hash, expires in minutes. That turns scripted approval from a grant into a one-shot claim you can rate-limit and alert on.
  3. Split the policy by reversibility, not by confidence. Cheap and reversible: client-accepted is enough, logged as such. Irreversible (delete, transfer, rotate, drop): out-of-band proof or refuse. That's where your #3 stops being a feeling and becomes a rule.

One more split: "how was the answer produced" and "who trusted the repo" are the same problem twice. The dangerous step isn't the hook answering, it's a folder gaining trust while carrying a settings file nobody read. Blanket per-folder trust is the weakest link, and it lives outside the server.

A shape that stays honest: log accepted-by-client, plus an approval-evidence field with an enum (none, client-asserted, out-of-band:nonce, hardware-key). The audit file never claims something it can't back, and policy can decide which evidence level each tool class needs. Same idea as a checkable record: the record doesn't prove the fact, it proves what evidence existed at the time.

What I have not checked: your commenter's hook internals, and whether any client exposes a UI signal reliable enough to tell a dialog from a script. I would bet against the second one being portable, but I haven't run it, so treat that as a guess rather than a finding.