Skip to content

Drive multi round-trip input_required results on the client per SEP-2322 - #500

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:client_input_required_driver
Open

Drive multi round-trip input_required results on the client per SEP-2322#500
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:client_input_required_driver

Conversation

@koic

@koic koic commented Aug 10, 2026

Copy link
Copy Markdown
Member

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.

  • MCP::Client.new gains elicitation_handler:, sampling_handler:, and roots_handler: callbacks plus input_required_max_rounds: (default 10, the TypeScript and Python default). Handlers receive the embedded request's params and return the corresponding result shape; a decline/cancel elicitation result is a valid answer and flows back to the server as-is.
  • With at least one handler configured, 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 without any 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 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.

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

None. All new constructor and method keyword arguments default to nil, and without configured handlers every code path is identical to before.

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

- `MCP::Client.new` gains `elicitation_handler:`, `sampling_handler:`, and `roots_handler:` callbacks plus
  `input_required_max_rounds:` (default 10, the TypeScript and Python default). Handlers receive
  the embedded request's params and return the corresponding result shape; a decline/cancel elicitation result
  is a valid answer and flows back to the server as-is.
- With at least one handler configured, `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 without any 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 `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 modelcontextprotocol#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.

`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

None. All new constructor and method keyword arguments default to `nil`, and without configured handlers every code path is
identical to before.
@koic
koic force-pushed the client_input_required_driver branch from 2d30e14 to 54fcff7 Compare August 10, 2026 03:39
@koic koic changed the title Fulfill input_required Results on the Legacy Wire per SEP-2322 Drive multi round-trip input_required results on the client per SEP-2322 Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant