Skip to content

SEP-2322: Multi Round-Trip Requests - #2322

Merged
CaitieM20 merged 112 commits into
modelcontextprotocol:mainfrom
CaitieM20:merge
May 6, 2026
Merged

SEP-2322: Multi Round-Trip Requests #2322
CaitieM20 merged 112 commits into
modelcontextprotocol:mainfrom
CaitieM20:merge

Conversation

@CaitieM20

@CaitieM20 CaitieM20 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

This is a draft SEP for Multi Round-Trip Requests (MRTR) from the Transports Working Group. It is one of the changes discussed and planned at the December 2025 Core Maintainer Meetup. Exploring the Future of MCP Transports Blog

This SEP Is still in draft, Schema changes are proposed but not locked, documentation updates, conformance tests and additional work still to come, but its ready to move out of Transports Working Group and open up to broader feedback.

Authors: Mark D. Roth (@markdroth), Caitie McCaffrey (@CaitieM20), Gabriel Zimmerman (@gjz22)

Motivation and Context

See SEP for details.

How Has This Been Tested?

Conformance Tests: modelcontextprotocol/conformance#188

Breaking Changes

Yes, see SEP for details

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

Additional context

CaitieM20 and others added 30 commits February 20, 2026 10:36
…uestParams — now available on any

  client-initiated request, not just tool calls
   2. Added typed InputResponse = ElicitResult | CreateMessageResult — InputResponses values are now properly typed
  instead of { result: { [key: string]: unknown } }
   3. Updated examples — removed the extra result wrapper from TaskInputResponseRequest and
  TaskInputResponseRequestParams examples to match the new typed schema

  All checks pass: ✅ TypeScript compiles, ✅ JSON schema up to date, ✅ 140/140 examples valid.

Caitie Note: Need to take a look at task-input-response-params.json looks wrong on cursory inspection.
…itRequets these should only be sent as part of IncompleteResponse Objects now
…a JSONRPCIncompleteResultResponse and add examples
…pper in the SEP because InputRequests maps keys directly to the requests so the InputResponses now follows that pattern and there is no benefit to the added wrapper. The only argument for the wrapper would be if we wanted to carry error's alongside results but we do not in these cases. If there is an error the client would retry until the necessary information was retrieved and then send back to the server.
This was referenced Aug 11, 2026
koic added a commit to modelcontextprotocol/ruby-sdk that referenced this pull request Aug 13, 2026
…-2322

## Motivation and Context

