
A deployment tool asked the engineer to confirm before it rolled production. It worked for three weeks on one pod. The week the server scaled to three pods, confirmations started vanishing: the tool sent elicitation/create, the user clicked approve, and the approval arrived at a pod that had never heard of the request. No error. The tool just never finished.
The fix was not a sticky session. The fix was the Model Context Protocol (MCP) changing underneath it. The 2026-07-28 revision inverted elicitation: a server no longer asks the client for input, it returns a result that says input is required, and the client comes back with the answer in a brand-new request that can land anywhere. That inversion is what Multi Round-Trip Requests (MRTR) are, and it is why every elicitation tutorial written before July 2026 describes a wire format that no longer exists.
This post covers what elicitation is for, both modes, how it worked under 2025-11-25, how it works now, what to store between rounds and what never to store, and where the user's third-party credentials come from when a tool needs them mid-call.
Most tools are complete on input: the model fills the arguments, the server executes. Some are not. A tool that files an expense needs the cost center the model does not know. A tool that deletes a project should require a human to type the project name. A tool that reads the user's calendar needs the user to have connected a calendar.
Elicitation is the protocol's answer: the server, mid-request, asks the client to put something in front of the user and send back what the user did. The response has three actions, accept, decline and cancel, and only accept carries content.
Two boundaries matter from the start. The spec is explicit that elicitation is not for the MCP client's own authorization to the MCP server; that is handled by the OAuth flow before any tool is called. And a form-mode accept is not authorization for a destructive action; it is a data entry event. The distinction between "the user typed yes" and "the user authorized this specific call" is the one the human-in-the-loop approval gates post is built on, and elicitation lives on the data entry side of it.
The line is about where bytes travel. In form mode, whatever the user types becomes part of the elicitation result, which the server sees, which the model may see, which ends up in logs. That is fine for a cost center. It is disqualifying for a GitHub token. The spec does not leave this to judgment: servers MUST NOT use form mode for sensitive information and MUST use URL mode for it.
URL mode is also the only mode that supports a third-party OAuth flow, because the consent screen for Google or Slack has to happen in a browser the MCP client does not own.
A client declaring the elicitation capability must support at least one mode. A server should check which before it asks; a URL-mode request to a form-only client fails, and the correct degradation is to fail the tool with a clear message, never to downgrade the credential request to a form.
Under the legacy revisions, elicitation was a server-initiated request. The server, inside a tool handler, sent its own JSON-RPC request back down the open connection:
The client rendered the form, the user answered, and the client replied to srv-17 on the same connection. The handler, still blocked in the same process, read the answer and continued.
URL mode added an elicitationId, and the server was expected to send notifications/elicitation/complete once the out-of-band interaction finished, so the client could stop waiting. A new error code, -32042, let a tool fail with "URL elicitation required" instead of asking inline.
Every part of this assumed a connection that stayed open and a process that stayed alive. That assumption is what broke across three pods.
MRTR (SEP-2322) removes server-initiated requests entirely. The server cannot push elicitation/create, sampling/createMessage or roots/list. It can only answer.
The tool handler runs, hits the point where it needs the user, and returns a result instead of a request:
Three fields carry the pattern. resultType is now required on every result and is either "complete" or "input_required". inputRequests is a map from a server-chosen key to a bare {method, params} request; a single result can carry several. requestState is an opaque string the client must echo back unchanged.
The connection then closes. Nothing is held open. The pod that answered can be gone by the time the user clicks.
The client renders each input request, collects the results, and sends the original method again with a new JSON-RPC id:
The handler runs again from the top, with the same arguments, this time finding inputResponses populated. It reads the answer and either completes or asks for the next thing.
InputRequiredResult is valid only on tools/call, resources/read and prompts/get. A client talking to an older server must treat a result with no resultType as "complete".
notifications/elicitation/complete and elicitationId are gone. Not deprecated; removed, and they had existed for one revision. The completion signal is the retry. Error code -32042 is retired; a modern server must not emit it.
The same tool, written twice, so the inversion is visible rather than described.
The await ctx.elicit(...) line is where the process blocks on an open connection. If that connection belongs to a different pod on the way back, the await never resolves.
Nothing blocks. The handler either has the answer or it does not, and it says so by returning. The SDK turns the raised error into the input_required result and parses inputResponses on the retry.
Two rules fall out of this shape:
Everything before the input point reruns. The db.get_project read happens on both rounds. Any write placed before the input check would happen twice. Reads before, writes after.
requestState is for correlation, not secrets. It travels to the client and back. Put a step marker and an identifier in it. Never put a token, a credential, or anything you would not log. If you need more state than fits in a string, mint a handle, store the state server-side keyed by that handle, and put the handle in requestState; that is also the spec's guidance for cross-call state now that sessions are gone.
requestState is also tamperable. The client echoes it, and the client is not trusted. If the handler makes any decision based on its contents (which step it is on, which project was confirmed), a client can edit it between rounds. Sign it, and verify the signature before reading it:
Include the user identity from the inbound token inside the signed payload and compare it on the retry. A requestState minted for one user must not be replayable by another.
A handler can ask more than once. Each round is a complete request-response pair; the client's job is to keep retrying until it sees "complete", and the server's job is to make each round pure given its inputs.
Client SDKs cap the number of rounds; the C# SDK and several community implementations default to around ten. A chain that needs more than a handful is usually a sign the tool should be split. Each round adds a human pause, and the agent framework above the client has to surface each one as an interrupt.
Declines and cancels end the chain. A decline means the user saw the request and said no; a cancel means they dismissed it. Both should produce a completed result with a clear message, not another input_required.
Half the readers of this post are not writing the server. They are writing the agent runtime that calls it, which means they are the MCP client, and the retry loop is their code.
The official 2.x clients handle the mechanics: they parse input_required, dispatch each entry in inputRequests to a callback you register, collect the results, and resend. What you own is the callback, and the callback is where the human is. In an agent runtime, that means the callback cannot block a worker thread for four hours waiting for someone to click; it has to hand the request to whatever your platform already uses to pause and resume a run.
Three rules for the callback, learned the hard way by every team that has shipped one:
Declare only what you can render. If your runtime cannot show a form, do not advertise form. If it cannot open a browser for the user, do not advertise url. A capability you declare and cannot honor produces a tool that hangs rather than a tool that fails cleanly.
Bound the wait. A server-side input_required has no TTL in the protocol. Your runtime decides how long a pending request lives. Return cancel on timeout so the server completes with a message, rather than leaving the tool call open forever in your checkpoint store.
Use the framework's interrupt, not a sleep. LangGraph's interrupt(), the OpenAI Agents SDK's RunState serialization, and the equivalent in whatever you run on are built for exactly this pause. The approval-gates post covers the pause-and-resume architecture; an elicitation callback is one more thing that goes through it.
If the server you are calling is a Virtual MCP server in META mode, you will not see input_required for the connect-account case at all. The execute_tool meta tool returns not_connected with a connection_name, the agent calls connect_account and gets a link back as an ordinary tool result, and your runtime surfaces that link the same way it would surface any other tool output. The pause is still yours to build; the protocol round-trip is not.
Support is uneven, and a server that assumes URL mode or MRTR without checking will fail for real users.
The practical rule: read io.modelcontextprotocol/clientCapabilities from the request's _meta before asking. A bare elicitation: {} means form only. No elicitation key means the client cannot answer at all, and the tool should fail with a message the model can relay rather than return input_required into the void.
Form mode is a data channel, not a consent channel. Validate the content against your own schema on the server, not only the client's rendering of it. Treat an accepted form as untrusted input from the user, because that is what it is. Do not treat a form-mode accept as sufficient authorization for a destructive or irreversible action; the spec community has pushed for exactly that clarification, and the queue-then-execute approval pattern exists because scope and consent are different questions.
URL mode moves the sensitive step out of the protocol. The client shows the target host and asks the user before navigating; the spec requires HTTPS; and the only thing that returns through the protocol is whether the user agreed to open the link. What happened at the link is between the user and whoever served it. The server finds out by checking state on the retry, not by trusting the elicitation result.
Both modes are events. Log every input_required with its keys, every inputResponses action, and the user identity from the inbound token. An auditor asking "who confirmed the deletion of billing-legacy" needs that row. The audit trail post covers the four event categories; elicitation rounds belong in the authorization category when they gate a write.
The most common reason a tool cannot finish is not a missing form field. It is that the user never connected the account the tool needs. A summarize_inbox tool reaches the point where it has to read Gmail, and this user has no Gmail grant.
This is the case URL mode was designed for, and the shape is precise:
Three things in that handler are doing the real work.
The status check runs on every round. The handler does not trust inputResponses["connect_gmail"].action == "accept" as proof the grant exists; URL-mode accept means the user agreed to open the page, nothing more. The connected account's status (PENDING, ACTIVE, EXPIRED, REVOKED, ERROR) is the source of truth, and it is checked after the user comes back.
The guard against looping matters. If the user opened the link and closed it, the retry arrives with an accept and a still-inactive account. Returning a clear completed result is correct; raising another input_required with the same link traps the client in a cycle.
The credential never enters the handler. get_authorization_link returns a URL; execute_tool resolves the user's token from the vault at call time and makes the Gmail request. The handler's memory, the server's logs and the model's context contain user_id and a result, not a token. That is the same property URL mode exists to protect, carried through to the downstream call.
The removed completion notification has a replacement outside the protocol, too. Account events deliver a connected account's lifecycle transitions as they happen, so an agent runtime that wants to resume automatically when the grant lands can subscribe to the ACTIVE transition rather than waiting for the human to click retry. The retry is still what drives the protocol; the event is what tells your runtime it is worth sending.
When the agent reaches connector tools through a Virtual MCP server in META exposure mode, this loop is already built into the five tools the agent sees: execute_tool returns not_connected or reauth_needed naming the connection, the agent calls connect_account, surfaces the link, and retries after the user finishes. The readiness states ready, needs_reauth and needs_connection are the same status model, exposed to the agent instead of to your handler. The handler above is what that looks like when the server is yours.
Recommended Reading: Managing auth state across tool calls
Elicitation did not get smaller in the rewrite. It got honest about what a server can assume: nothing beyond the request in front of it. A tool that needs the user asks by returning, keeps only a correlation handle between rounds, reruns cleanly, and checks real state when the user comes back rather than trusting that they did what the message asked.
The credential case is the one that makes the discipline worth it. The user's Gmail grant is the thing the whole protocol was redesigned to keep out of band, and the right pattern keeps it out of the handler as well: a link out, a status check back, a vault in between.
Grep your servers for ctx.elicit and for notifications/elicitation/complete; every hit is a handler that stopped working on 28 July. Then look at how users authorize a connection to see what the URL in a URL-mode request should point at.
A way for a server, mid-request, to ask the client to put something in front of the human and return what they did. It is for data the tool needs to finish, not for authorizing the client or approving a destructive action.
Form mode carries the answer through the client and into the model's context, so it is forbidden for passwords, keys and tokens. URL mode sends the user out of band and returns only whether they agreed to open the link.
The 2026-07-28 replacement for server-initiated requests. The server answers the original call with a result that says input is required, the client gathers answers and retries the same method, and the handler runs again from the top.
Because it reruns on every round and the retry can land on a different instance. Reads before the input check happen each time, so writes belong after it, and nothing secret belongs in the echoed request state.
The official Python, TypeScript and C# 2.x clients, MCP Inspector 2.x and Claude's clients do. Several SDKs declare form mode only, and some agent runtimes support neither, so read the client's capabilities before asking.
It checks the connected account, returns a URL-mode request pointing at that user's authorization link, and on retry checks the account status again rather than trusting the accept. The credential lands in the vault, never in the handler.