Skip to content

MCP elicitation: how servers ask users for input

On this page

MCP elicitation lets a server pause a tool call and ask the person for something it needs, such as a choice, a missing value or consent to open a sign-in page. The client shows the request, the person accepts, declines or cancels, and the server continues with the answer.

This guide was checked against the MCP elicitation specification, revision 2026-07-28, on 30 October 2026. That revision changed how the request travels, so examples written for earlier revisions look different.

What elicitation is for

Without elicitation, a server that lacks an input has two poor options: fail the call, or guess. With it, the server asks the person directly, through the client's own interface. Typical uses:

  • Picking one of several matches, such as which of three "Acme" customer records a refund applies to.
  • Filling in a value the model didn't provide, like a date range or a project name.
  • Confirming an action in the server's own words before it runs.
  • Sending the person to a web page to sign in to a third-party service or enter a credential the server must never see in the chat.

Elicitation is aimed at the person, not the model. The answer goes to the server as part of the retried call.

Form mode and URL mode

The 2026-07-28 specification defines two modes:

ModeWhat the person seesWhere the data goes
formA form the client builds from the server's schemaThrough the client to the server, so the client sees it
urlA request to open a URL, showing the full addressDirectly from the browser to the server's web page; the client only sees the URL

Form mode is the default: a request without a mode field is treated as a form. URL mode was introduced in revision 2025-11-25, and the specification still notes that its design may change.

The split exists because of one rule. Servers must not use form mode to ask for passwords, API keys, access tokens or payment credentials, and must use URL mode for anything involving them. Names, email addresses and usernames are not categorically banned from forms; the specification leaves those to the server, subject to the person's ability to review and decline.

How the request travels in revision 2026-07-28

Earlier revisions let a server send its own JSON-RPC request to the client in the middle of a call. Revision 2026-07-28 removed that. Servers now use the multi round-trip requests (MRTR) pattern: the server ends the call with a result that says input is required, and the client retries the call with the answers. The changelog lists this as a breaking change.

1. The client declares support

There's no initialize handshake in this revision, so every request carries the client's capabilities in _meta. A client that supports both modes sends:

JSON
{
  "_meta": {
    "io.modelcontextprotocol/clientCapabilities": {
      "elicitation": {
        "form": {},
        "url": {}
      }
    }
  }
}

An empty "elicitation": {} means form mode only. A server must not send a mode the client didn't declare, and must not elicit at all from a client that declared nothing.

2. The server answers with input_required

When a tools/call needs input, the server returns an InputRequiredResult. This is the specification's example from the tools page:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" }
            },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0..."
  }
}

inputRequests is a map: the keys are the server's own names for each question, so one result can ask several things at once. requestState is an opaque string the server uses to pick up where it left off. The original request is finished at this point; nothing is held open.

InputRequiredResult is allowed only on tools/call, prompts/get and resources/read.

3. The client retries with the answers

The client shows the form, collects the answer and sends the same call again with a new JSON-RPC id, the answers in inputResponses under the same keys, requestState echoed back unchanged, and the _meta protocol version and client capabilities that every 2026-07-28 request carries:

JSON
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    },
    "inputResponses": {
      "github_login": {
        "action": "accept",
        "content": {
          "name": "octocat"
        }
      }
    },
    "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0...",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "elicitation": { "form": {}, "url": {} }
      }
    }
  }
}

The server can return the final result, or another input_required if it still needs something. Clients must not inspect or change requestState, and servers must treat it as attacker-controlled: if it affects authorization or business logic, they protect it with an HMAC or AEAD and reject anything that fails verification. The specification also recommends binding it to the authenticated user, a short expiry and the original request.

Earlier revisions

Under revision 2025-11-25, the same question was a server-to-client JSON-RPC request with its own id, sent while the tool call was still open:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Please provide your GitHub username",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      },
      "required": ["name"]
    }
  }
}

The params are the same in both revisions. What changed is the envelope, and with it the server's state: a server on the new revision doesn't need to keep the call open or share session storage between instances. Revision 2026-07-28 also removed the notifications/elicitation/complete notification and the elicitationId field that 2025-11-25 used for URL mode.

What requestedSchema allows

Form schemas are deliberately limited so any client can render them. The schema is a flat object whose properties are primitives:

TypeOptions
stringtitle, description, minLength, maxLength, default, and format of email, uri, date or date-time
number or integertitle, description, minimum, maximum, default
booleantitle, description, default
Single-select enumA string with enum, or oneOf entries of const and title for display labels
Multi-select enumAn array with minItems, maxItems and items holding enum or anyOf

Nested objects, arrays of objects and other advanced JSON Schema features are not supported. If you need a richer form, split the question into several requests or use URL mode and host the form yourself. Clients that support defaults should prefill them, and both sides should validate the answer against the schema.

Accept, decline and cancel

Every answer has one of three actions, in both modes:

ActionMeaningcontent
acceptThe person submitted the form, or agreed to open the URLThe form data; omitted in URL mode
declineThe person explicitly said noUsually omitted
cancelThe person dismissed it without choosing: closed the dialog, pressed Escape, or the page failed to loadUsually omitted

Servers must handle decline and cancel, and shouldn't assume an elicitation will ever be answered. A reasonable pattern is to offer an alternative after a decline and to ask again later after a cancel.

In URL mode, accept only means the person agreed to open the page. The client never learns what happened there. On the retry, the server checks its own state to see whether the interaction finished, and either returns the result or asks again. Clients should give people a way to retry or cancel the original call while they wait.

URL mode: credentials and third-party sign-in

URL mode is how a server collects something sensitive without it passing through the client, the model or any MCP server in between. A request looks like this:

JSON
{
  "method": "elicitation/create",
  "params": {
    "mode": "url",
    "url": "https://mcp.example.com/ui/set_api_key",
    "message": "Please provide your API key to continue."
  }
}

The common case is a server that calls another service on the person's behalf, such as a server that files tickets in a third-party tracker. The server sends the person to its own "connect" page, that page starts the third-party OAuth flow, and the server stores the resulting tokens against the person's identity.

This is separate from MCP authentication. The client's sign-in to the MCP server itself uses the authorization flow, and a server must not use URL mode to authorize people for itself. Credentials obtained through URL mode stay with the server and are never sent to the client. The server also must not reuse the client's token for the third-party service; that's token passthrough, which the specification forbids.

Security rules in the specification

For servers:

  • Bind each elicitation to the client and user identity. For remote servers, derive the user from the authorization token, such as its sub claim, never from something the person typed.
  • Don't put credentials or personal data in a URL, and never send a pre-authenticated URL that would let whoever opens it act as the user.
  • Before accepting anything on a URL-mode page, confirm the person who opened it is the person who started the elicitation. Otherwise an attacker can trigger an elicitation and send the link to a victim, who then connects their own third-party account to the attacker's session. The specification's example compares the sub from the MCP authorization server with the session cookie on the page.
  • Don't include clickable URLs in form fields, and use HTTPS outside development.

For clients:

  • Show which server is asking, and offer clear decline and cancel options.
  • In form mode, let the person review and change answers before sending.
  • In URL mode, show the full URL and get consent before opening it. Never prefetch it, and open it somewhere the client and model can't read, such as the system browser. The specification also recommends highlighting the domain and warning about Punycode.

Which clients support MCP elicitation

As of October 2026, from each client's official documentation:

ClientSupportSource
Claude CodeForm and URL mode. Shows a dialog for forms and asks before opening a URL in your browserClaude Code MCP docs
CursorListed as supportedCursor MCP docs
CodexNot mentioned in its MCP documentationCodex MCP docs
Claude (custom connectors)Not mentioned in the connector articleCustom connectors

"Not mentioned" doesn't mean unsupported; it means we couldn't verify it. Check the capabilities a client actually declares before relying on it.

Claude Code has a few documented details worth knowing if you build servers:

  • It declares elicitation: {form: {}, url: {}} on connections that use revision 2026-07-28, and on earlier-revision connections to servers in your own MCP configuration from v2.1.287. On other earlier-revision connections, such as claude.ai connectors, it declares the bare elicitation: {}, which means form mode only.
  • It opens a URL-mode link by passing it to the system URL handler, with a length cap of about 8,000 characters, less for URLs heavy with percent-escapes. Above the cap, the person can only decline.
  • An Elicitation hook can answer requests without showing the dialog.

Elicitation through an MCP gateway

A gateway sits between your AI app and the upstream MCP server, so it has to carry the question back to the right person. In Toolstile, an elicitation from an upstream server is forwarded to the client of the agent that made the call, and the answer goes back to that upstream server. Your client shows its own dialog, with its own consent controls, so you decline or cancel there as usual.

A few consequences:

  • The upstream server only gets elicitation if your client declared it. If your client supports only form mode, the server sees that.
  • A URL-mode link opens the upstream server's own page in your browser, so what you enter there goes to that server.
  • On the 2026-07-28 revision, answer within ten minutes. Toolstile holds the original upstream call for at most that long.
  • A tool set to Require approval can't currently take new elicitation input on its retry, because the approval covers only the arguments the reviewer saw. Per-agent permissions explains approvals.

The MCP gateway guide covers what else a gateway does, and remote MCP servers explains how to add one to each client.

Adding elicitation to your own server

  1. Read the client's declared elicitation capability on each request, and only send modes it lists.
  2. Return input_required from tools/call, prompts/get or resources/read with a clear message that says why you need the input.
  3. Keep requestedSchema flat and primitive, and validate the answer you get back.
  4. Protect requestState with an HMAC or AEAD and bind it to the user, an expiry and the request.
  5. Use URL mode for anything secret, and check on your page that the person who opened it is the one who was asked.
  6. Handle decline, cancel and no answer at all without failing in confusing ways.

The official SDKs implement the message types; check that the version you use supports revision 2026-07-28 before relying on the new envelope.