Elicitation and Multi Round-Trip Requests
This guide explains how a server asks for user input in the middle of processing (Elicitation and Multi Round-Trip Requests, hereafter MRTR), and how to deal with the client features that are now deprecated (Roots, Sampling, and Logging). Because the 2026-07-28 version made the protocol stateless, the way a server sends requests to a client changed. For the overall list of changes, see MCP 2026-07-28 Spec Changes and Migration; for the request flow of Tools, see Tools - Defining and Executing Tools.
1. What This Document Covers
- The two modes of Elicitation
- Multi Round-Trip Requests (MRTR)
- What can go into
inputRequests - Handling
requestState - Differences from the old approach
- Security
- Deprecation of Roots
- Deprecation of Sampling
- Deprecation of Logging
The two modes of Elicitation
Elicitation is how a server asks the user for additional information through the client. The client does not give up control of its interaction with the user or of the data it shares. There are two modes.
| Mode | Use | Data flow |
|---|---|---|
Form (form) | Structured input (name, email, choices, and so on) | Goes through the client. The client renders the form |
URL (url) | Highly sensitive out-of-band interactions such as authentication, payment, or entering an API key | Data other than the URL is not visible to the client |
mode may be omitted, in which case it is treated as form. The request requires a message that explains why the input is needed.
The client declares the modes it supports in its capabilities. An empty elicitation: {} means form only. A server must not send a request for a mode the client has not declared.
{
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": { "form": {}, "url": {} }
}
}
}
Form mode specifies a subset of JSON Schema in requestedSchema. To keep client implementations simple, it is limited to a flat object whose properties are only primitive types. The usable types are strings (minLength, maxLength, and format of email / uri / date / date-time), numbers and integers (minimum, maximum), booleans, and enums (single select, single select with display names via oneOf and const, and multi select). Nested structures and arrays of objects are deliberately excluded. If you specify default, the client can use it as the initial value of the form.
The response is expressed with three actions.
action | Meaning | content |
|---|---|---|
accept | The user explicitly approved and submitted | For a form, values that match the schema. Omitted in URL mode |
decline | The user explicitly declined | Usually omitted |
cancel | Dismissed without an explicit choice (closing the dialog, Escape, and so on) | Usually omitted |
The server handles each of the three appropriately: for decline, offer an alternative; for cancel, ask again later; and so on. Write the server on the assumption that the user will decline or cancel, and that the client may fail to process the request.
URL mode specifies in url the URL that the user opens. It is for interactions that must not pass through the client or the model's context, such as authentication, payment, and entering an API key that the server will store.
{
"method": "elicitation/create",
"params": {
"mode": "url",
"url": "https://mcp.example.com/ui/set_api_key",
"message": "Please provide your API key to continue."
}
}
An accept in URL mode only means that the user agreed to open the URL. The interaction happens out of band, and the client is not told whether it was completed. The server decides for itself whether it was completed when it receives the retry (described later). Clients are recommended to provide controls that let the user retry or cancel the original request.
URL mode is not for the client's authorization to the MCP server itself (MCP Authorization - OAuth 2.1 and Metadata Discovery). It is used when the server obtains authorization to an external (third-party) service on the user's behalf. The client's bearer token does not change.
Multi Round-Trip Requests (MRTR)
In the 2026-07-28 version, a server cannot send an independent request to the client. Instead, the server answers a client request with "additional input is needed," and the client retries the same request with the input attached. This is MRTR. It makes server-initiated interactions possible without requiring shared storage or sticky load balancing.
The flow has four steps: the client's ordinary request, the server's input_required, the retry with the input attached, and the server's final result (the table is in "The Flow of MRTR" below).
The key point is that each request is completely independent. The server instance that handles the retry needs nothing beyond the information contained in the retry request.
MRTR can be used only on the following client requests. On any other request, the server must not return input_required.
tools/callresources/readprompts/get
The rules for the client are as follows.
- If there are
inputRequests, the client must build that input before retrying. If there are none, it may retry immediately - If there is a
requestState, attach it to the retry exactly as is. Do not inspect, parse, or modify it. If the server did not return arequestState, the client must not attach one - The JSON-RPC
idof the retry must be a different value from the first request, because they are separate requests inputRequestsandrequestStateaffect only the retry. They cannot be used for other requests sent in parallel
The rules for the server are as follows.
- An
InputRequiredResultincludes at least one ofinputRequestsandrequestState - Do not put into
inputRequestsa kind of request the client has not declared a capability for - There is no guarantee that the client will satisfy the input or retry. For a retry with missing required information, do not return an error; return
input_requiredagain and ask for it - If
inputResponsescontains unexpected items, they may be ignored
What can go into inputRequests
inputRequests is a map keyed by identifiers the server assigns, and each value is one of the following requests. Keys are unique within that request.
| Value request | Content | Status |
|---|---|---|
elicitation/create | Request for user input (form, URL) | Recommended |
sampling/createMessage | Request for generation from the client's LLM | Deprecated |
roots/list | List of the client's roots | Deprecated |
The corresponding inputResponses is a map that puts the client's result for each request (for Elicitation, the action and content) under the same key.
{
"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": "AEAD-protected blob"
}
The retry attaches inputResponses and requestState to the same tools/call.
{
"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": "AEAD-protected blob"
}
}
Handling requestState
requestState is an opaque string, meaningful only to the server, that the server uses to carry state across retries. The format is free (base64 JSON, an encrypted JWT, binary, and so on). The server can pack state into it and leave it with the client instead of storing it on the server side.
Because it passes through the client, however, it must be treated as attacker-controlled input.
- If
requestStateinfluences authorization, resource access, or business logic, it MUST be integrity-protected with HMAC or AEAD, and state that fails verification MUST be rejected. Protection may be omitted only when tampering can cause nothing worse than the request failing - To prevent replay, servers SHOULD include the following inside the integrity-protected payload and verify them on receipt
- The authenticated principal (reject state presented by a different principal)
- A short expiry (reject state presented after it lapses)
- An identifier of the original request (such as the method name and a digest of the main parameters; reject state presented on a request that does not match)
- These narrow the range of reuse and prevent reuse across users and requests, but they do not guarantee single use. If a state must be consumed at most once, as in a one-time redemption, enforce it on the server side
When a server embeds identifiers (a user ID, an order number, an external authorization state) in requestState, include them in what is signed, do not take the decrypted values at face value, and check them against the current credentials. Do not put secrets themselves (such as a token for an external service) into requestState, even if encrypted; keep them on the server side.
Differences from the old approach
In the old approach, while handling a client's request, the server sent elicitation/create, sampling/createMessage, and roots/list as independent requests, for example over an SSE stream, and waited for the client's response. The 2026-07-28 version does not support this. It is a breaking change. All requests from the server to the client are made with MRTR.
| Aspect | Old approach | MRTR |
|---|---|---|
| Origin | The server sends a request | The server asks for input in response to the client's request |
| Waiting | The original request stays open and the server waits for the response | The original request completes first. The retry is an independent request |
| State | Held in the connection or inside the server | Packed into requestState and left with the client |
| Instance | Must return to the same instance | Any instance can handle it |
| Long-lived connection | SSE or the like is needed | Not needed |
notifications/elicitation/complete, which told the client that a URL mode interaction had completed, was also removed as a server notification. When the client retries, the server learns from requestState (or from state it stores itself) whether the out-of-band interaction has completed, and returns the result, or returns input_required again if it has not. An implementation that waits inside the retry until completion is also possible. The basic rule is that you learn the result from the retry.
Not having to store state is an advantage of MRTR, but it is not a requirement. If the server stores state, it must bind it securely to the user and protect it from unauthorized access. For remote servers, the user's identity is derived from credentials obtained through MCP authorization (for example, the sub claim) (MCP Authorization - OAuth 2.1 and Metadata Discovery).
Security
The most important rule of Elicitation is never ask for sensitive information in form mode. Passwords, API keys, access tokens, and payment credentials must not be requested with a form (MUST NOT), and URL mode must be used. General contact and attribute information such as a name, email address, or username is not categorically prohibited. At the server's discretion, and on the premise that the user can review and decline, it can be requested with a form.
The client must do the following.
- Make clear in the UI which server is asking for the information
- Show clear options to decline and to cancel
- For forms, let the user review and modify the content before sending
- For URL mode, show the destination domain and obtain the user's consent before opening it
For abuse of URL mode (phishing), detailed rules about URLs are defined.
The server must:
- Not put sensitive user information (credentials, personal information) in the URL
- Not hand over a pre-authenticated URL to a protected resource, because a malicious client could impersonate the user with it
- Use HTTPS (except in development environments)
- Not put a URL meant to be clicked inside a field of a form mode request
The client must:
- Not automatically prefetch the URL or its metadata
- Not open the URL without the user's explicit consent
- Show the full URL so the user can examine it before consenting
- Open it in a secure way that does not let the client or the model inspect the page contents or the input (such as the OS's standard browser view). On iOS, for example,
SFSafariViewControlleris suitable, butWKWebViewis not - Highlight the domain to prevent subdomain spoofing, and show warnings for ambiguous or suspicious URIs such as ones containing Punycode
Another important attack is account takeover by forwarding the URL. Because URL mode returns a URL, an attacker can send that URL to a victim.
- A malicious user (Alice) connects to the server and triggers an Elicitation
- The server generates an OAuth authorization URL for an external service, and Alice's client displays it
- Alice does not open it herself but gets Bob, another user of the same server, to open the URL
- Bob completes it, thinking it is authorization for his own connection
- The server treats the callback as belonging to Alice's request and binds the external service's token to Alice's identity. As a result, Alice can use data that belongs to Bob's account
To prevent this, the server must verify that the user who started the Elicitation and the user who opens the URL and completes the authorization are the same person. A common method is to return the server's own "connect URL" (for example, https://mcp.example.com/connect?...) instead of the external authorization endpoint. That page identifies the user with a session cookie or the like, confirms that it matches the user for whom the Elicitation was generated (the sub from the MCP authorization server), and only then redirects to the external authorization server. The identification method must not be breakable by tampering with the URL.
Relatedly, the server must not pass tokens obtained from an external service to the MCP client. Also, token passthrough, reusing the client's token against an external service, is forbidden. See MCP Security Best Practices - Attacks and Mitigations for details. In addition, the server must not trust a user identification that the client supplies as a self-declaration (do not treat input such as I am [email protected] as identity verification).
Deprecation of Roots
Roots was a feature for the client to tell the server which directories and files are relevant. It was deprecated in the 2026-07-28 version (SEP-2577). It was only "informational guidance," servers had no obligation to follow it, and adoption was limited.
The replacement is to pass the working directory or files explicitly through tool arguments, resource URIs, or server configuration. For example, pass a path as an argument to a tool such as analyze_project, and have the server resolve it only within the permitted range.
Deprecation of Sampling
Sampling was a feature for the server to ask the client's LLM for a generation. It is deprecated as well. It was complex to implement (human approval, model selection, tool loops) and was also an attack surface for prompt injection and data exfiltration.
New implementations do not adopt it, and a server that needs an LLM uses an LLM provider's API directly. The server then controls model selection, parameters, and streaming. Existing implementations are also encouraged to migrate.
The thisServer and allServers values of includeContext are separately deprecated. Servers avoid them (they can be omitted, since the default none applies) and must not use them unless the client has declared sampling.context.
Deprecation of Logging
Logging (logging/setLevel, notifications/message) is also deprecated. The replacement is stderr for stdio servers, and OpenTelemetry for structured observability. See MCP Observability and Auditing for details.
All three deprecations remain in the specification for at least 12 months from the release of the 2026-07-28 version. In the meantime, behavior on the wire does not change and compatibility is kept. Implementations are recommended to emit a warning when a deprecated capability is negotiated. In MRTR, roots/list and sampling/createMessage can still be put into inputRequests, but new implementations do not use them.
2. The Flow of MRTR
| Step | Sender | Content |
|---|---|---|
| 1 | Client | Sends tools/call as usual |
| 2 | Server | If more information is needed, returns input_required and inputRequests |
| 3 | Client | Asks the user, then resends the same request with inputResponses attached |
| 4 | Server | Returns a complete result once it has the necessary information |
3. Common Pitfalls for Developers
3.1 Reusing the JSON-RPC id on retry
A retry is a new, independent request. Do not use the same id as the first request.
3.2 Processing requestState
An implementation where the client parses and rewrites requestState, or does not attach it, fails because the server cannot restore the state. Return the received string as is.
3.3 Trusting requestState without a signature
If you put "approved" or a "user ID" into requestState in plain text and trust it as is, the client can rewrite it to bypass authorization. Protect its integrity, bind it to the principal, the expiry, and the original request, and enforce single use on the server side if you need it.
3.4 Trying to keep waiting state inside the server
An implementation that waits for the response in server memory, as in the old approach, does not work with multiple instances. Pack the needed state into requestState (protected), or keep it in shared storage.
3.5 Sending inputRequests of an unsupported kind
Do not put elicitation/create in when the client has not declared elicitation. Check the capabilities in the _meta of each request.
3.6 Returning input_required from a method that cannot
MRTR applies only to tools/call, resources/read, and prompts/get.
3.7 Mistaking a URL mode accept for completion
accept is only consent. The server that receives the retry decides whether the user actually completed it. If not yet, return input_required again.
3.8 Asking for sensitive information in a form
A form for an API key or password violates the specification. Send the user to a trustworthy page of the server's own with URL mode.
3.9 Waiting for notifications/elicitation/complete
It has been removed. You learn about completion from the retry.
4. Code Examples
Asking for user confirmation in the middle of a tool (server)
This is an example in TypeScript that implements MRTR while protecting requestState with an HMAC. SDK APIs change, so focus on the exchange at the protocol level.
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.STATE_SECRET!; // Manage the key in a secret manager or the like
type State = { sub: string; tool: string; argsDigest: string; exp: number };
function seal(state: State): string {
const body = Buffer.from(JSON.stringify(state)).toString("base64url");
const mac = createHmac("sha256", SECRET).update(body).digest("base64url");
return `${body}.${mac}`;
}
function open(token: string, expectedSub: string, expectedDigest: string): State {
const [body, mac] = token.split(".");
const expected = createHmac("sha256", SECRET).update(body).digest("base64url");
if (!timingSafeEqual(Buffer.from(mac), Buffer.from(expected))) {
throw new Error("invalid requestState");
}
const state: State = JSON.parse(Buffer.from(body, "base64url").toString());
// Verify that the principal, expiry, and original request match
if (state.sub !== expectedSub || state.exp < Date.now() || state.argsDigest !== expectedDigest) {
throw new Error("requestState does not match this request");
}
return state;
}
async function handleDeleteProject(req: ToolCallRequest, principal: string) {
const digest = digestOf(req.params.name, req.params.arguments);
const responses = req.params.inputResponses;
if (!responses?.confirm) {
return {
resultType: "input_required",
inputRequests: {
confirm: {
method: "elicitation/create",
params: {
mode: "form",
message: `Delete project ${req.params.arguments.id}? This cannot be undone.`,
requestedSchema: {
type: "object",
properties: { ok: { type: "boolean", title: "Delete" } },
required: ["ok"],
},
},
},
},
requestState: seal({
sub: principal,
tool: req.params.name,
argsDigest: digest,
exp: Date.now() + 5 * 60_000,
}),
};
}
open(req.params.requestState!, principal, digest); // Throws if verification fails
const r = responses.confirm;
if (r.action !== "accept" || !r.content?.ok) {
return { resultType: "complete", isError: true, content: [{ type: "text", text: "Deletion was not confirmed." }] };
}
await deleteProject(req.params.arguments.id);
return { resultType: "complete", content: [{ type: "text", text: "Deleted." }] };
}
The retry loop (client)
async function callToolWithInput(client: McpClient, name: string, args: object) {
let inputResponses: Record<string, unknown> | undefined;
let requestState: string | undefined;
for (let attempt = 0; attempt < 5; attempt++) {
// The JSON-RPC id is new for every call (the SDK assigns it)
const result = await client.request("tools/call", {
name,
arguments: args,
...(inputResponses && { inputResponses }),
...(requestState !== undefined && { requestState }), // Return the received value as is
});
if (result.resultType !== "input_required") return result;
requestState = result.requestState; // Do not parse it
inputResponses = {};
for (const [key, request] of Object.entries(result.inputRequests ?? {})) {
inputResponses[key] = await askUser(request); // Show it to the user and get the answer
}
}
throw new Error("too many input_required rounds");
}
A URL mode request
{
"resultType": "input_required",
"inputRequests": {
"connect_github": {
"method": "elicitation/create",
"params": {
"mode": "url",
"url": "https://mcp.example.com/connect?ticket=opaque-one-time-ticket",
"message": "Connect your GitHub account to continue."
}
}
},
"requestState": "AEAD-protected blob"
}
Do not put sensitive user information in the URL, and do not make it a pre-authenticated URL. Make the ticket a single-use, opaque value on the server side that is bound to this user.
5. Summary
- Elicitation has form mode for structured input and URL mode for highly sensitive interactions. Do not ask for sensitive information in a form
- Requests from the server to the client are made with MRTR (
input_required,inputRequests,inputResponses,requestState). The old server-initiated requests are not supported - MRTR can be used on
tools/call,resources/read, andprompts/get - A retry is an independent request with a different
id. The client returnsrequestStateas is requestStateis attacker-controlled input. Protect its integrity, and bind it to the principal, the expiry, and the original request- In URL mode, verify that the user who opens the URL is the same user who started the Elicitation. The client shows the full URL and obtains consent
- Roots, Sampling, and Logging are deprecated. Migrate to tool arguments and configuration, direct use of an LLM provider, and stderr or OpenTelemetry