## Motivation and Context
`MCP::Client#tools`, `#resources`, `#resource_templates`, and `#prompts` follow `nextCursor`
until the server stops offering one. The `seen` set in `fetch_all_pages` catches a server that
repeats or cycles cursors, but a fresh cursor on every response walks past that guard, and
the client keeps requesting and retaining pages for as long as the server keeps producing them.
The transports' per-message limits do not help here, since each individual page can be small.
`MCP::Client.new` now takes `max_pages:`, defaulting to `MAX_PAGES` (1000). Reaching it raises
`PaginationLimitError` rather than truncating the collection silently, since a short array would
be indistinguishable from a server that genuinely ran out of items. The single-page `list_*` methods
are unchanged, so callers who need their own policy still have one.
## How Has This Been Tested?
New tests in `test/mcp/client_test.rb` cover a server that returns a distinct cursor per page,
the boundary where a server stops exactly at `max_pages`, and the argument validation.
The existing repeat-cursor and cycling-cursor tests are unaffected.
`bundle exec rake` (tests, RuboCop, and conformance baseline) passes.
## Breaking Changes
An all-pages call against a server offering more than 1000 pages now raises
`MCP::Client::PaginationLimitError` instead of continuing. Pass a larger `max_pages:` to
`MCP::Client.new` if that is expected.
## 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.
## Motivation and Context
SEP-2243's custom-header half (MCP 2026-07-28) lets a tool annotate `inputSchema` properties with
`x-mcp-header` so intermediaries can route on argument values without parsing bodies: the client MUST
mirror each annotated argument of a `tools/call` into an `Mcp-Param-{Name}` header, encode values that
cannot ride as plain ASCII field values with the `=?base64?...?=` sentinel, and omit the header for
`null` or absent arguments. `MCP::Client::HTTP` sent only the standard `Mcp-Method`/`Mcp-Name` mirror
headers, so every `http-custom-headers` check of the 2026-07-28 conformance requirements fails.
The new `MCP::Client::McpParamHeaders` module ports the TypeScript SDK's `mcpParamHeaders` codec:
- `scan` walks a tool's `inputSchema` for declarations and validates every constraint the spec places
on them: RFC 9110 token names, case-insensitive uniqueness, primitive-typed declaring properties
(`number` is accepted alongside the spec's `string`/`integer`/`boolean` because the published
referee annotates `type: "number"` parameters, the same accommodation the TypeScript SDK makes),
and static reachability through a chain of `properties` keys only: an annotation under `items`,
the combinators, or `$defs` invalidates the whole tool definition.
- `build` resolves each declaration's path in the call's `arguments`, omits `null`/absent and
non-representable values (unsafe integers, non-finite floats, non-primitives), converts primitives
per the spec's rules, and encodes with the sentinel exactly where a plain ASCII field value
cannot carry the value (empty, edge whitespace, bytes outside visible ASCII, sentinel-shaped input).
`MCP::Client::HTTP` learns declarations from the `tools/list` responses passing through it (the same
source of truth the TypeScript SDK's response cache uses) and mirrors on each `tools/call` alongside
the existing `Mcp-Method`/`Mcp-Name` construction. Learning and mirroring exist on the modern
lifecycle only, matching both reference SDKs. An invalid tool definition mirrors nothing, per
the spec's guidance to send without custom headers when no reliable declarations are available;
a complete (uncursored, `nextCursor`-less) listing rebuilds the registry so declarations of
unlisted tools stop mirroring (the Python SDK's pruning), and the registry is capped
(`MAX_MCP_PARAM_TOOLS`, 1000) so a server rotating tool names cannot grow it without bound.
On the modern lifecycle, `MCP::Client#list_tools` and `#tools` also enforce the spec's MUST:
a tool definition whose `x-mcp-header` annotations violate the constraints is excluded from
the listing, with a warning naming the tool and the reason (the spec's SHOULD), so one malformed
definition does not block the valid tools - the same filtering the Python SDK's
`_absorb_tool_listing` and the TypeScript SDK's cached-listing finalize apply. A value that cannot
be represented as UTF-8 omits its header like the other non-representable values.
The README documents the mirroring and the exclusion under the lifecycle negotiation section.
The server-side validation half of the codec is a follow-up.
## How Has This Been Tested?
New `test/mcp/client/mcp_param_headers_test.rb` covers the scan constraint matrix (root and
non-reachable placements, empty/non-token/duplicate names, non-primitive types), the primitive
conversions (including `42.0` serializing as `42` and unsafe integers refusing), the encoding matrix
(plain pass-through, empty, non-ASCII, edge whitespace, CR/LF, sentinel-shaped input), and header
construction with nested paths and omitted values (including a value that cannot be represented
as UTF-8). `test/mcp/client/http_test.rb` drives the transport end to end over a modern connection:
a `tools/list` teaching the declarations, the following `tools/call` carrying the mirrored headers,
an invalid declaration mirroring nothing, a legacy connection learning nothing, and a complete
listing pruning the declarations of unlisted tools. `test/mcp/client_test.rb` covers the MUST-level
exclusion: the invalid definition dropped from `#tools` with the warning on a modern connection,
and listed unchanged on a legacy one. `bundle exec rake` is green.
## Breaking Changes
None. Requests to servers whose tools carry no `x-mcp-header` annotations are byte-for-byte unchanged,
and the new headers only appear for tools that declare them.
## Motivation and Context
Final step of the stateless lifecycle (SEP-2575, modelcontextprotocol/modelcontextprotocol#2575) for
the 2026-07-28 MCP spec release, now that the server core and both server transports can serve modern traffic.
Server side, `Server#discover` reaches its spec-final shape:
- `supportedVersions` advertises modern versions only, matching the TypeScript and Python SDKs:
legacy versions are negotiated via `initialize`, not selected from discovery. This also closes
an interop hazard of the interim response: auto-negotiating TS and Python clients treat
any `DiscoverResult` as proof of a modern server and do not fall back to `initialize`,
so advertising only legacy versions made them fail outright.
- The `ttlMs`/`cacheScope` cache hints are REQUIRED on `DiscoverResult` (unlike the opt-in SEP-2549 hints
on list and read results), so the spec defaults (`0`, `"private"`) fill in when the server was not configured with
`ttl_ms`/`cache_scope`, and the configured SEP-2549 values are reused otherwise.
- The server identity rides in the result `_meta` as the optional `io.modelcontextprotocol/serverInfo` stamp
instead of a top-level `serverInfo` field, following spec PR modelcontextprotocol/modelcontextprotocol#3002
(the TypeScript SDK stamps `_meta` the same way and the Python `DiscoverResult` carries no top-level
`serverInfo`, so the top-level placement was invisible to both). `Client#discover` and
the `server_implementation` reader read the `_meta` stamp with a top-level fallback for servers built
against the frozen SEP text, and the key is exposed as `RequestEnvelope::SERVER_INFO_META_KEY`.
Client side, both bundled transports learn the modern lifecycle, and `MCP::Client` negotiates it automatically by default:
- `MCP::Client#discover` sends `server/discover` and returns a `DiscoverResult` struct
(`supported_versions`, `capabilities`, `server_info`, `instructions`, `ttl_ms`, `cache_scope`).
It works before `connect`; the stdio transport's connect guard exempts `server/discover`
and spawns the subprocess on demand.
- `connect` gains `mode:` and defaults to automatic negotiation, joining the Python, Go, and C# SDKs
(the TypeScript SDK keeps a legacy default): transports whose `connect` declares `mode:`
(the bundled HTTP and stdio transports) receive `:auto`, which probes `server/discover`
and falls back to the legacy handshake on any discovery failure, including a successful discovery
without a mutually supported modern version (rollout tolerance for servers that answer discovery
while only serving legacy versions). Custom transports whose `connect` does not declare `mode:`
keep receiving the historical legacy call shape, detected by keyword introspection;
a bare `**kwargs` deliberately does not count, so wrappers and test doubles that absorb arbitrary keywords
stay on the legacy shape. An explicit `:modern`/`:auto` against such a transport raises `ArgumentError`
instead of silently downgrading.
- The mode vocabulary is `:legacy`/`:auto` (shared with the Python and TypeScript SDKs) plus an explicit `:modern`,
instead of folding a version-string pin into `mode` as Python and TypeScript do: this client has carried
the scalar `protocol_version:` keyword since the handshake landed, so a second version-bearing input would make
the API ambiguous about which one wins. `mode:` stays the strategy and `protocol_version:` stays the version,
the same orthogonal split as the Go and C# scalar version pins; Python's `mode="2026-07-28"` is expressed here
as `mode: :modern` with `protocol_version: "2026-07-28"`.
- An explicit legacy-generation `protocol_version` (e.g. `"2025-11-25"`) pins the legacy handshake without a probe,
so an explicitly requested version is never overridden by the default negotiation adopting the modern lifecycle.
`mode: :legacy` forces the classic handshake unconditionally; prefer it for spawn-per-invocation CLI tools
and when using server-initiated requests, which exist only on the legacy lifecycle.
- Because the raw `connect` return value and `server_info` mirror the wire result, their shape depends
on the negotiated era (`InitializeResult` vs `DiscoverResult`). New era-independent readers absorb that difference:
`MCP::Client#protocol_version` (negotiated or adopted version; backed by a new stdio transport reader and the existing HTTP one),
`#server_capabilities`, `#instructions`, and `#server_implementation`, which reads the spec-final `_meta`
`io.modelcontextprotocol/serverInfo` stamp with a top-level fallback and returns `nil` when a modern server
does not identify itself. `server_info` remains the permanent raw window to everything the readers do not cover.
- The stdio `server/discover` probe is bounded by a new `DEFAULT_DISCOVER_PROBE_TIMEOUT` (5 seconds, matching the C# SDK)
when no `read_timeout` was configured: a compliant legacy server answers the probe with `-32601` immediately,
but a non-compliant one that silently drops unknown methods must read as legacy evidence for the auto fallback
instead of blocking `connect` forever. Regular requests keep the unbounded default.
- In modern mode, the new shared `MCP::Client::ModernEnvelope` stamps the SEP-2575 `_meta` triple onto
every request (never onto notifications, whose `_meta` carries no envelope), reusing the reserved key names
from `MCP::RequestEnvelope`. The HTTP transport sends the matching `MCP-Protocol-Version` header and
keeps the existing `Mcp-Method`/`Mcp-Name` mirror headers; no `Mcp-Session-Id` is ever attached because `initialize`
is never sent.
- The conformance client pins `mode: :legacy`: the referee validates the legacy `initialize` handshake
and its scenario servers do not expect a preceding probe.
Resolves#389.
## How Has This Been Tested?
New tests in `test/mcp/server_test.rb` pin the required cache hints (spec defaults and configured SEP-2549 values);
the existing discover assertions across `server_test.rb` and both transport test files were updated to
the modern-only `supportedVersions`.
New tests in `test/mcp/client_test.rb` cover the `DiscoverResult` struct mapping, `ValidationError`/`ServerError`
on malformed and error responses, and the mode resolution contract: the `:auto` default on transports declaring `mode:`,
the unchanged legacy call shape for transports without it, the legacy pin for an explicit stable `protocol_version`,
`ArgumentError` for explicit non-legacy modes on transports without `mode:`, unknown-mode rejection,
and the era-independent readers against legacy results, modern results, the `_meta` `serverInfo` stamp, and absence.
New tests in `test/mcp/client/http_test.rb` cover modern adoption via discovery, envelope stamping with
the matching header on subsequent requests, the no-mutual-version failure, both auto-mode fallbacks,
and argument validation. New tests in `test/mcp/client/stdio_test.rb` cover pre-connect `server/discover`,
modern adoption with envelope stamping and no `initialize` frame, the auto-mode fallback, the probe timeout
default applying only while probing, and the fallback when a server never answers the probe.
## Breaking Changes
`server/discover` responses change shape (modern-only `supportedVersions` plus the required `ttlMs`/`cacheScope`);
this method has not shipped in a gem release yet, so no released behavior changes.
On the bundled HTTP and stdio transports, `MCP::Client#connect` without `mode:` now probes `server/discover` before
the legacy handshake: against legacy servers this adds one round trip and an unknown-method entry in their logs with
an unchanged final result, and once a server serves the modern lifecycle the connection adopts it, changing
the raw result shape from `InitializeResult` to `DiscoverResult`. Pass `mode: :legacy` or an explicit legacy-generation
`protocol_version` to keep the previous behavior unconditionally, or use the era-independent readers, which are stable
across both lifecycles. Custom transports without `mode:` and direct `transport.connect` calls are unaffected.
Client#tools/#list_tools built Tool objects without an annotations
field even though the server includes annotations in every
tools/list response. Add the reader and pass the raw hash through,
mirroring how output_schema is already handled.
## Motivation and Context
The MCP specification defines `sampling/createMessage` as a server-to-client request
that lets a server ask the client to generate an LLM completion, so the server can leverage
the client's model access without its own API keys.
The Ruby SDK already supports the server side (sending the request) and, on the client side,
the generic server-to-client request infrastructure introduced for elicitation
(the standalone GET SSE listening stream and `on_server_request` dispatch).
This adds the client-side `sampling/createMessage` handler on top of that infrastructure.
## Changes
- Add `MCP::Client#on_sampling`, a thin wrapper that registers a `sampling/createMessage` handler
via `transport.on_server_request`, mirroring `on_elicitation`. The handler receives the request params
and returns a `CreateMessageResult`-shaped Hash sent back as the JSON-RPC result.
- Document client-side sampling in the README, including the `sampling` (and `sampling.tools`)
capability declaration and the human-in-the-loop guidance from the specification.
## How Has This Been Tested?
- Unit tests for `on_sampling` covering handler registration on the transport
and the error raised when the transport does not support server-to-client requests.
- An HTTP transport test that dispatches a `sampling/createMessage` server request delivered on
the SSE stream and returns a `CreateMessageResult`.
## Deprecation Note
SEP-2577 deprecates sampling (along with roots and logging) starting with protocol version `2026-07-28`.
The deprecation warnings are added in https://github.com/modelcontextprotocol/ruby-sdk/pull/406.
Sampling remains fully supported under the current `2025-11-25` protocol version and stays available
throughout the spec's deprecation window, so the client side is implemented here for parity with
the server side and with the Python and TypeScript SDKs.
`MCP::Client#on_sampling` carries the same `@deprecated` annotation as the server-side senders
annotated in #429, tailored to the receiving side: the handler exists to interoperate with servers
that still send sampling requests during the deprecation window. The README section carries
the same note.
Ref: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
## Motivation and Context
Resolves the `elicitation-sep1034-client-defaults` client conformance scenario
(SEP-1034, https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1034).
The Ruby HTTP client could not receive server-to-client requests at all:
it never opened the spec's standalone GET SSE listening stream, and SSE events that
were not responses were discarded. A server sending `elicitation/create` during a tools/call
(such as the conformance harness, which is built on the TypeScript SDK server)
either dropped the request silently or deadlocked the tool call waiting for an answer that
could never arrive.
This change adds three pieces:
### Server-to-client request dispatch
`MCP::Client::HTTP#on_server_request(method) { |params| ... }` registers a handler;
when a JSON-RPC request (a message with both `method` and `id`) arrives on any SSE stream,
the handler's return value is POSTed back to the server as the JSON-RPC `result`
(the server ACKs with 202 Accepted, per the transport spec).
Methods without a handler are answered with a `-32601` method-not-found error.
A handler may raise `MCP::Client::ServerRequestError` to answer with a specific JSON-RPC error code
(for example `-1` for a rejected sampling request, per the sampling spec),
mirroring the TypeScript SDK's `McpError` and the Python SDK's `ErrorData` return;
any other error is answered with a `-32603` internal error. In every case the server is not left waiting,
matching how the TypeScript and Python SDKs convert handler failures into JSON-RPC error responses.
### A standalone GET SSE listening stream
Registering a handler (once connected) opens the spec's listening stream on a background thread,
because servers deliver requests that are not tied to a client request on that stream -
the TypeScript SDK's `StreamableHTTPServerTransport` silently drops them when no GET stream is open.
Mirrors the TypeScript and Python SDK clients, which start listening right after the `initialize` handshake
without awaiting stream establishment. Reconnection semantics also follow both SDKs:
a gracefully closed stream is retried indefinitely after the server's `retry:` interval,
while consecutive connection failures (HTTP errors and idle read timeouts) stop the listener once
they reach the reconnection attempt cap, with a successful stream resetting the count;
a 405 stops immediately without retrying, since it is the spec's answer from a server that
offers no listening stream. The listening GET uses a 5-minute read timeout matching
the Python SDK's `sse_read_timeout` default. `close` terminates the thread.
### SEP-1034 defaults
`MCP::Client::Elicitation.apply_defaults` fills fields omitted from the elicitation content with
the `default` values declared in `requestedSchema` properties, never overwriting provided values
and skipping properties without a default. This follows the Python SDK, which applies defaults in
the elicitation callback; the TypeScript SDK's automatic `applyElicitationDefaults`
(gated on the `elicitation.form.applyDefaults` capability) is a possible follow-up.
`MCP::Client#on_elicitation` registers a handler for `elicitation/create` as a convenience.
The conformance client gains an `elicitation-sep1034-client-defaults` branch that connects with
`capabilities: { elicitation: {} }`, accepts the elicitation with defaults applied, and calls
the harness's `test_client_elicitation_defaults` tool; the scenario is removed from the expected failures baseline.
## How Has This Been Tested?
New unit tests covering: handler dispatch from a request's POST SSE stream with
the response body and 202 ACK verified, the `-32601` answer for unregistered methods,
the `-32603` answer for a raising handler, the custom-code answer when a handler raises
`ServerRequestError`, listener startup from both registration-after-connect and
connect-with-registered-handlers, listener shutdown after consecutive connection failures with
the count resetting after a successful stream and an immediate stop on 405,
and `apply_defaults` for all five SEP-1034 property types (string, integer, number, enum, boolean),
non-overwriting of provided values, omission of properties without defaults, and symbol-keyed schemas
## Breaking Changes
None. The listening stream and its background thread are created only when a server-request handler is registered;
clients that never call `on_server_request` / `on_elicitation` keep the existing single-request behavior.
## Motivation and Context
SEP-2549 (modelcontextprotocol/modelcontextprotocol#2549, merged for the 2026-07-28 spec release) adds a `CacheableResult`
shape with `ttlMs` (freshness lifetime in milliseconds, max-age semantics, 0 = do not cache) and `cacheScope`
(`"public"` or `"private"`, whether shared intermediaries may cache) to `ListToolsResult`, `ListPromptsResult`,
`ListResourcesResult`, `ListResourceTemplatesResult`, and `ReadResourceResult`, complementing the `listChanged` notifications.
The field shape follows the Python SDK's implementation (python-sdk#2824, `ttlMs` + `cacheScope` with `ttlMs: 0` /
`cacheScope: "public"` defaults; the TypeScript SDK's earlier `ttl`-seconds prototype #2070 was superseded by this shape).
Because this SDK negotiates protocol versions up to 2025-11-25, where the fields do not exist, emission is opt-in:
when neither `ttl_ms` nor `cache_scope` is configured, responses are serialized exactly as before. The fields become REQUIRED
in 2026-07-28; a follow-up can auto-emit defaults once that version enters `SUPPORTED_STABLE_PROTOCOL_VERSIONS`.
- `MCP::Server.new` gains `ttl_ms:` and `cache_scope:` keywords with validating writers modeled on `page_size=`
(`ttl_ms` must be nil or a non-negative Integer; `cache_scope` must be nil, `"public"`, or `"private"`).
- A private `apply_cache_metadata` wraps the five emission points. When the user sets only one of the two values,
the other is filled with the spec defaults (`ttlMs: 0`, the only universally safe freshness value, and `cacheScope: "public"`,
the spec and Python default).
- A `resources_read_handler` may return a full `{ contents:, ttlMs:, cacheScope: }` hash to override the server-level hints per result;
the documented bare-contents return shape keeps working unchanged.
- The client's `ListToolsResult` / `ListPromptsResult` / `ListResourcesResult` / `ListResourceTemplatesResult` structs gain
`ttl_ms` and `cache_scope` members populated from the response fields.
Resolves#387.
## How Has This Been Tested?
New tests in `test/mcp/server_test.rb`:
- with `ttl_ms: 5000`, all four list results carry `ttlMs: 5000` and the filled `cacheScope: "public"` default
- with only `cache_scope: "private"`, `resources/read` carries `ttlMs: 0`
- with neither configured, list and read results omit both fields (wire-format regression)
- the hints appear alongside `nextCursor` when paginating
- a `resources_read_handler` returning `{ contents:, ttlMs: 60_000 }` overrides the server-level value while the missing `cacheScope` is filled
- invalid writer values (`-1`, `1.5`, `"internal"`) raise `ArgumentError`
New tests in `test/mcp/client_test.rb` assert all four list result structs expose `ttl_ms` / `cache_scope`,
and that both are nil when the server omits the fields.
## Breaking Changes
None. Emission is opt-in, the new constructor keywords default to nil, and the client structs gain additive members.
## Motivation and Context
SEP-2322 (modelcontextprotocol/modelcontextprotocol#2322, merged for the 2026-07-28 spec release)
introduces Multi Round-Trip Requests: instead of issuing in-flight server-to-client JSON-RPC requests,
a server answers with a result whose `resultType` is `"input_required"`, carrying an `inputRequests` map
(of `sampling/createMessage`, `roots/list`, and `elicitation/create` request shapes) and
an opaque `requestState`; the client fulfills the requests and re-issues the original request with
`inputResponses` and the echoed `requestState`.
The wire contract (the `resultType` discriminator and the `inputRequests`/`requestState` shape)
stayed stable across all three closed TypeScript prototype iterations (typescript-sdk#2062/#2065,
the v2-stateless stack, and #2251) and the Python draft (python-sdk#2322), but the server-side
suspend/resume mechanism is still unsettled in both SDKs (typescript-sdk#2251 was put on hold on 2026-06-08).
This change therefore implements only the stable, additive vocabulary and the client-side recognition,
leaving server emission and automatic resumption for a follow-up once the reference design lands:
- New `MCP::ResultType` module with `COMPLETE` and `INPUT_REQUIRED` constants documenting the `resultType` values.
- `MCP::Client` raises the new `MCP::Client::InputRequiredError` (exposing `input_requests`, `request_state`,
and the raw `result`) when any response carries `resultType: "input_required"`, instead of silently returning
a non-final result as if it were the answer. The check lives in the shared request path,
so every client method is covered. Servers on stable protocol versions never emit `resultType`,
so default behavior is unchanged.
Part of #382.
## How Has This Been Tested?
New tests in `test/mcp/client_test.rb`:
- `call_tool` raises `InputRequiredError` for an `input_required` result and exposes `input_requests`,
`request_state`, and the full raw result
- `call_tool` returns normally when `resultType` is `"complete"` and when it is absent
(wire-compat regression for stable-protocol servers)
- `list_tools` also raises for `input_required` results, proving the recognition covers the shared request path
`bundle exec rake` (tests, RuboCop, and conformance baseline) passes.
## Breaking Changes
None for spec-compliant stable servers, which never send `resultType`. A response that does carry
`resultType: "input_required"` now raises `MCP::Client::InputRequiredError` instead of being returned as
a final result, which was always a misinterpretation of the draft semantics.
## Motivation and Context
The server-side half of MCP `notifications/cancelled` shipped earlier and lets handlers
observe cancellation via `server_context.cancelled?` / `raise_if_cancelled!`.
This PR delivers the client-side equivalent that was deferred at the time:
an `MCP::Client` user can now cancel a request they have already issued and have
the calling thread wake up, matching the Python SDK
(`anyio.CancelScope`) and TypeScript SDK (`AbortSignal`) ergonomics.
The recommended pattern is to pass an `MCP::Cancellation` token into the request method,
run the request on a worker thread, and call `cancellation.cancel(reason:)` from another thread.
The cancelling thread sends `notifications/cancelled` to the server, and the calling thread is
woken up with `MCP::CancelledError`:
```ruby
cancellation = MCP::Cancellation.new
Thread.new do
client.call_tool(name: "slow_tool", arguments: {}, cancellation: cancellation)
rescue MCP::CancelledError
# cleanup
end
cancellation.cancel(reason: "user pressed cancel")
```
For low-level use, `Client#cancel(request_id:, reason:)` sends the notification without managing
the calling thread, and `Client#generate_request_id` lets callers pre-allocate an id before kicking off
a request on another thread. The `request_id:` and `cancellation:` keywords are accepted by
every request method (`tools`, `list_tools`, `resources`, `list_resources`, `resource_templates`,
`list_resource_templates`, `prompts`, `list_prompts`, `call_tool`, `read_resource`, `get_prompt`,
`complete`, `ping`).
When `cancellation:` is supplied, the actual blocking `transport.send_request` runs on a worker thread;
the calling thread waits on a `Queue` woken either by the response or by the cancel signal
(whichever arrives first - the same race contract as the server-side `StreamableHTTPTransport#cancel_pending_request`).
On a normal completion the `on_cancel` hook is deregistered so a late cancel does not emit a stray `notifications/cancelled`.
The worker thread is *not* force-killed when a cancel wins the race; it stays blocked on the underlying I/O until
the transport actually returns (or the user closes it). For `Client::HTTP` the leak resolves as soon as
the server sends any response; for `Client::Stdio` the user may need to call `client.transport.close`
if the server stops responding entirely.
`Client::Stdio` and `Client::HTTP` gain `send_notification(notification:)` so `Client#cancel` can deliver
the JSON-RPC notification through the existing transport plumbing. Custom transports that do not implement
`send_notification` cause `Client#cancel` to raise `NoMethodError`.
Ref: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
## How Has This Been Tested?
`test/mcp/client_test.rb` covers `Client#cancel` payload (with and without `reason`),
`Client#generate_request_id` returning distinct UUIDs, every request method threading `request_id:` through,
and the full `cancellation:` flow:
- Returns the response when no cancel happens (no behaviour regression for callers that do not pass a token).
- Pre-cancelled token raises `MCP::CancelledError` immediately and never reaches the transport
(no stray request, no stray notification).
- Mid-flight cancel raises `MCP::CancelledError` carrying the supplied reason and sends `notifications/cancelled`
through the transport.
- Late cancel after a normal completion does not emit a stray notification
(the `on_cancel` hook is deregistered in the `ensure` block).
`test/mcp/client/stdio_test.rb` covers `send_notification` writing the JSON line through the spawned process
and returning `nil` without waiting for a response.
`test/mcp/client/http_test.rb` covers `send_notification` POSTing the body, tolerating HTTP 202 Accepted,
and surfacing Faraday errors as `RequestHandlerError`.
## Breaking Change
None. All new keywords (`request_id:`, `cancellation:`) on request methods are optional and default to `nil`.
Existing callers that do not pass them get the original synchronous behaviour. Custom transports that already
implement `send_request(request:)` continue to work; only callers of `Client#cancel` need a transport
that responds to `send_notification`, and the failure mode is a clear `NoMethodError` rather than silent breakage.
## Motivation and Context
SEP-2164 (modelcontextprotocol/modelcontextprotocol#2164, merged for the 2026-07-28 spec release)
standardizes the resource-not-found error on the JSON-RPC Invalid Params code (-32602),
dropping the legacy -32002 code, and both reference SDK implementations attach the requested URI
as structured error data (typescript-sdk#2267 throws `InvalidParams` with `data: { uri }`;
python-sdk#2344 adds a `ResourceNotFoundError` that maps to `code=-32602, data={"uri": ...}`).
The Ruby server already used -32602 (via `error_type: :invalid_params`) for its built-in not-found path
(`completion/complete` with an unknown `ref/resource` URI), but the response carried no structured data
and the generic "Invalid params" message, and there was no public error class for `resources_read_handler`
blocks to raise. The Ruby client never special-cased -32002, so nothing needed removal there.
- New public `MCP::Server::ResourceNotFoundError` (modeled on the `URLElicitationRequiredError` precedent):
code -32602, message `"Resource not found: <uri>"`, and `data: { uri: <uri> }`.
Custom `resources_read_handler` blocks raise it for unknown URIs; the README documents the pattern.
- The `completion/complete` `ref/resource` not-found path now raises this class,
so its error response gains `data: { uri: }` and a descriptive message instead of
the generic "Invalid params" text.
- The default `resources/read` behavior (returning empty contents for unknown URIs
when no handler is registered) is intentionally unchanged; raising there would
alter existing wire behavior.
Resolves#379.
## How Has This Been Tested?
New tests in `test/mcp/server_test.rb`:
- `resources/read` with a handler raising `ResourceNotFoundError` responds
with code -32602, message `"Resource not found: file:///missing.txt"`,
and `data: { uri: "file:///missing.txt" }`
- `completion/complete` with an unknown `ref/resource` URI carries the same code,
message, and structured data
New test in `test/mcp/client_test.rb` asserts `Client#read_resource` surfaces
the -32602 code and the `data` hash unmodified through `Client::ServerError`
(regression: no special-casing swallows it).
The pre-existing completion error-code tests pass unchanged.
`bundle exec rake` (tests, RuboCop, and conformance baseline) passes.
## Breaking Changes
None at the JSON-RPC code level (-32602 before and after).
The `completion/complete` resource-not-found error response changes shape within
the error object: `message` becomes `"Resource not found: <uri>"` instead of
`"Invalid params"`, and `data` becomes `{ uri: <uri> }` instead of the message string.
Both members are advisory per JSON-RPC 2.0.
## Motivation and Context
SEP-414 (modelcontextprotocol/modelcontextprotocol#414, merged for
the 2026-07-28 spec release) documents OpenTelemetry trace context propagation
conventions for MCP: the un-prefixed `_meta` keys `traceparent`,
`tracestate`, and `baggage` are reserved (an explicit exception to
the reverse-DNS prefix rule) so trace context can flow between MCP clients and
servers per the W3C Trace Context and Baggage specifications.
This follows the TypeScript SDK's approach (typescript-sdk#2270):
guarantee `_meta` passthrough, export constants for the reserved key names,
and lock the behavior in with regression tests and documentation, without adding
an OpenTelemetry dependency (the Python SDK's python-sdk#2381 makes
`opentelemetry-api` a hard dependency instead, which conflicts with
this SDK's keep-dependencies-minimal policy):
- New `MCP::TraceContext` module exposing `TRACEPARENT_META_KEY`,
`TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`.
- The server side already passes incoming request `_meta` to tool and prompt
handlers untouched via `server_context[:_meta]`; new regression tests pin
that guarantee for the three reserved keys.
- Every `MCP::Client` request method (`call_tool`, `read_resource`,
`get_prompt`, `complete`, `ping`, and the `list_*` methods) gains
a `meta:` keyword so Ruby clients can inject trace context (or any other
`_meta` entries) into outgoing requests; per SEP-414, trace context
should flow on every request, not only tool calls. The caller's hash is
never mutated. On `call_tool`, `progress_token` takes precedence over
a `progressToken` entry in `meta`, and an empty merge result omits `_meta`
entirely, preserving the existing wire format.
- README documents the convention and how to bridge the values to a tracing
system such as the `opentelemetry-ruby` gems.
Scope is intentionally client-to-server: server-initiated requests
(`sampling/createMessage`, `roots/list`, `elicitation/create`) do not accept
`meta:` yet; propagating trace context on those is a separate follow-up.
Resolves#374.
## How Has This Been Tested?
- New `test/mcp/trace_context_test.rb` pins the exact reserved key names and
the frozen `META_KEYS` list.
- New tests in `test/mcp/server_test.rb` assert that `tools/call` and
`prompts/get` deliver `traceparent`, `tracestate`, and `baggage` (alongside
`progressToken`) to handlers through `server_context[:_meta]` unchanged.
- New tests in `test/mcp/client_test.rb` cover the `meta:` keyword: the
trace keys are sent in `_meta` on `call_tool` and `read_resource`,
a table-driven test exercises the remaining request methods (`tools/list`,
`resources/list`, `resources/templates/list`, `prompts/list`,
`prompts/get`, `completion/complete`, `ping`), `progress_token` overrides
a `progressToken` entry in `meta` without duplicating the key,
the caller's `meta` hash is not mutated, and requests without `meta:`
serialize exactly as before (wire-format regressions for both the empty `meta`
hash and the no-params list request).
## Breaking Changes
None. `MCP::TraceContext` and the `meta:` keyword are purely additive, and
requests without `meta:` serialize exactly as before.
## Motivation and Context
The MCP specification defines the `ping` utility for verifying that the peer of
a connection is still responsive. The Ruby SDK server side already replies to
incoming `ping` requests with an empty result, but `MCP::Client` had no public API
to send a `ping` request. Users had to build the JSON-RPC payload themselves and
call the transport directly.
This adds a client-side counterpart to the existing server handler and aligns
the Ruby SDK in spirit with the Python and TypeScript SDKs, which expose
`ClientSession.send_ping` and `Client.ping` respectively. Those SDKs validate
the response envelope via type schemas (`EmptyResult` / `EmptyResultSchema`) and raise
their validation library's generic exception (Pydantic's `ValidationError` / Zod's `ZodError`).
The Ruby client applies a Hash type check on `result`
(matching Python's behavior of requiring a dict while allowing arbitrary contents)
and raises a new `MCP::Client::ValidationError` for malformed responses,
keeping `MCP::Client::ServerError` reserved for JSON-RPC error responses.
## How Has This Been Tested?
Client tests cover:
- request structure (method is `ping`, no `params`)
- empty result on success
- `ServerError` on JSON-RPC error response
- `ValidationError` on a response missing `result`
- `ValidationError` on a `result` of the wrong type (e.g., String)
- propagation of transport-level errors raised by `transport.send_request`
The existing server-side `ping` handler tests continue to pass unchanged.
## Breaking Changes
None. `MCP::Client#ping` and `MCP::Client::ValidationError` are purely additive.
The server-side `ping` handler behavior is unchanged.
## Motivation and Context
The MCP specification defines cursor-based pagination for list operations that
may return large result sets:
https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination
Pagination allows servers to yield results in smaller chunks rather than all at once,
which is especially important when connecting to external services over the internet.
The Ruby SDK previously returned complete arrays for all list endpoints
(`tools/list`, `resources/list`, `prompts/list`, `resources/templates/list`)
without pagination support. This adds cursor-based pagination.
### Server-side
A new `MCP::Server::Pagination` module is introduced (mixed into `MCP::Server`)
that provides the `paginate` helper for slicing a collection by cursor and
a `cursor_from` helper that extracts the cursor from the request params
while rejecting non-Hash inputs with `-32602 Invalid params` per the specification.
`MCP::Server.new` accepts a new `page_size:` keyword argument.
When `nil` (the default), all items are returned in a single response,
preserving the existing behavior. When set to a positive integer,
list responses include a `nextCursor` field if more pages are available.
`Server#page_size=` is a validating setter that rejects anything other than
`nil` or a positive `Integer`, raising `ArgumentError` on invalid inputs.
The constructor routes through the setter, so both `Server.new(page_size: 0)`
and `server.page_size = -1` raise.
Cursors are string tokens carrying a zero-based offset, treated as opaque by
clients. Cursor inputs are validated to be strings per the MCP spec;
invalid cursors (non-string, non-numeric, negative, or out-of-range) raise
`RequestHandlerError` with error code `-32602 (Invalid params)` per the spec.
### Client-side: two complementary APIs
The SDK exposes two API shapes for listing, each serving a different use case:
1. **Single-page with cursor** (`client.list_tools(cursor:)`,
`list_resources`, `list_resource_templates`, `list_prompts`): each call issues
one JSON-RPC request and returns a result object (`MCP::Client::ListToolsResult` etc.)
carrying the page items and an optional `next_cursor`.
These methods match the single-page-with-cursor convention used by the Python SDK
(`session.list_tools(params=PaginatedRequestParams(cursor=...))`)
and the TypeScript SDK (`client.listTools({ cursor })`). The method name and
the `cursor` parameter name are identical across the three SDKs,
so pagination code translates directly between languages.
2. **Whole-collection** (`client.tools`, `client.resources`, `client.resource_templates`,
`client.prompts`): auto-iterates through all pages and returns a plain array of items,
guaranteeing the full collection regardless of the server's `page_size` setting.
The auto-pagination loop tracks previously seen cursors in a set and exits if the server
revisits any of them, guarding against both immediate repeats and multi-cursor cycles
(e.g. `A -> B -> A`). This mirrors Rust SDK's `list_all_*` convenience methods,
though the Ruby names are preserved from the pre-pagination 0.13.0 API for backward compatibility.
Future rename to `list_all_*` is left as a TODO in the source.
Result classes are `Struct`s named to mirror Python SDK's `ListToolsResult` /
`ListPromptsResult` / etc. to align naming with other SDKs. Each struct exposes
its page items (e.g. `result.tools`), an optional `next_cursor` for continuation,
and an optional `meta` field mirroring the MCP `Result` type's `_meta` response field
so servers that decorate list responses do not lose metadata through the client.
### Ruby-specific ergonomics
`Server.new(page_size:)` and the `Pagination` module are Ruby-specific.
The Python and TypeScript SDKs do not expose a built-in `page_size` helper;
developers there re-implement cursor slicing inside each handler.
In Ruby, setting `page_size:` on the server is enough for the SDK to handle
the slicing across the four built-in list endpoints.
### README
A Pagination section documents both the server-side `page_size:` option and
the client-side iteration patterns, including an explicit note that each `list_*` call
is a single JSON-RPC round trip whose response size depends on the server's `page_size`,
and that `client.tools` / `.resources` / `.resource_templates` / `.prompts` return
the complete collection when needed.
## How Has This Been Tested?
- Added tests for the `Pagination` module (`test/mcp/server/pagination_test.rb`),
server-side pagination (`test/mcp/server_test.rb`), and client-side pagination
(`test/mcp/client_test.rb`).
- Added and existing tests pass. `rake rubocop` is clean and `rake conformance` passes.
## Breaking Changes
None. This is a new feature addition. Existing code continues to work unchanged:
`page_size` defaults to `nil` on the server (returns all items, no `nextCursor`),
and the client's existing `tools` / `resources` / `resource_templates` / `prompts` methods
continue to return complete arrays by transparently iterating through pages.
## Motivation and Context
The MCP specification defines `completion/complete` for providing autocompletion
suggestions for prompt arguments and resource template URIs.
The Ruby SDK previously had only a no-op handler that returned empty results,
with no way for users to define custom completion logic.
This aligns the Ruby SDK with the Python and TypeScript SDKs,
both of which support user-defined completion handlers.
## How Has This Been Tested?
Server tests cover: default handler, custom handler for `ref/prompt` and `ref/resource`,
context argument passing, 100-item truncation, and error responses for nonexistent prompts,
nonexistent resource templates, and invalid ref types.
Client tests cover: request structure, context parameter inclusion,
and fallback when result is missing.
## Breaking Changes
None. The existing default no-op handler behavior is preserved.
The `completion_handler` method is purely additive. The only behavioral change
is that `completion/complete` requests now validate that the referenced prompt
or resource template exists before calling the handler,
returning an `invalid_params` error for unknown references.
## Motivation and Context
Client methods (`tools`, `resources`, `resource_templates`, `prompts`, `read_resource`, `get_prompt`, `call_tool`)
silently swallowed JSON-RPC error responses by falling back to empty defaults. This did not conform to JSON-RPC 2.0,
which clearly distinguishes error responses from success responses.
Both the Python SDK (`MCPError`) and TypeScript SDK (`ProtocolError`) raise exceptions on error responses.
The Ruby SDK was the only one silently discarding them.
Introduces a common `request` method that handles JSON-RPC request construction and error checking in one place,
replacing the duplicated `transport.send_request` calls across all client methods.
## How Has This Been Tested?
Added tests for each affected method verifying that `ServerError` is raised with the correct `code`, `message`,
and `data` attributes.
## Breaking Changes
The previous behavior of silently returning empty defaults on server errors did not conform to JSON-RPC 2.0.
Error responses should be surfaced to callers, not swallowed.
Callers that relied on the silent fallback behavior will need to rescue `MCP::Client::ServerError`.
## Motivation and Context
`Client#call_tool` previously required an `MCP::Client::Tool` object to call a tool.
This made the API harder to use compared to other MCP SDK implementations,
which accept a tool name string directly:
- Python SDK: `session.call_tool(name, arguments)` accepts a name string.
https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L370
- TypeScript SDK: `client.callTool({ name, arguments })` accepts a name string.
ccb78f2/packages/client/src/client/client.ts (L834)
## How Has This Been Tested?
Updated existing tests and added new tests.
## Breaking Change
No. This change adds a `name:` parameter so callers can pass a tool name string
directly without looking up a `Tool` object first. The existing `tool:` parameter
continues to work for backward compatibility.
## Motivation and Context
The MCP specification defines `notifications/progress` for reporting incremental progress
during long-running tool operations. The Ruby SDK did not yet support this, so tools had no way to
send progress updates to clients. This caused the `tools-call-with-progress` conformance test to fail.
This adds an `MCP::ServerContext` wrapper that provides a `report_progress` method on
`server_context`. Tools call `server_context.report_progress(value, total:, message:)` to send
notifications back to the client. The method is a no-op when no `progressToken` was provided,
so tools can call it unconditionally without checking for a token.
This design is consistent with the TypeScript and Python SDKs, which provide progress
reporting through a context object rather than a separate parameter.
On the client side, `Client#call_tool` now accepts a `progress_token:` parameter that sets
`_meta.progressToken` in the request.
Ref: https://modelcontextprotocol.io/specification/latest/server/utilities/progress
## How Has This Been Tested?
Added comprehensive tests in `test/mcp/server_progress_test.rb` covering `notify_progress`,
tool integration with progress, block-defined tools, `**kwargs` tools, and no-op behavior
without a token.
Added client tests for `progress_token:` parameter inclusion and omission.
Added `test/mcp/server_context_test.rb` test for non-Hash `server_context` preservation
when `_meta` is present.
Conformance: `tools-call-with-progress` removed from expected failures.
## Breaking Change
`server_context` is now always an `MCP::ServerContext` instance when passed to tools,
even if the server has no context configured. Code that checks `server_context ?` to
detect a nil context will need to be updated, as `ServerContext` is always truthy.
`ServerContext` delegates method calls to the underlying context object via
`method_missing`, so `server_context[:key]` and other accessor patterns continue to work.
In practice, this is unlikely to affect users. A tool that declares `server_context:`
is opting in to use the context; if the server has no context configured, that is a
server configuration issue rather than something the tool should handle with a nil check.
Due to overly aggressive detection by RuboCop Minitest, the distinction between `[]` and `{}` was uniformly replaced with `assert_empty`,
making it impossible to differentiate between them.
As a result of https://github.com/rubocop/rubocop-minitest/issues/344, where the `Minitest/AssertEmptyLiteral` cop was disabled,
the return value has been expressed using explicit literal forms.
## Summary
Unlike applications, libraries provide value by remaining usable for a broad range of users,
even when that means supporting older Ruby versions beyond Ruby's own EOL timeline.
I maintain RuboCop, a Ruby linter. Because linters need to work across the ecosystem,
they often need to keep compatibility with older Ruby versions to some extent.
The trade-off is that the MCP Ruby SDK must remain compatible with Ruby 2.7.
However, given the ecosystem impact, supporting Ruby 2.7 is still worthwhile.
For example, bundled gems such as `csv` (Ruby >= 2.5), `bigdecimal` (Ruby >= 2.5), `json` (Ruby >= 2.7),
and `language_server-protocol` (Ruby >= 2.5) tend to keep compatibility with older Ruby versions.
Unlike dropping support, which imposes restrictions on users,
this change broadens compatibility and does not introduce breaking changes.
## Development Note
`rubocop-shopify` (2.18) supports Ruby 3.1+, and this PR does not change that.
Since RuboCop can configure the runtime Ruby version and the target Ruby version for analysis independently,
CI continues to run RuboCop on Ruby 4.0, while analyzing code with `TargetRubyVersion: 2.7`.
When setting `TargetRubyVersion: 2.7`, several RuboCop offenses were resolved.
Additionally, Sorbet-related libraries support Ruby 3.0+. Since these are not dependencies of the MCP Ruby SDK itself,
they are versioned and tested in the development Gemfile.
Update test_resources_returns_empty_array_when_no_resources to use
correct mock response format with empty resources array instead of
empty result object. Also reorder test to group with other resources
tests.
Follow up: https://github.com/modelcontextprotocol/ruby-sdk/pull/160