Final step of SEP-2322 (modelcontextprotocol/modelcontextprotocol#2322) for the 2026-07-28 MCP spec release.
The client so far only recognized `input_required` results by raising `InputRequiredError`; this adds the resume loop,
mirroring the Python SDK's high-level `Client` driver and the TypeScript SDK's `runInputRequiredDriver`.

- The driver answers embedded requests with the handlers already registered through `on_elicitation`,
  `on_sampling`, and the new `on_roots`. There is no second registration path: a server can ask for input two
  ways, as a real request mid-call and as an entry in an `input_required` result, and one registration covers
  both. This follows the TypeScript driver, which dispatches "to the client's already-registered handlers
  (elicitation, sampling, roots - one generic engine, no per-feature API)".
- `on_elicitation` and `on_sampling` no longer refuse a transport without `on_server_request`. They record
  the handler either way and wire it to the transport only when it can carry server-to-client requests. Refusing
  would have locked stdio clients, and every 2026-07-28 connection, out of the only route the spec leaves:
  the modern lifecycle forbids server-to-client requests, so an embedded request is how it asks.
- `MCP::Client.new` gains `input_required_max_rounds:` (default 10, the TypeScript and Python default).
- Once a handler is registered, `call_tool`, `get_prompt`, and `read_resource` resume automatically:
  each `inputRequests` entry is fulfilled by the matching handler and the ORIGINAL request is re-issued with
  `inputResponses` under the same keys plus the byte-exact echoed `requestState`, on a fresh JSON-RPC id per leg
  (modern envelope stamping happens in the transports per send, so every leg carries the SEP-2575 triple).
  A `requestState`-only result (load shedding) retries after an exponential backoff from 50ms to a 250ms cap,
  matching the Python SDK; every leg counts against the round cap, whose exhaustion raises `InputRequiredError`
  carrying the last result.
- A requested kind without a matching handler falls back to the manual path by raising `InputRequiredError`,
  and clients with no handlers keep the exact pre-existing behavior. For manual driving,
  `call_tool` / `get_prompt` / `read_resource` gain `input_responses:` and `request_state:` keyword arguments
  that ride at the params top level. Non-MRTR methods such as `tools/list` keep raising unconditionally.

The conformance client rides the same change: it derives its lifecycle from the wire version the harness names per run
(`MCP_CONFORMANCE_PROTOCOL_VERSION`), connecting with `mode: :modern` for the 2026 draft and keeping the pinned legacy
handshake through 2025-11-25, retrying a modern connect once when the server rejects the first request with `-32022`
naming its supported versions. New scenario branches drive `sep-2322-client-request-state` through this driver
(an accepting `on_elicitation` handler plus the four `test_mrtr_*` tools), `request-metadata`, both SEP-2243 header
scenarios, and `json-schema-ref-no-deref`. The expected-failures baseline drops the six scenarios this stack resolves,
leaving only the not-scored extensions and the post-anchor `json-schema-2020-12-preservation`.

Verified end-to-end over stdio: a modern connection (`connect(mode: :modern)` with declared form-elicitation capability)
against a server whose tool returns `InputRequiredResult` under `RequestStateSecurity` sealing completes in two legs with
the handler supplying the answer, and a handler-less client receives `InputRequiredError` with the sealed opaque state.

Part of #382.

## How Has This Been Tested?

New tests in `test/mcp/client_test.rb` (Mocha sequenced transports) cover: the two-leg happy path asserting preserved original params,
key-matched `inputResponses`, byte-exact `requestState` echo, and a fresh id per leg; the exponential backoff sequence for
`requestState`-only legs (stubbed `sleep`); round-cap exhaustion raising `InputRequiredError` with the last state; the unhandled-kind
fallback; the manual `input_responses:`/`request_state:` keyword arguments; and the `prompts/get` driver path. The existing SEP-2322
recognition tests pin the no-handler behavior unchanged.

Three tests cover the shared-registration design specifically: the driver resolving an embedded request on a transport that
has no `on_server_request` at all, an `on_roots` handler answering an embedded `roots/list`, and `on_elicitation` and
`on_sampling` accepting a registration on such a transport rather than raising, which replaces the two tests that pinned
the old refusal.

`bundle exec rake` (tests, RuboCop, and conformance baseline) passes, plus the stdio end-to-end script described above.

With the separate SEP-2243 mirroring change applied alongside, every scored scenario of the frozen 2026-07-28 client leg passes
(`sep-2322-client-request-state` 5/5, `request-metadata` 5/5 with no version-retry warning, `http-custom-headers` 18/18,
`http-invalid-tool-headers` 11/11), and the `--requirements` client legs at both revisions report failures only
in not-scored scenarios. The full conformance task passes its baseline check on both legs.

## Breaking Changes

`on_elicitation` and `on_sampling` no longer raise `ArgumentError` on a transport without `on_server_request`; they
register the handler for the `input_required` route instead. Code that relied on the refusal to detect an unsuitable
transport needs another check. Everything else is additive: the new keyword arguments default to `nil`, and with no
handler registered every code path is identical to before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

final SEP finalized. roadmap/transport Roadmap: Transport Evolution & Scalability (incl. Server Cards) SEP transport Related to MCP transports

Projects

Status: Accepted

Development

Successfully merging this pull request may close these issues.