Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
106 NOTES IN PRINT
FOLIO XCVIII 2026-05-27 · 8 MIN · LONG-FORM

Elicitation: When the Server Needs to Ask the User

The server asks a person for input mid-task, with a flat form for ordinary data and a URL handoff for the secrets a form must never touch

Diagram · folio xcviii
sequenceDiagram
  autonumber
  participant Srv as Server
  participant Cli as Client
  participant U as User
  participant B as Browser
  Cli->>Srv: tools/call
  Srv->>Cli: -32042 URLElicitationRequiredError
  Cli->>U: show full URL, ask consent
  U-->>Cli: consent
  Cli->>B: open URL in a secure surface
  B-->>Srv: user completes the flow out of band
  Srv--)Cli: notifications/elicitation/complete
  Cli->>Srv: retry tools/call

Elicitation is the server asking a person for input in the middle of a task. elicitation/create runs server to client to user, the third and last of the inverted requests after sampling and roots. It has two modes: a form for ordinary structured input, and a URL handoff for the sensitive data a form is forbidden to touch. This post reads both on the wire, and the rules that keep a server from asking for your password through a text box.

This is part of MCP on the Wire, a series that takes the Model Context Protocol apart message by message, in Go. It comes out of building and running MCP servers in production, including the open-source txn2/mcp-data-platform, an Apache-2.0 platform in Go that connects AI assistants to Trino, DataHub, and S3 through one MCP endpoint, enriching every result with semantic context (ownership, lineage, PII, data quality) behind OAuth 2.1 auth, personas, and an audit trail. Everything here is read straight off the wire against spec revision 2025-11-25, with the official Go SDK at v1.6.1.

§The Two Modes

A client that supports elicitation declares it, and the declaration says which modes it can handle. The capability has two sub-flags, and the handshake from a client that supports both looks like this:

"capabilities": { "elicitation": { "form": {}, "url": {} } }

An empty elicitation: {} means form mode only, for backwards compatibility, and a server must not send a mode the client did not declare. The two modes exist for two different jobs, and choosing the wrong one is a security mistake, not a style choice.

 
Form mode
URL mode
Use for
ordinary structured input
OAuth, payment, credentials
Data path
through the client, visible to it
out of band, never reaches the client
Carries
requestedSchema (flat primitives)
url + elicitationId
Completion
accept returns content now
accept = consent; done later
Secrets
forbidden, MUST NOT
the only place they belong

§Form Mode

A deploy tool that wants the user to confirm a target sends a form-mode elicitation. The request carries a message and a requestedSchema, captured here:

{ "method": "elicitation/create", "params": {
  "mode": "form",
  "message": "Confirm the deployment target.",
  "requestedSchema": {
    "type": "object",
    "properties": {
      "environment": { "type": "string", "enum": ["staging", "production"], "description": "target environment" },
      "confirm":     { "type": "boolean", "description": "proceed with the deploy" }
    },
    "required": ["environment", "confirm"]
  } } }

The user fills it in, and the client returns the result:

{ "result": { "action": "accept", "content": { "confirm": true, "environment": "production" } } }

The requestedSchema is deliberately limited. It must be a flat object of primitive properties: strings, with optional formats like email, uri, date, and date-time; numbers and integers, with minimum and maximum; booleans; and enums, single or multi-select. No nested objects, no arrays of objects. The restriction is so any client, a terminal, a chat window, an IDE, can render the schema as a plain form without a JSON-Schema engine. Every primitive can carry a default the client pre-fills. The result is one of three actions, with content matching the schema only on accept.

§No Secrets in a Form

There is one hard prohibition, and it is the reason the two modes exist. A server must not use form mode to request passwords, API keys, access tokens, or payment credentials. The reason is the middle column of the figure: form content passes through the client, which means it can land in client-side logs, in the model’s context, in any intermediary. A password typed into a form is a password leaked into places it should never be. General profile data like a name or an email is not categorically banned, at the user’s discretion, but secrets are. Anything that grants access or authorizes a transaction has to go through the other mode, where it never touches the client at all.

§URL Mode

URL mode, new in 2025-11-25, is the secure path. Instead of a schema, the server sends a URL and an id, and the user completes the interaction out of band, in a browser, away from the client and the model:

