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

October 20, 2026 · Part 13 of 35

![Multi round-trip requests: how a stateless MCP server asks for more](https://pournasserian.com/_astro/lead.Kg4bJ4SH_1uAnOQ.webp)

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.

```mermaid
sequenceDiagram
    actor U as User
    participant C as Client
    participant S as Server (any instance)

    C->>S: tools/call deploy (id 1)
    Note over S: Needs confirmation.<br/>Seals context into requestState.
    S-->>C: resultType input_required<br/>inputRequests {confirm: elicitation/create}<br/>requestState "AEAD blob"
    Note over C,S: Request 1 is finished
    C->>U: "Deploy api to prod?"
    U-->>C: Approve
    C->>S: tools/call deploy (id 2)<br/>+ inputResponses {confirm: accept}<br/>+ requestState "AEAD blob"
    Note over S: Verifies and opens state,<br/>completes execution
    S-->>C: resultType complete + CallToolResult
```

### On the wire

Round 1 is an ordinary `tools/call` in JSON-RPC 2.0:

```json
{ "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:

```json
{ "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:

```json
{ "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)
```

```text
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.

I'm [Amir Pournasserian](https://pournasserian.com/about). I build AI and platform systems for a living, maintain [FluentCMS](https://pournasserian.com/open-source/fluentcms), [YeSvelte](https://pournasserian.com/open-source/yesvelte) and [mcp-my-site](https://pournasserian.com/open-source/mcp-my-site), and write here about what I find along the way.
