2026-07-16
MCP Elicitation: A 28-Case Consent and Retry Drill
A deterministic MCP elicitation drill separates form input from URL secrets, then tests capability negotiation, consent, completion binding, expiry, lost notifications, and idempotent retry.
MCP elicitation looks like a request for one missing field. In production it is a routing decision across two different trust paths. A harmless deployment region can remain inside a reviewed form. A password, payment credential, or third-party authorization must leave that path entirely. Treating both cases as “show a dialog” is how secrets leak into transcripts and successful consent gets mistaken for completed work.
A deterministic policy drill was built to expose those mistakes before connecting a real client or server. It exercised 28 cases across capability negotiation, form schemas, result actions, URL admission, completion binding, expiry, lost notifications, manual retry, and the URL-required error. All 28 produced the expected result. The drill is deliberately narrower than an interoperability test: it proves that a proposed operator policy is internally consistent, not that a specific MCP implementation behaves correctly.
Operating decision: route by data sensitivity first. Keep non-sensitive, reviewable primitives in form mode. Put secrets and external transactions behind an allowlisted HTTPS URL, then track consent, completion, and retry as separate states.
The drill is evidence, but it is not an MCP conformance suite
The retained fixture contains 28 named cases and hashes to ae49ab0838bdc066f994e2fca970a360dad22c9e427112cb3edfc020cbd5f218. A Node.js probe evaluated every case and wrote the actual decision beside the expected one. There were no mismatches.
No MCP client, MCP server, browser redirect, identity provider, credential, account, network callback, or human interaction was used. Latency, transport behavior, UI clarity, browser isolation, callback security, and library compatibility remain untested. This is local policy evidence: it answers whether the rules reject the failure shapes they claim to reject.
| Surface | Cases | Matched | Local question answered |
|---|---|---|---|
| Capability routing | 4 | 4 | Was the requested elicitation mode negotiated? |
| Form request | 5 | 5 | Is the schema simple and free of credential-like fields? |
| Form result | 4 | 4 | Does accepted data exist, and is data removed on decline or cancel? |
| URL admission | 5 | 5 | Is the destination HTTPS, allowlisted, opaque, and free of secrets in the query? |
| Consent and completion | 6 | 6 | Is navigation consent kept separate from completion and bound to the same user? |
| Timeout and retry | 2 | 2 | Can a lost signal produce a visible, idempotent recovery path? |
| URL-required error | 2 | 2 | Is -32042 accepted only with a usable URL and elicitation ID? |
Some rules come from the MCP 2025-11-25 specification. Others are deliberately stricter local choices. The specification defines form and URL modes, the three result actions, URL consent versus completion, an optional completion notification, and the URL-required error code. The exact origin allowlist, flat-form restriction, credential vocabulary, 15-minute expiry, user binding, and retry ledger belong to the operator. Mixing those categories makes a local preference sound normative and a real protocol requirement sound optional.
Two paths, because the client should not see every value
Form mode returns structured content through the MCP client. That is appropriate for information the user can inspect before submission: a region, a project name, a bounded choice, or a confirmation. The current specification requires clients to let users review and modify form values before sending them.
It is not an acceptable path for passwords, API keys, access tokens, payment credentials, or similarly sensitive data. The specification is unusually direct here: servers must not request those values through form mode and must use URL mode instead. In URL mode, the client presents an external destination and the sensitive exchange happens outside MCP.
The local router turns that distinction into an admission rule:
if request contains a secret or external transaction:
require negotiated URL mode
require HTTPS and an allowlisted origin
keep credentials out of URL parameters and MCP messages
else:
permit a reviewed form with bounded primitive fields
This is intentionally less flexible than “pick the nicest interface.” Flexibility is not useful when it obscures where a credential travels. The mode should be chosen before the schema or external page is designed, because the mode defines which components are allowed to observe the value.
Capability bits are admission control, not decoration
An MCP server cannot assume elicitation support because a client has a graphical interface. The feature must be negotiated. A client advertising the elicitation capability can support form mode. URL mode requires the URL capability as well. The first four fixture cases cover exactly that boundary: missing elicitation is denied, negotiated form is admitted, URL without URL support is denied, and negotiated URL is admitted.
Perform this check before constructing the request. A late capability failure creates awkward recovery code and tempts developers to fall back from URL mode to a form. That fallback is unsafe when URL mode was selected because the input is sensitive. “Cannot elicit here” is a valid result; “ask for the secret another way” is not.
A useful server-side record names the decision rather than preserving only a boolean:
{
"requestedMode": "url",
"negotiated": true,
"reason": "third-party authorization",
"fallback": "none"
}
The reason matters during an incident. Without it, a later maintainer cannot distinguish “URL was available” from “URL was required to keep a secret out of MCP.”
A form schema is still untrusted input
The protocol supports structured form requests, but a client should not render arbitrary schemas blindly. The drill admits one flat object containing a required enum and rejects nested objects, arrays, a credential-like property, and a password request hidden in the surrounding message.
The nested-object and array denials are local policy, not a claim that MCP forbids them. They establish a small surface for an initial implementation. A flat primitive form is easier to review, validate, log without ambiguity, and reproduce across clients. Richer schemas can be introduced after there are fixtures for their rendering and validation semantics.
| Form shape | Decision | Reason |
|---|---|---|
| Required region enum | ALLOW | Non-sensitive, bounded, and reviewable |
| Nested profile object | DENY | Outside the initial flat-form policy |
| Array of project IDs | DENY | Ordering, cardinality, and rendering semantics are not yet tested |
api_token string field | DENY | A secret must not return through the client |
| Neutral field under “Enter your password” | DENY | Field names alone are not a sufficient secret detector |
The credential vocabulary is a backstop, not a classifier worth trusting on its own. Production clients should combine field inspection with server identity, request purpose, schema validation, user-facing review, and a conservative refusal path. A field called value does not become safe because it avoids the word password.
Accept, decline, and cancel must remain different outcomes
Elicitation returns one of three actions. accept means the user submitted the request. decline means the user chose not to provide the requested information. cancel means the interaction ended without a decision, which may happen because the dialog closed or the surrounding operation stopped.
Collapsing decline and cancel into one generic error removes useful product behavior. A decline may end the operation cleanly. A cancellation might keep the task resumable. Neither outcome should smuggle partially entered content back to the server. The drill strips content attached to decline or cancel and rejects an accepted form that has no content object.
These distinctions also belong in metrics. “User rejected the request” and “window disappeared” suggest different problems. One may be a trust issue; the other may be a UI or lifecycle defect. A single counter named elicitation_failed hides both.
URL admission needs more than an HTTPS prefix
Origin is the boundary.
URL mode moves the sensitive exchange out of MCP, but it does not make every external page trustworthy. The client should show the destination host and obtain consent before navigation. The server should issue a unique elicitation ID. The external service should bind its state to the correct user and session.
The local fixture admits only https://consent.example.com. It rejects plain HTTP, a lookalike origin, an api_token query parameter, and a one-character correlation ID. Exact origin matching is stricter than suffix matching: consent.example.com.attacker.net must not inherit trust from the string it contains.
Do not put personally identifiable information or credentials in the URL. URLs leak into browser history, intermediary logs, analytics systems, support screenshots, referrer data, and copy-paste workflows. Use a short-lived opaque identifier that resolves server-side to state the external page needs. The identifier should not encode the email address, account number, requested scope, or secret.
Consent is not completion
This is the most consequential state-machine detail. When a user accepts a URL elicitation request, the user has agreed to open or interact with the external URL. The authorization, payment, or other external operation has not necessarily finished. The drill therefore maps URL accept to CONSENT_PENDING, never directly to COMPLETE.
The MCP 2025-11-25 lifecycle allows the server to send an optional notifications/elicitation/complete notification after the out-of-band operation finishes. The notification carries the same elicitation ID so the client can correlate it. The local registry then applies additional policy: same tenant, same user, known ID, pending state, and an unexpired window.
That registry produced five distinct outcomes:
- A matching pending entry completed once.
- The same completion a second time was ignored.
- An unknown ID was ignored.
- A completion for the wrong user was denied.
- A completion after the 15-minute local window was denied.
The same-user check is not busywork. An external URL often carries authorization state that can be phished or replayed. Binding completion to the user who initiated the elicitation keeps one user’s browser action from satisfying another user’s blocked tool call.
Troubleshooting a lost completion notification
Completion notifications are optional and can be lost. A client restart, network interruption, server crash, or closed window can leave the external operation complete while the original tool call still appears blocked. A design that handles only the happy path forces users to repeat sensitive work or leaves operators guessing which side committed.
The fixture contains one explicit loss case. The user reports that the external action finished, but no completion notification arrived. The result is RETRY_REQUIRED, not failure and not assumed success. A manual retry with the same elicitation ID then resolves the stored state and completes once.
Idempotency belongs at the operation boundary. Retrying the original tool should query or consume the same external result, not create a second authorization, charge, registration, or connection. Store enough state to answer four questions:
- Which user and tenant initiated this elicitation?
- Which original tool request is waiting?
- Did the external service finish, and what stable result identifier did it return?
- Has that result already been applied to the original operation?
A cancel path is equally important. If the result cannot be recovered, the user must be able to abandon the pending state without an administrator editing storage. Retry and cancel are operational features, not polish added after the protocol works.
-32042 is a recovery contract, not a generic error bucket
The URL-required error gives a tool a structured way to say that it cannot continue until URL elicitation occurs. The code is -32042. The drill admits the code only when it accompanies a valid external URL and opaque elicitation ID, and denies a generic internal-error code carrying the same fields.
Use the error for the condition it names. It should not become a wrapper for every authentication failure, missing form value, denied permission, or upstream outage. A client that receives it needs enough validated data to present the destination, obtain consent, correlate later completion, and offer retry or cancel.
{
"code": -32042,
"data": {
"url": "https://consent.example.com/start?flow=auth",
"elicitationId": "elc_4e7f3b45"
}
}
The example URL contains only a flow label; the correlation state remains opaque. If the client did not negotiate URL elicitation, the server should return an understandable unsupported-path result rather than downgrading to a credential form.
Failure modes the ledger must keep visible
A pass count is not enough for release review. Retain a small ledger for each scenario: case ID, mode, expected action, actual action, match, correlation ID class, policy revision, and fixture hash. Do not log form content or URL secrets. The ledger should make the state transition explainable without becoming another secret store.
| Failure | Visible decision | Operator response |
|---|---|---|
| Mode was not negotiated | DENY before request creation | Use a non-elicitation workflow or a compatible client; never downgrade a secret |
| Form asks for a credential | DENY with policy reason | Redesign as URL mode |
| External origin is not allowlisted | DENY before navigation | Review the integration and register an exact trusted origin |
| Completion ID is unknown | IGNORE and record a bounded anomaly | Do not create state from an unsolicited notification |
| User binding differs | DENY | Investigate replay, session mix-up, or phishing |
| Notification is lost | RETRY_REQUIRED | Retry the same logical operation idempotently or cancel it |
Alerts should be proportional. An unknown duplicate can be ignored after a bounded log entry. Repeated wrong-user completions deserve a security signal. A rising rate of lost notifications points to lifecycle or transport trouble. The system becomes operable when each state leads to a named response rather than a generic modal saying “try again.”
Testing the boundary against real client and server pairs
Ship the feature only when the implementation can demonstrate all of the following against representative client/server pairs:
- Capability negotiation blocks unsupported modes before sensitive data is requested.
- Form requests are rendered from validated schemas and never ask for credentials or payment data.
- Users can review, edit, accept, decline, or cancel without partial content leaking on non-accept actions.
- URL destinations use HTTPS, show the target host, match an exact origin policy, and contain no secret or PII.
- Opaque elicitation IDs bind pending state to the initiating user, tenant, and original tool request.
- URL acceptance records consent only; completion requires separate external evidence.
- Duplicate, unknown, expired, and wrong-user completions have deterministic outcomes.
- A lost completion signal has an idempotent manual retry and a cancel path.
- Logs retain decisions and identifiers without retaining elicited secrets.
The local 28-case drill is a useful admission test for that plan. It is not the release gate by itself. The next evidence should come from real protocol fixtures: at least two clients, a test server, browser redirects, callback-state tampering, restart recovery, duplicate notifications, and a trace audit proving secrets never enter MCP logs.
If the integration runs on a managed worker, create an AI agent on GolemWorkers and preserve the same boundaries in its tool policy, secrets, browser access, approvals, and retry state. Hosting changes who operates the runtime. It does not merge consent with completion or make an untrusted URL safe.
Where the protocol claims come from
- MCP 2025-11-25: Elicitation — form and URL modes, capability negotiation, result actions, sensitive-data rules, completion notification, and security requirements.
- MCP 2025-11-25: Lifecycle — initialization and negotiated capability boundaries.
- MCP 2025-11-25: Tools — tool invocation context for elicitation-required recovery.
- MCP 2025-11-25 changelog — introduction of URL mode in the current specification revision.
- SEP-1036: URL mode elicitation — design rationale and lifecycle discussion for out-of-band sensitive interactions.