{ "method": "elicitation/create", "params": {
  "mode": "url",
  "message": "Authorize access to your account.",
  "url": "https://auth.example.com/authorize?client_id=abc&scope=read",
  "elicitationId": "elicit-7f3a" } }
{ "result": { "action": "accept" } }

The result has no content, and the accept means something narrower than in form mode: the user consented to open the link, not that the flow finished. The interaction happens in the browser, and the server learns it completed only when it sends the client a notifications/elicitation/complete carrying the same elicitationId. The credentials the user enters, an API key, an OAuth consent, a card number, are exchanged between the browser and the server directly. They never pass through the client or the LLM context, which is the entire point.

This is not the same as the OAuth authorization between the client and the server, covered in its own post later. URL mode is the server obtaining third-party access on the user’s behalf, acting as an OAuth client to some other service, while the client’s own token stays untouched.

§The -32042 Handshake

A tool often cannot run until the user has authorized something. Rather than fail, the server returns a specific error, -32042, that tells the client a URL elicitation must happen first:

{ "error": { "code": -32042, "message": "This request requires more information.",
  "data": { "elicitations": [
    { "mode": "url", "elicitationId": "550e8400-...", "url": "https://mcp.example.com/connect?...",
      "message": "Authorization is required to access your files." } ] } } }

The flow is the sequence diagram at the top of this post. The client calls a tool, the server answers -32042 with the elicitations to complete, the user consents and finishes the flow in the browser, the server sends notifications/elicitation/complete, and the client retries the tool, which now succeeds. It is the protocol’s way of letting a tool say “authorize first, then call me again,” without ever routing the secret through the model.

§Anti-Phishing Is in the Specification

Handing a client a URL to open is a phishing surface, and the spec treats it as one, writing client UX rules into a wire protocol, which is rare enough to notice. A server must not put credentials or personal data in the URL, and must not hand over a pre-authenticated link. A client must not pre-fetch the URL or open it without consent, must show the full URL for inspection first, and must open it in a surface the client and model cannot read. It should highlight the domain to fight subdomain spoofing and warn on Punycode.

The attack these rules defend against is worth stating, because it is not obvious. A malicious user triggers an elicitation, then tricks a second, innocent user into clicking the resulting link. The innocent user completes the authorization, and the tokens get bound to the attacker’s session: an account takeover. The mitigation is a server requirement: the server must verify that the person who completes the flow is the same person who started it, by checking a session against the identity the elicitation was issued for. The spec spends real space on this because the URL is the one part of MCP a stranger can put in front of someone else.

§Accept, Decline, Cancel

Both modes share a three-action result. accept is an explicit yes, carrying content in form mode. decline is an explicit no, the user rejecting the request. cancel is a dismissal without a choice, a closed dialog, an Escape key, a browser that failed to load. A server handles them differently: process the data on accept, offer an alternative on decline, try again later on cancel. Conflating “no” with “closed the window” loses information the protocol went out of its way to preserve.

§The Go Side

A server elicits with req.Session.Elicit from inside a handler. A client opts in with an ElicitationHandler, which advertises the capability, and here is a detail worth knowing: the default handler advertises form mode only. A server that sends a URL elicitation to such a client gets refused, the SDK surfacing it as a failed call, where the spec’s own rule is a -32602 for an undeclared mode. To accept URL elicitations, a client sets its capabilities explicitly with both Form and URL. The server side stays the same either way; the client decides how far it will go.

txn2/mcp-data-platform uses form-mode elicitation as a guardrail on its Trino queries. Before a query runs, it estimates the cost with EXPLAIN IO, and if the query is too expensive or touches PII columns it elicits a confirmation first, asking, for a PII query, whether to proceed with access to the columns it found. Decline, and the query never executes and the platform records the result as user-declined.

§What’s Next

That completes the primitives, the four the client calls and the three the server calls back. What is left is the plumbing every one of them shares. Progress, Cancellation, Ping, Pagination, and _meta reads the cross-cutting utilities: how a long-running call reports progress, how either side cancels an in-flight request, how a connection is checked with a ping, how a long list is paged, and what the reserved _meta field carries.


The production data platform behind this series is txn2/mcp-data-platform, available hosted as Plexara.

← back to all notes