Multi round-trip requests: how a stateless MCP server asks for more The 2026-07-28 revision of the Model Context Protocol (MCP) specification replaces server-initiated requests with Multi Round-Trip Requests (MRTR), requiring servers to end a request with resultType "input_required" instead of sending elicitation, sampling or roots requests back over an open connection. Under the new pattern, servers MUST use MRTR for elicitation, sampling and roots, and the old server-initiated style is no longer supported — a breaking change — with only tools/call, resources/read and prompts/get permitted to return input_required and undeclared capabilities answered with error -32021. In the Python SDK, a resolver asks the question and the Client runs the retry loop, while MCPServer seals requestState with a key of its own process unless every instance is given the same key. Multi round-trip requests: how a stateless MCP server asks for more Sometimes a server can’t finish a request without more information: a confirmation, a missing parameter, a sign-in. Before the 2026-07-28 revision, a Model Context Protocol MCP server sent its own request back to the client elicitation/create , sampling/createMessage or roots/list and waited on the open connection. That needed a live channel in both directions, and with it sticky routing or shared state. What I like about its replacement is how plain it is: the server doesn’t wait, it answers, and the client asks again. This is part 13 of my series on what an MCP server does under the 2026-07-28 specification. Part 12 https://pournasserian.com/writing/mcp-2026-choosing-a-primitive closed the parts on what a server offers; this one takes apart Multi Round-Trip Requests MRTR https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr , which part 2 https://pournasserian.com/writing/mcp-2026-what-changed introduced. The facts are as I read them in October 2026. In brief 1. MRTR replaces every request a server used to send to the client. Servers MUST use it for elicitation, sampling and roots, and the old server-initiated style is no longer supported: a breaking change. 2. A server that needs more ends the request with resultType: "input required" . The client then retries the original request with a new ID, the answers and the requestState it was given. 3. Only tools/call , resources/read and prompts/get may return input required . Every other method MUST NOT. 4. A server asks only for what the request declared it can handle. If the operation can’t go on without an undeclared capability, the server MUST answer with error -32021. 5. In the Python software development kit SDK , a resolver asks the question and the Client runs the retry loop. MCPServer seals requestState for you, with a key of its own process unless you give every instance the same key. The idea in four steps 1. The client sends a normal request. 2. The server decides it needs more and returns an InputRequiredResult instead of a final result. This ends the request. 3. The client gathers the input, from the user, the model or local context. 4. The client retries the original request with the answers attached. The server either finishes or asks again. The two requests are fully independent. Whichever server instance handles the retry needs nothing beyond what’s in the retry itself. The server keeps what it needs to remember in requestState . In the diagram it’s sealed with Authenticated Encryption with Associated Data AEAD ; protecting that state is the subject of part 14. On the wire Round 1 is an ordinary tools/call in JSON-RPC 2.0: { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "deploy", "arguments": { "service": "api", "env": "prod" } } } The server needs a confirmation, so it answers with an elicitation under a key it chose confirm and a requestState for the client to echo: { "jsonrpc": "2.0", "id": 1, "result": { "resultType": "input required", "inputRequests": { "confirm": { "method": "elicitation/create", "params": { "mode": "form", "message": "Deploy api to prod?", "requestedSchema": { "type": "object", "properties": { "approve": { "type": "boolean" } }, "required": "approve" } } } }, "requestState": "eyJzdGVwIjoiY29uZmlybSJ9.sig" } } The retry has a new ID, the same method and arguments, and the answers under the same key: { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "deploy", "arguments": { "service": "api", "env": "prod" }, "inputResponses": { "confirm": { "action": "accept", "content": { "approve": true } } }, "requestState": "eyJzdGVwIjoiY29uZmlybSJ9.sig" } } The server finishes with "resultType": "complete" and the normal tool result. That’s the input required branch of part 5’s client loop https://pournasserian.com/writing/mcp-2026-stateless-requests . The building blocks The specification’s MRTR page defines three fields: | Field | Who sends it | Rules | |---|---|---| | inputRequests | Server | A map of server-chosen keys unique within the request to request objects. Values MUST be ElicitRequest , CreateMessageRequest or ListRootsRequest . Several can be asked at once. | | inputResponses | Client, on retry | A map of the same keys to results ElicitResult , CreateMessageResult , ListRootsResult | | requestState | Server, echoed by client | An opaque string meaningful only to the server. Clients MUST NOT inspect, parse or modify it, MUST echo it exactly, and MUST NOT add one if none was given. | Every InputRequiredResult MUST include at least one of inputRequests or requestState . A result with only requestState means “retry immediately”: the server keeps its progress in the state and picks up from there, as part 14’s wizard does. Where MRTR is allowed | Method | May return input required ? | |---|---| | tools/call | Yes | | resources/read | Yes | | prompts/get | Yes | | Everything else | No: servers MUST NOT | So a resource read https://pournasserian.com/writing/mcp-2026-resources or a prompt https://pournasserian.com/writing/mcp-2026-prompts can ask for more too. Rules for servers and clients Server rules 1. Ask only for what the request declared. No elicitation/create unless the request’s capabilities include elicitation . 2. Never assume the client will answer or retry. It may abandon the request at any point. 3. Asking more than once is fine. You can return input required repeatedly until you have what you need. 4. If a retry is missing a required answer, ask again with a new InputRequiredResult rather than an error. Ignore unrecognized fields in inputResponses . Malformed data still gets a JSON-RPC error. If the operation can’t proceed without a capability the request didn’t declare, the specification’s overview https://modelcontextprotocol.io/specification/2026-07-28/basic/index says the server MUST return a MissingRequiredClientCapabilityError -32021 whose data.requiredCapabilities lists what’s missing; on HTTP, the status MUST be 400 Bad Request. My advice: a tool error isError fits only when the tool can still do something useful without it, such as telling the model which argument to supply. Client rules 1. Answer everything asked. If inputRequests is present, construct every requested input before retrying. 2. Use a new ID. The retry uses a new JSON-RPC id . 3. Keep the answers with their request. inputResponses and requestState belong only to the retry of that request, never to parallel requests. MRTR and Tasks The Tasks extension uses the same inputRequests and inputResponses shapes, but surfaces its requests through tasks/get and takes the answers through tasks/update . MRTR is for input before you commit to work and task input is for input during it, so servers SHOULD resolve MRTR rounds synchronously before returning a task a later part covers Tasks . The deploy confirmation in Python With the official Python SDK https://py.sdk.modelcontextprotocol.io/handlers/multi-round-trip/ , mcp 2.3.0, you don’t build the InputRequiredResult yourself: a resolver returns the question, and the SDK fills a tool parameter with the answer. Here is the deploy confirmation from the diagram, in Python: python from typing import Annotated from pydantic import BaseModel from mcp.server import MCPServer from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve mcp = MCPServer "deploy-server" class Approve BaseModel : approve: bool async def ask service: str, env: str - Elicit Approve : Runs on every round, so any code here repeats. return Elicit f"Deploy {service} to {env}?", Approve @mcp.tool async def deploy service: str, env: str, confirm: Annotated ElicitationResult Approve , Resolve ask , - str: """Deploy a service to an environment, once the user confirms.""" if isinstance confirm, AcceptedElicitation and confirm.data.approve: return f"Deployed {service} to {env}." stand-in for the real deployment return "Deployment cancelled." ask takes the tool’s arguments by name and returns Elicit : the message, and the Pydantic model the answer must fit. Since confirm is annotated ElicitationResult Approve , it carries the whole outcome accepted, declined or cancelled . The tool body runs only once the answer is in. The same code serves clients on 2025-11-25 and earlier, where the SDK sends the question as its own request mid-call. If the request didn’t declare elicitation, the SDK refuses with -32021, naming form elicitation in requiredCapabilities . To try it, put an in-memory client in the same file. Given an elicitation callback , the Client declares the capability and runs the retry loop for you: python import anyio from mcp import Client from mcp.types import ElicitResult ARGS = {"service": "api", "env": "prod"} async def answer context, params - ElicitResult: print "Asked:", params.message return ElicitResult action="accept", content={"approve": True} async def main - None: async with Client mcp, elicitation callback=answer as client: result = await client.call tool "deploy", ARGS print result.content 0 .text anyio.run main Asked: Deploy api to prod? Deployed api to prod. Driven by hand with client.session.call tool ..., allow input required=True , the first round returns the elicitation under the key main :ask the resolver’s module and name and a sealed requestState . By default MCPServer seals it with a key held only by the running process, so a second instance can’t open it without shared keys; part 14 covers that. Method and caveats - Built from chapter 13 of my guide to MCP servers and written against the 2026-07-28 specification. Product facts are as of October 2026. - The Python sample ran against mcp 2.3.0 on 9 October 2026 with the SDK’s in-memory client and printed the output shown. My guide’s sketch was SDK-agnostic C with a hand-written codec for requestState , which the SDK replaces. - Where the specification is more precise than my notes what a server answers when an operation needs a capability the request didn’t declare , this article follows the specification’s overview. - Protecting requestState , multi-step wizards and URL-mode elicitation follow in parts 14 and 15.