{"slug": "multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more", "title": "Multi round-trip requests: how a stateless MCP server asks for more", "summary": "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.", "body_md": "# Multi round-trip requests: how a stateless MCP server asks for more\n\nSometimes 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.\n\nThis 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.\n\n## In brief\n\n1. **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.\n2. **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.\n3. **Only `tools/call`, `resources/read` and `prompts/get` may return `input_required`.** Every other method MUST NOT.\n4. **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.\n5. **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.\n\n## The idea in four steps\n\n1. The client sends a normal request.\n2. The server decides it needs more and returns an `InputRequiredResult` instead of a final result. This ends the request.\n3. The client gathers the input, from the user, the model or local context.\n4. The client retries the original request with the answers attached. The server either finishes or asks again.\n\nThe 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.\n\n### On the wire\n\nRound 1 is an ordinary `tools/call` in JSON-RPC 2.0:\n\n```\n{ \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\",\n  \"params\": { \"name\": \"deploy\", \"arguments\": { \"service\": \"api\", \"env\": \"prod\" } } }\n```\n\nThe server needs a confirmation, so it answers with an elicitation under a key it chose (`confirm`) and a `requestState` for the client to echo:\n\n```\n{ \"jsonrpc\": \"2.0\", \"id\": 1, \"result\": {\n    \"resultType\": \"input_required\",\n    \"inputRequests\": {\n      \"confirm\": { \"method\": \"elicitation/create\", \"params\": {\n        \"mode\": \"form\", \"message\": \"Deploy api to prod?\",\n        \"requestedSchema\": { \"type\": \"object\",\n          \"properties\": { \"approve\": { \"type\": \"boolean\" } }, \"required\": [\"approve\"] } } }\n    },\n    \"requestState\": \"eyJzdGVwIjoiY29uZmlybSJ9.sig\" } }\n```\n\nThe retry has a new ID, the same method and arguments, and the answers under the same key:\n\n```\n{ \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n  \"params\": { \"name\": \"deploy\", \"arguments\": { \"service\": \"api\", \"env\": \"prod\" },\n    \"inputResponses\": { \"confirm\": { \"action\": \"accept\", \"content\": { \"approve\": true } } },\n    \"requestState\": \"eyJzdGVwIjoiY29uZmlybSJ9.sig\" } }\n```\n\nThe 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).\n\n## The building blocks\n\nThe specification’s MRTR page defines three fields:\n\n| Field | Who sends it | Rules | \n|---|---|---|\n| `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. | \n| `inputResponses` | Client, on retry | A map of the same keys to results ( `ElicitResult` ,`CreateMessageResult` ,`ListRootsResult` ) | \n| `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. | \n\nEvery `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.\n\n### Where MRTR is allowed\n\n| Method | May return `input_required` ? | \n|---|---|\n| `tools/call` | Yes | \n| `resources/read` | Yes | \n| `prompts/get` | Yes | \n| Everything else | No: servers MUST NOT | \n\nSo 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.\n\n## Rules for servers and clients\n\n### Server rules\n\n1. **Ask only for what the request declared.** No`elicitation/create` unless the request’s capabilities include`elicitation` .\n2. **Never assume the client will answer or retry.** It may abandon the request at any point.\n3. **Asking more than once is fine.** You can return`input_required` repeatedly until you have what you need.\n4. **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.\n\nIf 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.\n\n### Client rules\n\n1. **Answer everything asked.** If`inputRequests` is present, construct every requested input before retrying.\n2. **Use a new ID.** The retry uses a new JSON-RPC`id` .\n3. **Keep the answers with their request.**`inputResponses` and`requestState` belong only to the retry of that request, never to parallel requests.\n\n### MRTR and Tasks\n\nThe 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).\n\n## The deploy confirmation in Python\n\nWith 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:\n\n``` python\nfrom typing import Annotated\n\nfrom pydantic import BaseModel\n\nfrom mcp.server import MCPServer\nfrom mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve\n\nmcp = MCPServer(\"deploy-server\")\n\nclass Approve(BaseModel):\n    approve: bool\n\nasync def ask(service: str, env: str) -> Elicit[Approve]:\n    # Runs on every round, so any code here repeats.\n    return Elicit(f\"Deploy {service} to {env}?\", Approve)\n\n@mcp.tool()\nasync def deploy(\n    service: str,\n    env: str,\n    confirm: Annotated[ElicitationResult[Approve], Resolve(ask)],\n) -> str:\n    \"\"\"Deploy a service to an environment, once the user confirms.\"\"\"\n    if isinstance(confirm, AcceptedElicitation) and confirm.data.approve:\n        return f\"Deployed {service} to {env}.\"  # stand-in for the real deployment\n    return \"Deployment cancelled.\"\n```\n\n`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.\n\nIf the request didn’t declare elicitation, the SDK refuses with -32021, naming form elicitation in `requiredCapabilities`.\n\nTo 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:\n\n``` python\nimport anyio\nfrom mcp import Client\nfrom mcp.types import ElicitResult\n\nARGS = {\"service\": \"api\", \"env\": \"prod\"}\n\nasync def answer(context, params) -> ElicitResult:\n    print(\"Asked:\", params.message)\n    return ElicitResult(action=\"accept\", content={\"approve\": True})\n\nasync def main() -> None:\n    async with Client(mcp, elicitation_callback=answer) as client:\n        result = await client.call_tool(\"deploy\", ARGS)\n        print(result.content[0].text)\n\nanyio.run(main)\nAsked: Deploy api to prod?\nDeployed api to prod.\n```\n\nDriven 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.\n\n## Method and caveats\n\n- 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.\n- 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.\n- 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.\n- Protecting `requestState` , multi-step wizards and URL-mode elicitation follow in parts 14 and 15.", "url": "https://wpnews.pro/news/multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more", "canonical_source": "https://pournasserian.com/writing/mcp-2026-multi-round-trip-requests", "published_at": "2026-10-20 00:00:00+00:00", "updated_at": "2026-10-20 23:48:59.960035+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Model Context Protocol", "Multi Round-Trip Requests", "Python SDK", "MCPServer", "Client"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more", "markdown": "https://wpnews.pro/news/multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more.md", "text": "https://wpnews.pro/news/multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more.txt", "jsonld": "https://wpnews.pro/news/multi-round-trip-requests-how-a-stateless-mcp-server-asks-for-more.jsonld"}}