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 revision2025-11-25, with the official Go SDK atv1.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.
requestedSchema (flat primitives)url + elicitationIdaccept returns content nowaccept = consent; done laterMUST NOT§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.