Multi round-trip requests: how a stateless MCP server asks for more

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 closed the parts on what a server offers; this one takes apart Multi Round-Trip Requests (MRTR), which part 2 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.

Client → Server (any instance): tools/call deploy (id 1); Note (Server (any instance)): Needs confirmation. Seals context into requestState.; Server (any instance) → Client: resultType input_required inputRequests {confirm: elicitation/create} requestState "AEAD blob"; Note (Client, Server (any instance)): Request 1 is finished; Client → User: "Deploy api to prod?"; User → Client: Approve; Client → Server (any instance): tools/call deploy (id 2) + inputResponses {confirm: accept} + requestState "AEAD blob"; Note (Server (any instance)): Verifies and opens state, completes execution; Server (any instance) → Client: resultType complete + CallToolResultServer (any instance)ClientNeeds confirmation.Seals context into requestState.Request 1 is finishedVerifies and opens state,completes executionUsertools/call deploy (id 1)resultType input_requiredinputRequests {confirm: elicitation/create}requestState "AEAD blob""Deploy api to prod?"Approvetools/call deploy (id 2)+ inputResponses {confirm: accept}+ requestState "AEAD blob"resultType complete + CallToolResult

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.

The building blocks

The specification’s MRTR page defines three fields:

FieldWho sends itRules
inputRequestsServerA 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.
inputResponsesClient, on retryA map of the same keys to results (ElicitResult, CreateMessageResult, ListRootsResult)
requestStateServer, echoed by clientAn 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

MethodMay return input_required?
tools/callYes
resources/readYes
prompts/getYes
Everything elseNo: servers MUST NOT

So a resource read or a prompt 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 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, 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:

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:

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.