Tutorial
How to get user input from an MCP tool call (2026-07-28 stateless elicitation)
How a tool asks the user for input in the 2026-07-28 MCP spec: return an InputRequiredResult from tools/call, then retry with inputResponses and an echoed requestState. No stream, no sessions.
MCPOrbit Team
Engineering, MCPOrbit
- Published
- Updated
- · Updated
- Read time
- · 9 min read

In the 2026-07-28 MCP spec, a tool asks the user for input by returning from tools/call instead of pushing a message. The server sends back an InputRequiredResult: a normal result that contains an inputRequests object (the questions to ask) and an opaque, base64-encoded requestState. The client shows the prompts, collects answers, and re-issues the same tools/call with an inputResponses object and the requestState echoed back unchanged. There is no SSE stream and no server-held session: because every bit of in-flight state travels inside requestState, any server instance can finish the call. That is the whole pattern. The rest of this post is the wire trace and how to migrate to it.
The mechanism you knew is gone: elicitation no longer rides a stream
Before 2026-07-28, a tool that needed input mid-call relied on the server pushing an elicitation/create request to the client over an open connection, then waiting on that same session for the answer. Two changes in the new spec remove the ground that stood on:
- Sessions are removed (SEP-2567) and the initialize handshake is removed (SEP-2575). There is no long-lived, server-owned connection state to pin a pending elicitation to.
- Server-initiated requests are constrained (SEP-2260): a server may issue a request to the client only while it is actively processing a client request. A server can no longer wake up later and push a question across an idle stream.
The practical consequence: mid-tool-call input can no longer be a side-channel message. It has to be part of the request and response cycle itself.
The new shape: InputRequiredResult, inputRequests, requestState
The replacement is the multi round-trip request (SEP-2322). When a tools/call cannot finish without more input, the server does not error and does not stream. It returns a result whose resultType is input_required. That result carries two things:
- `inputRequests`: the elicitation prompts, meaning the questions, their schemas, and anything the client needs to render a form.
- `requestState`: an opaque, base64-encoded blob that captures everything the server needs to resume this exact call. The client treats it as a token. Store nothing, understand nothing, just echo it back.
The client renders the prompts, gathers the user's answers, and retries the original tools/call (same tool, same arguments), adding an inputResponses object and the requestState it was handed. The server reads requestState, applies the responses, and continues. If it needs another round, it returns another InputRequiredResult. If it is done, it returns the normal tool result.
The full round trip, on the wire
1. Client calls the tool
A deploy tool that needs a confirmation before it proceeds:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "deploy_release", "arguments": { "service": "api", "version": "4.8.1" } }
}2. Server needs input, returns (not pushes) an InputRequiredResult
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm_prod": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Deploy api 4.8.1 to production?",
"requestedSchema": {
"type": "object",
"properties": {
"confirm": { "type": "boolean", "title": "Confirm production deploy" }
},
"required": ["confirm"]
}
}
}
},
"requestState": "eyJjYWxsIjoiZGVwbG95X3JlbGVhc2UiLCJzdGVwIjoxfQ=="
}
}3. Client answers by re-issuing the same call, echoing requestState
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "deploy_release",
"arguments": { "service": "api", "version": "4.8.1" },
"inputResponses": { "confirm_prod": { "action": "accept", "content": { "confirm": true } } },
"requestState": "eyJjYWxsIjoiZGVwbG95X3JlbGVhc2UiLCJzdGVwIjoxfQ=="
}
}4. Server resumes from requestState and returns the final result
{
"jsonrpc": "2.0",
"id": 2,
"result": { "resultType": "complete", "content": [ { "type": "text", "text": "Deployed api 4.8.1 to production." } ] }
}The second request could land on a completely different server process than the first. Nothing breaks, because the server put everything it needs to resume into requestState and the client handed it straight back.
Why the state lives in the payload, not the server
This is the stateless core doing exactly what it was designed for. In the old model, the server held the pending elicitation in memory tied to a session, so the answer had to come back to the same process over the same connection, which means sticky sessions, a shared session store, or both. In the new model the resume state is in the message, so:
- Any instance can finish the call. Put your servers behind a plain round-robin load balancer; the retry does not need affinity.
- Restarts do not strand in-flight prompts. There is no server memory to lose, because the client is holding the `requestState`.
- The state is inspectable, not hidden. The spec's phrasing for the broader pattern is that state becomes visible to the model rather than hidden away.
The cost you pay for this is that requestState crosses a trust boundary: it goes to the client and comes back. Which leads to the one rule that matters most.
Migrating from the old elicitation flow: the checklist
- Stop pushing `elicitation/create` over the stream. Return an `InputRequiredResult` from `tools/call` instead. Input is now part of the call's result, not a side-channel message.
- Move per-call state out of server memory and into `requestState`. Anything you used to keep on the session for a pending prompt now serializes into the blob.
- Sign or encrypt `requestState`, and verify it on the way back. It round-trips through the client, so treat it as untrusted input: authenticate it (for example an HMAC or a sealed token) and reject tampering. Never put raw secrets in it in the clear.
- Do not assume affinity. The response may hit a different instance, so avoid in-memory lookups keyed by session.
- Respect SEP-2260. Any genuinely server-initiated interaction must happen while you are processing a client request; you cannot defer a question to later on an idle connection.
- Handle multiple rounds. If one answer is not enough, return another `InputRequiredResult`. Each round is a fresh return plus echo; there is no open dialogue to keep alive.
Multi-step prompts and validation
Because each round is an independent return-and-retry, multi-question flows compose naturally: ask for a value, validate it when the retry comes back, and if it is invalid, return another InputRequiredResult with the same question and an error hint. The server stays stateless the whole time, because the growing context lives in requestState, which you can extend on each round. This also makes validation server-side and authoritative: the client cannot skip a required prompt, because without the matching inputResponses the server just returns the input-required result again.
FAQ: getting user input from an MCP tool in the 2026-07-28 spec
- How does a tool ask the user a question in the 2026-07-28 MCP spec?
- It returns an
InputRequiredResultfromtools/call: a result containing aninputRequestsobject (the prompts) and an opaquerequestState. The client collects answers and re-calls the same tool withinputResponsesand the echoedrequestState. - What replaced the elicitation SSE stream?
- The multi round-trip request pattern (SEP-2322). Sessions and the always-open server-to-client channel were removed (SEP-2567, SEP-2575), so input is now a return value the client answers with a retry, not a message pushed over a stream.
- What is requestState and do I store it server-side?
- It is a base64-encoded, opaque payload that captures everything needed to resume the call. You do not store it server-side. The client holds it and echoes it back. Because it round-trips through the client, sign or encrypt it and verify it on return.
- Can any server instance handle the response?
- Yes, that is the entire point. All resume state lives in
requestState, so the retry can land on any instance behind a round-robin load balancer with no session affinity. - Is the old elicitation deprecated or removed?
- The session and handshake it depended on are removed (SEP-2567, SEP-2575), and server-initiated requests are constrained to active request processing (SEP-2260). The supported way to ask for input is now
InputRequiredResult. - Does this apply to stdio (local) servers too?
- The pattern is part of the core protocol, so use
InputRequiredResultregardless of transport. It matters most for remote HTTP servers behind a load balancer, but adopting it everywhere keeps one code path.
About the author
MCPOrbit Team
Engineering, MCPOrbit
The MCPOrbit engineering team builds tooling for running Model Context Protocol servers in production, and monitors a fleet of public MCP endpoints in the wild.


