ruby-sdk/test
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Koichi ITO b159314baa Fulfill input_required results on the legacy wire per SEP-2322
## Motivation and Context

SEP-2322 handlers return `InputRequiredResult` instead of issuing in-flight server-to-client requests,
but that result type exists only on the 2026-07-28 wire: pre-2026 clients treat an unknown `resultType`
as a final result, so the server rejected `input_required` on legacy requests with an internal error.
That forced authors to keep two handler styles, one per era.

This adds the dual-era authoring shim both reference implementations converged on:
the TypeScript SDK's default-on legacy fulfilment shim (typescript-sdk#2381)
and the Python SDK's version-negotiating resolver (python-sdk#2986).
When a `tools/call`, `prompts/get`, or `resources/read` handler returns an `InputRequiredResult` on
the legacy wire, the server fulfills it in place of the client's driver:

- Every `inputRequests` entry is sent verbatim as the equivalent real server-to-client request through
  a new `ServerSession#fulfill_input_request` (per the spec, clients treat each embedded entry exactly like
  the standalone request), stamped with the originating request id so the association requirement of
  SEP-2260 holds and Streamable HTTP delivery rides the originating POST stream. One exception to
  the verbatim forwarding: a URL-mode elicitation leg gains a synthesized `elicitationId` when
  the handler supplied none, since the 2025-11-25 wire requires the field and the 2026-07-28 in-band
  shape dropped it (correlation rides `requestState` there) - matching the TypeScript shim.
- The answers are collected under the same keys and the handler re-runs with `inputResponses`
  and the raw `requestState` merged into the original params - the same deterministic replay contract
  the modern client driver follows, read through the existing `server_context.input_responses`/`request_state` accessors.
  Only the current round's responses are sent, matching the client driver; handlers carry earlier answers through `requestState`.
- `requestState` round-trips in-process as the value the handler wrote. `RequestStateSecurity` sealing is
  wire hardening against a tampering client and does not apply to the in-process replay.
- The embedded-capability gate still runs each round against the session's declared client capabilities;
  a violation fails before any client contact. The `-32021` code is 2026-only, so the legacy wire gets
  an internal error naming the missing capabilities.
- Rounds are capped at 8 (`LEGACY_INPUT_REQUIRED_MAX_ROUNDS`), matching the TypeScript shim's `maxRounds` default;
  exhaustion fails with an explanatory internal error, which also bounds the `requestState`-only load-shedding form
  that cannot make progress in-process.

The shim is on by default, matching the TypeScript SDK; `Server.new(input_required_legacy_shim: false)` restores
the strict rejection. Requests without a session (no notification path back to a client) and all modern requests
are untouched: the modern wire keeps returning the `input_required` result for the client driver.

Resolves #382.

## How Has This Been Tested?

`bundle exec rake` passes: 1691 test runs / 4381 assertions with zero failures and RuboCop clean.
The conformance baseline check passes on both legs (the frozen 2025-11-25 wire is where the shim lives),
and the modern `input-required-result-*` scenarios stay green, confirming the modern path is untouched.

New `test/mcp/server_input_required_legacy_shim_test.rb` covering: completion through
a real elicitation with the SEP-2260 association asserted on the transport, replay exposing
the collected responses and the raw `requestState`, sealing bypass under `RequestStateSecurity`,
`prompts/get` and `resources/read` shimming, multi-round collection, the round cap, the capability gate failing
before client contact, the URL-mode leg gaining a synthesized `elicitationId` (and a handler-supplied
one riding unchanged), a client answering a fulfilment leg with an error failing the original request,
the opt-out, session-less rejection, and modern requests staying untouched.

The pre-existing legacy rejection test (session-less) and all modern MRTR serialization tests pass unchanged.

## Breaking Changes

Only for handlers that returned `InputRequiredResult` to legacy clients THROUGH A SESSION and relied
on the internal-error rejection: those now drive a real fulfilment round trip by default.
No such handler could have worked end to end before (the request always failed), so this is the intended upgrade;
`input_required_legacy_shim: false` restores the previous behavior. Everything else is unchanged.
2026-08-14 02:46:38 +09:00
..
mcp Fulfill input_required results on the legacy wire per SEP-2322 2026-08-14 02:46:38 +09:00
initialize_params_test_helper.rb Validate Required Params for initialize and resources/subscribe 2026-07-11 22:36:34 +09:00
instrumentation_test_helper.rb Internal Release 0.5.1 2025-05-08 17:38:05 -04:00
json_rpc_handler_test.rb fix: stop leaking exception messages to clients via JSON-RPC error data 2026-08-06 18:28:17 +02:00
test_helper.rb feat: deprecate roots sampling and logging 2026-08-02 13:13:10 -04:00