Skip to content

Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project

Elicitation

Audience:engineerarchitectsecuritynon-technicalMCP spec 2026-07-28

TL;DR: Elicitation lets a server ask the user, through the host, for information it needs mid-operation. As of 2026-07-28 it is delivered through the Multi Round-Trip Requests (MRTR) pattern: the server answers the triggering call with an InputRequiredResult (resultType: "input_required") carrying an elicitation/create entry in inputRequests, and the client retries the original request with the user’s ElicitResult in inputResponses. It is a client capability (declared per request in _meta under io.modelcontextprotocol/clientCapabilities) with two modes: form (structured data described by a restricted JSON Schema, rendered as a UI by the host) and url (send the user to an external page for sensitive interactions that must not pass through the client). Every request resolves to one of three actions (accept, decline, or cancel) and the user is always in control. The server-supplied schema, message, and URL are untrusted: the host gates them, servers must never harvest secrets through a form, and URL mode carries a real phishing risk both sides must defend against.

Plain-language explanation

This is the second server-to-host inversion in MCP, alongside sampling. Normally the host drives the server (a tool call). With elicitation the server, partway through some work, realizes it needs something only the user can provide: a missing parameter, a confirmation, a choice between options, or (in url mode) a credential or third-party authorization. Rather than the model guessing or the server failing, the server asks, and the host puts a controlled prompt in front of the user. The model is kept out of it: the user answers the host’s UI directly.

Unlike sampling, elicitation is not deprecated in 2026-07-28. What changed is the delivery: the server no longer opens a request back at the client mid-call. It returns “input required” as the result of the call, the host gathers the input on its own time, and the client comes back with a retry. The section on what changed from 2025-11-25 maps the removed pieces.

The two modes

ModeForWhere the data goes
formStructured, non-sensitive input (a name, a choice, a number)In-band: the answer is returned to the client/server
urlSensitive interactions: credentials, payments, third-party OAuthOut-of-band: the user interacts on an external page; only the URL passes through the client, never the data

The split is a hard security rule, not a convenience: servers MUST NOT use form mode for passwords, API keys, tokens, or payment credentials, and MUST use url mode for those. Ordinary profile data (a name, an email address) is allowed in form mode; secrets are not.

The MRTR delivery

  1. The client sends the original request (say tools/call).
  2. The server, needing user input, returns resultType: "input_required" with an elicitation/create entry in inputRequests, plus an opaque requestState blob encoding whatever it needs to resume.
  3. The host puts the prompt in front of the user (a rendered form, or a consent dialog for a URL).
  4. The client retries the original request with a new JSON-RPC id, the ElicitResult keyed into inputResponses, and requestState echoed back byte for byte, uninspected and unmodified.
  5. The server reconstitutes its state from requestState and finishes the call, or returns another input_required round if it still lacks something (for url mode, if the out-of-band interaction has not completed yet).
ServerClientHostUserServerClientHostUserneeds user inputfirst invocation ends hereinteraction not finished yet, ask againalt[form mode][url mode]tools/call (id 1)InputRequiredResult (elicitation/create in inputRequests, requestState)render a form from requestedSchema (flat primitives)show form with provenance and the stated reasonfill in, decline, or canceltools/call retry (id 2, ElicitResult in inputResponses, requestState echoed)result (resultType complete)show the full URL and ask consentapprove opening the URLopen the URL out of band, in a sandboxed viewtools/call retry (id 2, accept with no content, requestState echoed)another InputRequiredResult, or the final result
ServerClientHostUserServerClientHostUserneeds user inputfirst invocation ends hereinteraction not finished yet, ask againalt[form mode][url mode]tools/call (id 1)InputRequiredResult (elicitation/create in inputRequests, requestState)render a form from requestedSchema (flat primitives)show form with provenance and the stated reasonfill in, decline, or canceltools/call retry (id 2, ElicitResult in inputResponses, requestState echoed)result (resultType complete)show the full URL and ask consentapprove opening the URLopen the URL out of band, in a sandboxed viewtools/call retry (id 2, accept with no content, requestState echoed)another InputRequiredResult, or the final result
Mermaid sequence diagramOpen in Mermaid Live Editor
Diagram source (Mermaid)
sequenceDiagram
    participant User
    participant Host
    participant Client
    participant Server
    Client->>Server: tools/call (id 1)
    Note over Server: needs user input
    Server-->>Client: InputRequiredResult (elicitation/create in inputRequests, requestState)
    Note over Client,Server: first invocation ends here
    alt form mode
        Client->>Host: render a form from requestedSchema (flat primitives)
        Host->>User: show form with provenance and the stated reason
        User-->>Host: fill in, decline, or cancel
        Client->>Server: tools/call retry (id 2, ElicitResult in inputResponses, requestState echoed)
        Server-->>Client: result (resultType complete)
    else url mode
        Client->>Host: show the full URL and ask consent
        User-->>Host: approve opening the URL
        Host->>User: open the URL out of band, in a sandboxed view
        Client->>Server: tools/call retry (id 2, accept with no content, requestState echoed)
        Note over Server: interaction not finished yet, ask again
        Server-->>Client: another InputRequiredResult, or the final result
    end

