Development Choices

Implement Multi Round-Trip Requests in MCP

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
10 min read3 sources cited

Return `input_required` from the original MCP operation, let the client satisfy the embedded elicitation, sampling, or roots requests, then retry that operation with `inputResponses` and unchanged opaque `requestState`. Validate capability support, authorization, integrity, expiry, origin, replay, round limits, and response shape before resuming.

Prerequisites

Implement both sides against MCP revision 2026-07-28. Every request must carry the required protocol version, client identity, and client capabilities in _meta; there is no initialization exchange from which the server can recover them later. If the implementation still depends on the previous bidirectional flow or hidden transport-session state, handle that work as part of the migration from MCP 2025-11-25 to 2026-07-28.

Decide which client features the workflow can actually satisfy. Elicitation needs client UI, sampling needs access to a model, and roots needs a current set of filesystem boundaries. A client declares those capabilities on each request. A server must not embed an input request for a capability or elicitation mode the client did not declare.

Input becomes an in-band result

An MCP request returns input required, the client gathers input, retries with responses, and receives a final result
MRTR preserves interaction without a held-open server-to-client request channel.
  1. Replace server-initiated calls with a returned result

    Remove code that sends elicitation/create, sampling/createMessage, or roots/list as a new server-to-client JSON-RPC request while holding the original operation open. In revision 2026-07-28, the release description says MRTR replaces those server-initiated requests because the protocol core now uses independent request-response exchanges rather than a constantly open bidirectional stream.

    The server instead returns an InputRequiredResult while handling the original prompts/get, resources/read, or tools/call operation. Those are the three operations on which the revision permits this result; returning it from another operation is invalid. The result has resultType: "input_required" and at least one of inputRequests or requestState.

    This is a breaking wire change, not a new name for the old callback. Roots and sampling remain features in this revision, although both are deprecated; their former server-initiated delivery pattern is no longer supported. Existing sampling code therefore needs the MRTR envelope described here, while a new design should first check whether it needs server-initiated sampling at all. The same distinction applies to roots used as client-declared filesystem boundaries: MRTR changes how a server asks for the roots, not what the returned roots mean.

    The cost is one additional client-server exchange for every input_required round, plus whatever user interaction or model call satisfies the embedded request. That cost is justified when the original operation cannot finish without information available only to the client. MRTR is the wrong answer when the server already has the data, when the missing value can be an ordinary argument supplied with the first request, or when the work is independent enough to be a separate tool call rather than a continuation.

  2. Build only the input requests needed for the current decision

    Put each required client action in the inputRequests map. Assign every entry a server-chosen string key that is unique within this result. Its value is a complete elicitation/create, sampling/createMessage, or roots/list request. The client will use the same key when it constructs inputResponses, so choose stable identifiers for the round rather than depending on array position.

    A server asking for a deployment approval and current roots might return this shape:

    {
      "jsonrpc": "2.0",
      "id": 41,
      "result": {
        "resultType": "input_required",
        "inputRequests": {
          "deployment_approval": {
            "method": "elicitation/create",
            "params": {
              "mode": "form",
              "message": "Approve this deployment?",
              "requestedSchema": {
                "type": "object",
                "properties": {
                  "approved": { "type": "boolean" }
                },
                "required": ["approved"]
              }
            }
          },
          "workspace_roots": {
            "method": "roots/list",
            "params": {}
          }
        },
        "requestState": "opaque-integrity-protected-value"
      }
    }

    Request the inputs together when both are already known to be necessary. That avoids adding a round merely to discover the next predictable requirement. Do not request speculative information: the client may need to interrupt the user, invoke a model, or inspect local boundaries for every entry, and it may decline to perform any of them. Servers cannot assume that the client will answer or retry.

    Use elicitation when the missing value belongs to the user. The 2026-07-28 elicitation rules provide two modes:

    • Form mode collects structured data through the client. Its schema is limited to a flat object with primitive properties, and the client must allow review before submission. It is suitable for a confirmation, label, date, username, or other non-secret value. It is the wrong answer for a password, API key, access token, or payment credential; servers must not request those through form mode.
    • URL mode directs the user to an external interaction whose data must not pass through the MCP client. It suits sensitive input and third-party authorization. The client shows the target host and obtains consent before navigation. An accept response records consent to the interaction, not proof that the out-of-band work finished; after retry, the server checks its state and may need another input_required result. URL mode costs server-side web and identity handling, and it is the wrong answer for authorizing the MCP client to the MCP server itself.
    • Sampling asks the client to obtain a model result. Use it only when the client declared sampling and the operation truly needs model output. It adds a model invocation and its associated latency and resource use. It is the wrong answer for deterministic validation or data the server can compute directly.
    • Roots asks for the client’s current filesystem boundaries. Use it only when a resource or tool decision depends on those boundaries. It is not authorization by itself, and it is the wrong answer when the operation does not touch client-declared filesystem scope.

    Elicitation responses distinguish accept, decline, and cancel. Treat them as three outcomes. A decline is an explicit decision; cancellation means no decision was made. Do not translate either one into fabricated input or silently proceed as though approval was granted.

  3. Encode the continuation without exposing server state semantics

    Add requestState when the retry needs context that is not otherwise recoverable from the original parameters and input responses. The MRTR specification defines it as an opaque string meaningful only to the server. The server may encode its continuation in a protected blob, allowing the retry to land on another instance without shared storage or stateful load balancing.

    Include enough protected context to identify what can resume: the authenticated principal, a short expiry, the originating method, and a digest of the parameters that matter to the operation. If multiple rounds are possible, also carry and protect the current round count so the server can enforce its configured ceiling. Do not place secrets in a merely encoded but readable value on the assumption that “opaque” means confidential; opacity is a rule for client behavior, not a guarantee about representation.

    Stateless continuation is useful when any instance should be able to process the retry. Its concrete cost is a larger request and the engineering work to serialize, protect, rotate, and validate state. It is the wrong answer when the operation requires a strict single-use guarantee but no server-side record exists: an expiry and signed origin can bound replay, but they cannot prove that a valid state value has not already been consumed. For a one-time redemption or irreversible side effect, store a nonce or operation record and mark it consumed atomically.

    Server-side state is still allowed. It can reduce what travels through the client and can enforce single use, but it adds storage, cleanup, availability, and consistency work. Choose it when those properties are required, not merely because the earlier protocol kept transport sessions. MRTR requests are independent even when the application retains state.

  4. Have the client satisfy the result and retry the same logical operation

    When the client receives input_required, it processes each supported entry in inputRequests. It gathers user input, obtains the sampling result, or lists roots as requested. It then creates inputResponses, using each request key for its corresponding result.

    The retry is the same logical operation: preserve the original method and salient parameters, and attach inputResponses plus the exact requestState returned by the server. On the wire it is nevertheless a new, independent JSON-RPC request, so it must use a different JSON-RPC id and repeat the required _meta fields.

    {
      "jsonrpc": "2.0",
      "id": 42,
      "method": "tools/call",
      "params": {
        "name": "deploy",
        "arguments": {
          "environment": "staging"
        },
        "inputResponses": {
          "deployment_approval": {
            "action": "accept",
            "content": {
              "approved": true
            }
          },
          "workspace_roots": {
            "roots": []
          }
        },
        "requestState": "opaque-integrity-protected-value"
      },
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28"
      }
    }

    The abbreviated _meta above shows only the protocol version; a real request also includes the required client identity and capabilities.

    If the result contained requestState, the client must echo the exact string. It must not inspect, parse, edit, or infer anything from the value. If the result omitted requestState, the retry must omit it too. The state and responses belong only to this retry of the originating operation; do not copy them onto another request running in parallel.

    If inputRequests is absent, the client may retry immediately with the returned state. If it is present, the client must construct the requested inputs first. The client should also expose a way to decline, cancel, or stop retrying. Automatic unbounded retries turn a legitimate multi-round interaction into a loop and remove the user’s control over elicitation.

  5. Validate the resumed operation as a fresh security decision

    Treat returned requestState as attacker-controlled even when the client was instructed not to inspect it. Verify its integrity before it influences authorization, resource access, or business logic. Reject a value with a failed signature or authentication tag rather than attempting a partial recovery.

    Then verify every binding carried by the protected state:

    • The authenticated principal still matches. Reject state created for another user or client context.
    • The state has not passed its short expiry.
    • The method and digest of salient original parameters match the retry. This prevents state issued for one operation from being attached to another.
    • The protected round count remains within the server’s configured limit. Stop with a terminal outcome when the limit is reached instead of issuing another input_required result.
    • Any single-use state has not already been consumed. Enforce that property in server-side storage when replay could repeat an irreversible action.

    Re-evaluate the current authorization context before resuming. Do not let a previously issued state value preserve access after credentials, identity, permissions, or resource policy have changed. Binding the original principal prevents cross-user reuse; checking current authorization protects the operation when the same principal’s access has changed.

    These checks have real costs: integrity verification on each retry, key management, expiry handling, and storage for strict single-use cases. Omitting them is acceptable only in the narrow case identified by the specification where tampering can cause nothing worse than request failure. That exception is the wrong choice when state affects a resource, approval, authorization decision, side effect, or billable action.

  6. Validate responses and either finish or request the missing input

    Confirm that inputResponses is a valid map and that each recognized value can be parsed as the result type requested under that key. For form elicitation, validate the submitted content against the requested schema on the server even if the client already validated it. Handle decline and cancel explicitly rather than treating missing content as acceptance.

    Ignore additional response entries the server does not recognize or need. For malformed JSON, an invalid response shape, or an internal processing failure, return an appropriate JSON-RPC error. If a required answer is merely absent, return another input_required result for the missing information instead of turning an incomplete interaction into a protocol error.

    A second round is appropriate when the need could not have been known earlier, when an out-of-band URL interaction has not completed, or when the client omitted necessary input. It is the wrong answer when the server is repeating the same request without a condition that can change. Count every round, preserve the security bindings in the next state, and stop at the configured ceiling.

    Once the server has sufficient valid input and current authorization, complete the original operation and return its normal final result. Do not include another input_required result merely to acknowledge the responses.

Expected result

The completed implementation returns client work as an in-band input_required result, resumes through a new request for the same logical operation, and finishes with the operation’s normal result. The client supplies keyed inputResponses and echoes requestState unchanged; the server validates state, authorization, replay controls, round limits, and response data before performing resumed work.

Sources

  1. the release description says MRTR replaces those server-initiated requestsblog.modelcontextprotocol.io
  2. 2026-07-28 elicitation rulesmodelcontextprotocol.io
  3. MRTR specification defines it as an opaque string meaningful only to the servermodelcontextprotocol.io

See also