If the client retries without the requested input, the server SHOULD respond with a fresh InputRequiredResult asking again rather than erroring, and a server MUST NOT assume the client will ever fulfill the request or retry at all: a decline or a silent walk-away are both outcomes it has to survive.

Wire status: the pinned stack (mcp-handler 2.1.1 on @modelcontextprotocol/server 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK Client defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.

Form mode

The elicitation/create entry carries a message (why it is asking) and a requestedSchema. To keep the host’s generated UI predictable and safe, that schema is a restricted subset of JSON Schema: a flat object of primitive properties only.

  • string (with minLength/maxLength/format where format is email, uri, date, or date-time), number/integer (minimum/maximum), boolean, and enum, including titled and untitled enums and single-select and multi-select variants, all with optional default values the host should pre-populate.
  • No nested objects, arrays of objects, or advanced JSON Schema. That restriction is the point: the host can always render a simple, reviewable form, and the attack surface stays small.

The host renders the form, lets the user review and modify before sending, validates against the schema, and returns the result in the retry. For backwards compatibility, a request that omits mode MUST be treated as form mode.

URL mode

For anything sensitive, the server sends mode: "url" with a url and a message. The host shows the user the full URL, gets consent, and opens it out of band: the data (a credential, an OAuth consent) is entered on the external page and never passes through the client or the model. An accept in the retry just signals the user consented to open the URL; it does not mean the interaction finished. The client is never directly told the outcome. It learns it by retrying the original request: the server checks the echoed requestState (or its own stored state) to see whether the out-of-band interaction completed, and either returns the final result or another InputRequiredResult. Clients SHOULD give the user manual controls to retry or cancel the original request, since no notification will arrive to prompt them (2025-11-25’s notifications/elicitation/complete is gone; see below).

This is how a server brokers third-party authorization (acting as an OAuth client to some external API) without the client ever seeing those tokens; the server stores them bound to the user. It is distinct from MCP authorization (client to server), a server MUST NOT use url elicitation to authorize users for itself, and the token-passthrough prohibition still applies: the server must not reuse the client’s bearer for the third party. URL mode remains flagged in the spec as a newer feature whose design may still change in future revisions.

What changed from 2025-11-25

Keep these in mind when reading older material or serving older clients; you will still meet the legacy shapes wherever the negotiated protocol version is 2025-11-25 or earlier, which today includes any SDK Client left at its default legacy negotiation and every in-memory test suite.

  • elicitation/create is no longer a server-initiated request. It rode the open SSE response stream of the triggering call and blocked the server until the client answered. Under 2026-07-28, servers MUST deliver it inside an InputRequiredResult (MRTR); server-initiated requests are removed.
  • notifications/elicitation/complete and the url-mode elicitationId are removed (both were new in 2025-11-25). The client learns the outcome of an out-of-band interaction by retrying the original request, so a server-initiated completion signal, and the identifier used to correlate it, no longer fit the protocol. Servers that need to correlate an elicitation across retries encode their own identifier in requestState.
  • URLElicitationRequiredError (-32042) does not survive into the 2026-07-28 schema. Its end-the-call-and-retry shape was the right instinct, and MRTR made it the protocol’s normal shape: a server that needs a url-mode interaction before proceeding returns input_required instead of a special error.

The three-action model

Every elicitation resolves to exactly one action, returned inside inputResponses on the retry, and a server must handle all three distinctly:

  • accept - the user submitted. Form mode carries the data in content; url mode omits it (consent to open, not completion).
  • decline - the user explicitly said no. Offer an alternative, do not re-ask blindly.
  • cancel - the user dismissed it (closed the dialog, pressed Escape, the page failed to load). Not consent; treat as “no decision yet.”

Collapsing decline or cancel into accept, or reading a dismissed dialog as approval, is a consent bug. And an accepted form is still just data: if the form asks “proceed?” and the user submits approved: false, that accept is an explicit no.

Capability declaration

A client that supports elicitation declares the elicitation capability, naming the modes it supports (elicitation: { form: {}, url: {} }). Since 2026-07-28 removed the initialize handshake, the declaration travels in _meta under io.modelcontextprotocol/clientCapabilities on each request. A client declaring the capability MUST support at least one mode, and a server MUST NOT put an elicitation mode in inputRequests that the client did not advertise. For backwards compatibility, an empty elicitation: {} means form mode only. See capability negotiation.

The serverless shape

The 2025-11-25 version of this section was a warning about maxDuration; the 2026-07-28 version is mostly relief. Consequences of the function model described in Serverless sessions:

  • User think time is off the clock. The invocation that needs input ends the moment it returns input_required. The user can leave a form open all afternoon; the server pays nothing. The retry is a fresh invocation whose own execution is the only thing maxDuration (300s default under Fluid compute, 800s max on Pro and Enterprise) still bounds.
  • requestState is the resume mechanism. The retry can land on any instance, so everything the server needs to continue rides in the integrity-protected blob the client echoes back, not in function memory. This is the same handles-as-arguments discipline the rest of the stateless protocol uses.
  • Do not block a url-mode retry. If the retry arrives before the out-of-band interaction completes, return another InputRequiredResult rather than holding the invocation open polling for completion; the client will come back. Blocking rebuilds the old maxDuration squeeze voluntarily.

Elicitation-driven servers that do keep state (collected answers, third-party tokens) must bind it correctly: state storage MUST be protected against unauthorized access and, for remote servers, user identification MUST be derived from MCP authorization credentials (the sub claim), never from a client-asserted identity or a session id alone. On Vercel that state lives in an external store (a Marketplace Redis or Postgres), never in function memory.

Security considerations

Elicitation is a powerful, user-facing channel a malicious or compromised server can abuse, so it is heavily guarded:

  • Never elicit secrets via form mode. Passwords, API keys, tokens, payment details MUST go through url mode. The host should reinforce this and make sensitive-looking form requests suspicious. See the consent checklist.
  • The server’s schema, message, and URL are untrusted. Treat labels and descriptions like any tool output: sanitize, do not render as live markup, and always show which server is asking (provenance) and why.
  • Human-in-the-loop, always. Clear decline and cancel at any time; review-and-modify before send; the host should rate-limit so a server cannot spam prompts through repeated input_required rounds on the same call.
  • Safe URL handling (url mode). The client MUST NOT auto-prefetch the URL or its metadata, MUST NOT open it without explicit consent, MUST show the full URL for inspection first, and MUST open it in a sandboxed view the client and LLM cannot inspect. It SHOULD highlight the domain and warn on Punycode or ambiguous URIs, and SHOULD NOT render server-supplied URLs as clickable outside the url-mode url field. The server has duties too: it MUST NOT put credentials or PII in the URL itself, and MUST NOT send a pre-authenticated URL a malicious client could replay to impersonate the user.
  • requestState cuts both ways. The client MUST echo it exactly and only on the retry of the same request; it MUST NOT inspect, parse, or modify it. The server MUST treat the echoed value as attacker-controlled input: integrity-protect it (HMAC or AEAD) whenever it influences authorization or business logic, bind it to the authenticated principal and the originating request, give it a short TTL, and reject anything that fails verification.
  • Bind the elicitation to the user identity: the phishing trap. A url-mode URL can be lifted and sent to a different user. Attacker Alice triggers an elicitation, then tricks victim Bob into completing the OAuth flow, so the third-party tokens bind to Alice: account takeover. The server MUST verify that the user who started the elicitation is the one who completes it (compare the authenticated sub behind the browser session against the elicitation’s bound identity, resiliently against a tampered URL). Bind state to user identity, never to a session id alone; see Identity and principals.

Common pitfalls

  • Requesting secrets in a form - the prohibited path; use url mode.
  • Rendering server-supplied labels or URLs as live markup or clickable links - UI injection and phishing surface.
  • Auto-opening the url-mode URL - must be explicit user consent on an inspectable full URL.
  • Treating cancel as accept - a dismissed dialog is not approval.
  • Not binding the elicitation to the initiating user - enables the Alice/Bob token takeover.
  • Assuming nested or complex schemas - form mode is flat primitives only.
  • Waiting for notifications/elicitation/complete - removed in 2026-07-28, and it was optional even in 2025-11-25; the retry of the original request is how the outcome is learned, so give the user a manual retry control.
  • Inspecting or editing requestState, or reusing the original JSON-RPC id on the retry - both break the MRTR contract; a tampered blob gets the retry rejected by the server’s integrity check.
  • Blocking a retry while an out-of-band interaction finishes - return another InputRequiredResult instead of spending maxDuration polling.

Example implementation

  • examples/elicitation-server (in the repository) - a TypeScript server whose tool requests structured input mid-call with a flat, form-mode schema. Its vitest suite drives the server through an in-memory client that declares the elicitation capability and answers the request, exercising all three outcomes distinctly: accept (uses the supplied data), decline, cancel (explicitly not treated as accept), plus the accept-with-approved: false case as an explicit no. It covers form mode only; url-mode third-party auth remains a candidate for a future example. Wire status: the pinned stack (mcp-handler 2.1.1 on @modelcontextprotocol/server 2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDK Client defaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.

The protocol-level definition is in capability primitives and the gating discipline in Consent UX.

  • Consent UX - consent is the approval gate before an action; elicitation is the input gate that gathers what an action needs. Same fail-closed, human-in-the-loop discipline, now living in the MRTR retry decision.
  • Sampling-request handling - the other server-to-host inversion (the server wants the host’s model rather than the user’s input), same MRTR delivery, but deprecated.
  • Tool-result rendering - the same untrusted-server-content rendering care applies to elicitation labels and URLs.
  • Credential brokering - url-mode elicitation is how a server obtains third-party credentials without the client seeing them; the token-passthrough prohibition applies.
  • Identity and principals - binding the elicitation to a verified user identity is what defeats the phishing attack.
  • Serverless sessions - the invocation-lifetime mechanics that make MRTR the natural serverless shape.
  • The 2026-07-28 revision - the full change set behind MRTR and the m11 removals.

Bibliography