Commit graph ruby-sdk/conformance
Author SHA1 Message Date
Koichi ITO
cf27c5dca6
Merge pull request #399 from koic/client_credentials_basic
Support OAuth `client_credentials` grant in OAuth client
2026-06-13 20:06:29 +09:00
Koichi ITO
2289db84f8 Support OAuth client_credentials grant in OAuth client
## Motivation and Context

The MCP authorization specification and SEP-1046 allow confidential clients to
obtain tokens through the OAuth 2.1 `client_credentials` grant: a machine-to-machine flow
with no user, no browser redirect, and no authorization code. The Ruby SDK
previously supported only the interactive authorization-code flow, so it could not
authenticate against a server that requires `client_credentials`. This makes
the `auth/client-credentials-basic` conformance scenario pass and is removed from
the expected failures list.

Following the TypeScript SDK (`ClientCredentialsProvider`) and the Python SDK
(`ClientCredentialsOAuthProvider`), the grant gets its own provider class rather than
overloading the authorization-code provider. The change adds:

- `MCP::Client::OAuth::ClientCredentialsProvider`, a dedicated provider
  for the grant. `client_id` and `client_secret` are mandatory
  (the grant is for confidential clients), `token_endpoint_auth_method` is
  `client_secret_basic` (default) or `client_secret_post` -- `none` is
  rejected because an unauthenticated `client_credentials` request is
  meaningless. It has no redirect arguments, so a credentials-only
  client never supplies the redirect URI / handlers that grant has no use for.
- `MCP::Client::OAuth::StorageBackedProvider`, an internal module that
  shares the token / client-information persistence between the two provider classes
  so `Flow` can treat any provider uniformly.
- `Provider#authorization_flow` (`:authorization_code`) and
  `ClientCredentialsProvider#authorization_flow` (`:client_credentials`).
  `Flow#run!` dispatches on this rather than inspecting
  `client_metadata[:grant_types]`, which is protocol metadata for
  the authorization server, not an SDK control signal.
- `run_client_credentials!` shares the same PRM / AS metadata discovery
  and Communication Security checks as the interactive flow, then
  exchanges `grant_type=client_credentials` at the token endpoint via
  the existing `post_to_token_endpoint` (which already applies
  `client_secret_basic`). There is no PKCE, redirect, or `offline_access`
  augmentation, because the grant does not issue a refresh token
  (OAuth 2.1 Section 4.3.3).
- `conformance/client.rb` builds a `ClientCredentialsProvider` for
  the `auth/client-credentials-*` scenarios, reusing the harness-injected
  `client_id` / `client_secret`.

## How Has This Been Tested?

New `ClientCredentialsProvider` tests cover: credentials are stored as
client information; `authorization_flow` is `:client_credentials`;
`client_secret_post` is accepted; missing `client_id` / `client_secret`
and `token_endpoint_auth_method: "none"` are rejected; and the token helpers
delegate to storage.

New `Flow` tests cover: a `client_credentials` provider exchanges
`grant_type=client_credentials` with HTTP Basic auth, saves the token,
and never contacts the authorization or registration endpoint;
the requested scope is taken from PRM `scopes_supported`; and a standard
`Provider` runs the authorization-code grant regardless of what its
`client_metadata[:grant_types]` lists. A `Provider` test asserts
`authorization_flow` is `:authorization_code`.

Conformance: `auth/client-credentials-basic` now passes 7/7 with no failures.

## Breaking Changes

None. `ClientCredentialsProvider` is a new class; the existing `Provider` keeps
its required `redirect_uri` / `redirect_handler` / `callback_handler` arguments
and its behaviour is unchanged. Existing interactive clients are unaffected.
2026-06-13 00:12:19 +09:00
Koichi ITO
a407cea086 Re-run OAuth flow on 403 insufficient_scope (step-up)
## Motivation and Context

The MCP authorization specification (2025-11-25 scope-selection-strategy)
and RFC 6750 Section 3.1 describe OAuth 2.0 step-up authentication:
the client already holds a valid access token, but the server returns a 403
with `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."`
to demand a higher-privileged scope for a specific operation.
The client is expected to run a fresh authorization request with
the escalated scope and retry.

Until now the Ruby SDK only ran the OAuth flow on 401 responses.
A 403 surfaced unchanged as a `RequestHandlerError` and the operation
that needed the escalated scope failed permanently, even with a valid token
in storage. This lined up with the `auth/scope-step-up` conformance
scenario being listed in `conformance/expected_failures.yml`.

The change adds:

- `MCP::Client::HTTP#send_request` recognises a 403 carrying
  an `insufficient_scope` Bearer challenge as a step-up signal,
  re-runs a full authorization request with the scope from the challenge,
  and retries the original request. Retries are capped at
  one per `send_request` call so a server that repeatedly demands more
  scope cannot loop the transport.
- `run_step_up_flow!` deliberately bypasses the refresh-token path that
  `run_oauth_flow!` uses on 401. Refreshing would re-issue the same
  scope set the existing token already has and the server already
  rejected. Only a fresh `authorization_code` grant with the escalated
  scope can succeed.
- A plain 403 with no `insufficient_scope` challenge continues to
  surface as `RequestHandlerError` with `error_type: :forbidden`.
- `conformance/client.rb` invokes a tool for the `auth/scope-step-up`
  scenario; the scenario's authorization middleware only requires
  the escalated scope on `tools/call`, so the previous `tools/list`-only
  client could never trigger the second authorization.
- `auth/scope-step-up` is removed from `conformance/expected_failures.yml`.

Resolves #383.

## How Has This Been Tested?

New `HTTPOAuthTest` cases cover: the 403 insufficient_scope path
re-authorizes with the escalated scope from the challenge and retries
the original request with the new bearer token; a plain 403 without
the `insufficient_scope` challenge is surfaced as
`error_type: :forbidden` with no OAuth side effects; repeated
`insufficient_scope` responses retry at most once and then surface the 403;
a provider with a saved `refresh_token` still goes through
the full authorization request rather than exchanging the refresh token,
because the existing scope set is precisely what was rejected.

Conformance: `auth/scope-step-up` now passes 22/22 with no warnings.
`bundle exec rake test`, `bundle exec rake rubocop`, and
`bundle exec rake conformance` are all green.

## Breaking Changes

None. Behaviour on 401 is unchanged. A 403 without the `insufficient_scope`
Bearer challenge still raises the same `RequestHandlerError` as before.
The new retry only fires when both the WWW-Authenticate header signals step-up
and the transport was constructed with an OAuth provider.
2026-06-12 11:59:02 +09:00
Koichi ITO
cf44475ce9
Merge pull request #365 from koic/conformance_client_auth_offline_access_scope
Request offline_access scope when supported (SEP-2207)
2026-06-01 01:09:01 +09:00
Koichi ITO
fdca6f5a2f Request offline_access scope when supported (SEP-2207)
## Motivation and Context

SEP-2207 describes how an OAuth client obtains refresh tokens: it
declares the `refresh_token` grant type in its client metadata and
requests the `offline_access` scope, but only when the authorization
server advertises `offline_access` in its metadata `scopes_supported`.
Requesting `offline_access` when the server does not list it is forbidden.

The Ruby SDK already supports the refresh grant (`Flow#refresh!`) but never
requested `offline_access`, so it could not obtain a refresh token from a server
that gates it behind that scope. This aligns the SDK with the Python and TypeScript SDKs
and makes the `auth/offline-access-scope` conformance scenario pass
(previously it reported one warning, `sep-2207-client-metadata-grant-types`,
and was listed in the expected-failures baseline).

The change adds:

- `Flow#augment_scope_with_offline_access`, called from `run!` after
  `resolve_scope`, appends `offline_access` to the requested scope when
  both the client opted into refresh tokens (its registered
  `grant_types` include `refresh_token`) and the authorization server
  advertises `offline_access` in its metadata `scopes_supported`.
  Already-present `offline_access` is not duplicated. Gating on the server
  advertisement keeps the SDK from ever requesting the scope where it is not supported.
- `conformance/client.rb` declares `refresh_token` in `grant_types`, an honest reflection
  of the SDK's refresh support, which clears the SEP-2207 grant-types warning.
- `auth/offline-access-scope` is removed from `conformance/expected_failures.yml`.

## How Has This Been Tested?

New `Flow` tests cover: `offline_access` is requested when the server advertises it
and the client declared the `refresh_token` grant; `offline_access` is NOT requested
when the client did not declare the grant; `offline_access` is NOT requested
when the server does not advertise it (even with the grant declared);
and `offline_access` is not duplicated when it is already part of the resolved scope.

Conformance: `auth/offline-access-scope` now passes 14/14 with no warnings,
and `auth/offline-access-not-supported` still passes 13/13 (no regression).
`bundle exec rake test`, `bundle exec rake rubocop`, and `bundle exec rake conformance`
are all green.

## Breaking Changes

None. `offline_access` is appended only when the client's own `grant_types` opt
into refresh tokens and the authorization server advertises the scope, so existing clients
that do neither see no change in the scope they request.
2026-05-29 13:14:14 +09:00
Koichi ITO
355c904899 Support Client ID Metadata Documents in OAuth client
## Motivation and Context

The MCP 2025-11-25 authorization specification and `draft-ietf-oauth-client-id-metadata-document-00` define
Client ID Metadata Documents (CIMD): a client publishes its OAuth metadata at an HTTPS URL and uses that URL
as the OAuth `client_id` instead of going through Dynamic Client Registration. The Python and TypeScript SDKs
already implement this; the Ruby SDK now reaches parity.

The change adds:

- `MCP::Client::OAuth::Provider` accepts an optional `client_id_metadata_document_url:` keyword.
- A new `Discovery.client_id_metadata_document_url?` validator enforces structural requirements.
  Spec-required: `https` scheme, a non-root path, and no fragment, userinfo, or `.`/`..` segments
  (including the percent-encoded form `%2E`). Stricter than the draft as an SDK policy: query strings
  are also rejected, because different encodings of the same query would yield distinct `client_id` strings for
  the same logical document. The loopback `http` carve-out used for discovery URLs does not extend to CIMD URLs,
  since the value is sent verbatim to the authorization server as the OAuth `client_id` and travels off-loopback.
  Validation failures raise `Provider::InvalidClientIDMetadataDocumentURLError`.
- `Flow#ensure_client_registered` routes through CIMD only when the authorization server advertises
  `client_id_metadata_document_supported` as JSON `boolean true`; truthy non-boolean values
  (string `"true"`, `"false"`, etc.) fall back to DCR, matching the gating in the Python and TypeScript SDKs.
- The CIMD `client_id` is NOT persisted to storage. AS support is re-read on every flow so a server
  that later drops CIMD support stops receiving the stale URL. Pre-registered `client_information`
  continues to win over CIMD when both are available.
- `Flow#refresh!` reconstructs the CIMD `client_id` from live AS metadata when refreshing tokens obtained via CIMD.
  When support has been withdrawn and no other identity is stored, it raises a clear `AuthorizationError` rather than
  sending a `client_id` the authorization server no longer recognises.
- The Provider docstring and README OAuth section explain that DCR `client_metadata` and the JSON document served at
  the CIMD URL are separate artifacts: DCR metadata MUST NOT include `client_id`, while the CIMD document MUST include
  `client_id` set to the URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
- `auth/basic-cimd` is removed from `conformance/expected_failures.yml`.

## How Has This Been Tested?

`Discovery` tests cover: https with path accepted; non-https rejected (http, ftp, file); root and empty path rejected;
fragment, query, and userinfo rejected; dot segments rejected including the percent-encoded form `%2E`; nil, empty,
and malformed inputs rejected.

`Provider` tests cover: the default `nil`; rejection of http URLs (loopback included); non-https schemes;
root and empty path; fragment; query; userinfo; and dot segments.

`Flow#run!` tests cover: skip DCR when the authorization server advertises CIMD and the provider has a URL;
fall back to DCR when the authorization server does not advertise support; fall back to DCR when the value is
the string `"false"` or `"true"`; pre-registered `client_information` wins over CIMD; the CIMD `client_id` is not
persisted to storage; a second flow against an authorization server that has dropped CIMD support recovers via DCR.

`Flow#refresh!` tests cover: refresh reconstructs the CIMD `client_id` without a stored `client_information`;
refresh raises when the authorization server no longer advertises CIMD support; stored `client_information`
takes precedence over the CIMD URL during refresh.

Conformance: `auth/basic-cimd` now passes 12/12 with no warnings. `bundle exec rake test`, `bundle exec rake rubocop`,
and `bundle exec rake conformance` are all green.

## Breaking Changes

None. The `client_id_metadata_document_url:` keyword is purely opt-in and defaults to `nil`.
When the keyword is omitted, the DCR flow is unchanged. Existing OAuth users see no behaviour change.
The CIMD path is only entered when both the user explicitly configures a URL and the authorization server
advertises support via `client_id_metadata_document_supported: true`.
2026-05-26 18:56:03 +09:00
Koichi ITO
5766069923 Add OAuth 2.1 client support for MCP authorization flow
## Motivation and Context

The MCP Authorization spec (2025-11-25) requires clients to support
Protected Resource Metadata discovery (RFC 9728), Authorization
Server Metadata discovery (RFC 8414), Dynamic Client Registration,
and OAuth 2.1 Authorization Code grant with PKCE (S256). The Ruby SDK
previously had no client-side OAuth support: the HTTP transport raised
`RequestHandlerError` on a 401 with no recovery, and 21 `auth/*` scenarios
were listed as known failures in `conformance/expected_failures.yml`.

This aligns the Ruby SDK with the Python and TypeScript SDKs,
both of which expose a pluggable OAuth client provider that drives
the full discovery + DCR + authorization + token exchange flow.
`MCP::Client::OAuth::Provider` mirrors that surface: callers supply
`client_metadata`, a `redirect_uri`, and pluggable `redirect_handler`
/ `callback_handler` hooks plus a token/client information storage.
`MCP::Client::OAuth::InMemoryStorage` is provided as a default.

`MCP::Client::HTTP` accepts a new optional `oauth:` keyword. When present,
the transport attaches `Authorization: Bearer` to every request,
and on a 401 parses `WWW-Authenticate` for the `resource_metadata` URL
and `scope` challenge, drives the discovery + DCR + authorization code flow
through the provider, and retries the failed request once with the acquired token.

Spec compliance details:

- The `WWW-Authenticate` parser handles multiple challenges
  (e.g. `Basic ..., Bearer ...` or `Bearer ..., DPoP ...`), extracting only
  the parameters that belong to the Bearer challenge.
- The PRM `resource` value is required to cover the MCP server URL
  (same scheme/host/port, with the PRM path as a prefix of the server path)
  before the flow continues, preventing redirection of issued tokens to a different audience.
- Both authorization and token requests always include a `resource`
  parameter (RFC 8707), defaulting to a canonicalized form of
  the MCP server URL when PRM does not advertise one.
- `client_secret_basic`, `client_secret_post`, and `none` token endpoint
  authentication methods are supported. When pre-registered client information
  specifies no `token_endpoint_auth_method`, the transport defaults to
  `client_secret_basic` per RFC 6749 §2.3.1 an dthe TS/Python SDK convention.
  Pre-registered client credentials supplied via `Storage#save_client_information`
  with either string or symbol keys are honored, skipping DCR.
- Scope resolution follows the spec order: the `WWW-Authenticate` challenge scope wins first,
  then PRM `scopes_supported`, and `Provider#scope` is the last-resort fallback only
  if neither is supplied (the provider-supplied scope must not pre-empt server-advertised scope).
- Token endpoint error responses (RFC 6749 §5.2) are parsed: a 4x with
  `error: "invalid_grant"` raises `Flow::InvalidGrantError`,
  so `MCP::Client::HTTP` can discard the stored refresh token only when
  the AS truly says it is dead. Generic `AuthorizationError`
  (5xx, network failures, other error codes) leaves the refresh token
  intact, so a transient AS outage does not force interactive reauth
  on the next attempt.
- The OAuth `state` parameter is compared via
  `OpenSSL.fixed_length_secure_compare` (with a `bytesize` guard) to
  prevent timing-based discovery of the expected value.
- The 401 retry guard is a request-scoped local variable, so an MCP server
  that keeps returning 401 is surfaced as an error after one
  retry rather than triggering an infinite re-authorization loop.

## How Has This Been Tested?

Unit tests cover PKCE generation, multi-challenge `WWW-Authenticate` parsing,
PRM and Authorization Server metadata URL builders, the full Flow orchestrator
(success path, state mismatch, missing PKCE support, skipping DCR for both string
and symbol-keyed preregistered client information, `client_secret_basic`
token endpoint authentication, PRM `resource` mismatch rejection, fallback to
the server URL when PRM omits `resource`, scope resolution preferring PRM
`scopes_supported` over `Provider#scope`, default `client_secret_basic` for
confidential clients without an explicit `token_endpoint_auth_method`,
and the new `InvalidGrantError` vs generic `AuthorizationError` split for
token endpoint rejections), and the HTTP transport behavior
(401 recovery + retry, no-provider fallthrough, no infinite loop on repeated 401,
no infinite loop on flow failure, refresh-token retention on transient 5xx from
the token endpoint).

Conformance: `bundle exec rake conformance` newly passes 16 `auth/*`
scenarios beyond the existing baseline, including
`auth/metadata-default`, `metadata-var{1,2,3}`,
`scope-from-www-authenticate`, `scope-from-scopes-supported`,
`scope-omitted-when-undefined`, `scope-retry-limit`,
`token-endpoint-auth-{basic,post,none}`, `pre-registration`,
`resource-mismatch`, and `offline-access-not-supported`. Remaining
`auth/*` scenarios stay in `expected_failures.yml`.
`bundle exec rake test rubocop` is clean.

## Breaking Changes

None. `MCP::Client::HTTP#initialize` accepts a new optional `oauth:`
keyword that defaults to `nil`, preserving existing behavior. When unset,
the transport raises `MCP::Client::RequestHandlerError` on a 401 exactly as before.
2026-05-17 00:48:41 +09:00
Koichi ITO
9603b2fe93 Support running MCP client conformance tests against the Ruby SDK
## Motivation and Context

PR #248 introduced server conformance testing using `@modelcontextprotocol/conformance`.
This extends coverage to the client side so that `MCP::Client` behavior can also be validated against
the MCP specification.

### Scope

Client conformance is driven by the conformance runner, which spawns test servers for each scenario
and invokes `conformance/client.rb` against them. The client script delegates the MCP initialize
handshake to `MCP::Client#connect` and executes scenario-specific operations (for example listing
tools or calling a tool).

### Usage

Run all conformance tests (server + client):

```console
bundle exec rake conformance
```

Run a single client scenario:

```console
bundle exec rake conformance SCENARIO=initialize
```

List available scenarios:

```console
bundle exec rake conformance_list
```

## How Has This Been Tested?

Ran `bundle exec rake conformance` locally.

- Server conformance: 40 passed, 0 failed.
- Client conformance: 3 passed, 54 failed (all 54 listed in `expected_failures.yml`).

## Breaking Change

No. This change only adds test infrastructure under `conformance/` and does not modify SDK behavior.
2026-05-08 15:11:28 +09:00
Koichi ITO
d80fba4fbc Support resource subscriptions per MCP specification
## Motivation and Context

The MCP specification defines `resources/subscribe`, `resources/unsubscribe`,
and `notifications/resources/updated` for clients to monitor resource changes:
https://modelcontextprotocol.io/specification/2025-03-26/server/resources#subscriptions

The Ruby SDK had stub (no-op) handlers but provided no way for server developers to
customize subscription behavior or send update notifications.

Following the Python SDK approach, the SDK does not track subscription state internally.
Server developers register handler blocks and manage their own subscription state,
allowing flexibility for different subscription semantics
(per-session tracking, persistence, debouncing, etc.).

Three methods are added:

- `Server#resources_subscribe_handler`: registers a handler for
  `resources/subscribe` requests
- `Server#resources_unsubscribe_handler`: registers a handler for
  `resources/unsubscribe` requests
- `ServerSession#notify_resources_updated`: sends a
  `notifications/resources/updated` notification to the subscribing client

`ServerContext#notify_resources_updated` is also added so that tool handlers
can send the notification scoped to the originating session.

## How Has This Been Tested?

All tests pass (`rake test`), RuboCop is clean. New tests cover custom handler registration
for `resources/subscribe` and `resources/unsubscribe`, session-scoped `notify_resources_updated` notifications,
error handling, and `ServerContext` delegation.

## Breaking Changes

None.
2026-04-19 03:48:46 +09:00
Koichi ITO
985fd03ecb Support elicitation/create per MCP specification
## Motivation and Context

The MCP specification (2025-11-25) defines elicitation as a server-to-client
request mechanism that allows servers to request user input during tool execution:
https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation

This aligns with the Python SDK, which also exposes separate methods for form and URL mode elicitation.
Three methods are added following the Python SDK's API design:

- `create_form_elicitation`: collects structured data via JSON Schema
- `create_url_elicitation`: directs users to external URLs for sensitive
  interactions (OAuth, credentials, etc.)
- `notify_elicitation_complete`: notifies the client that a URL mode
  interaction has completed

`URLElicitationRequiredError` (error code `-32042`) is defined as a subclass of `RequestHandlerError`
with its own `error_code` and `error_data` attributes. `JsonRpcHandler#handle_request_error` is
extended to check for these attributes so that MCP-specific error codes can be propagated without
embedding MCP knowledge in the generic JSON-RPC layer.

## How Has This Been Tested?

- Unit tests for `ServerSession` form/URL mode, capability checks, nil
  capabilities, notification, and `URLElicitationRequiredError`
- `ServerContext` delegation tests for all three elicitation methods
- `JsonRpcHandler` tests for custom error code and fallback behavior
- `Methods` capability test for `NOTIFICATIONS_ELICITATION_COMPLETE`
- Conformance tests pass (40/40), including `tools-call-elicitation`,
  `elicitation-sep1034-defaults`, and `elicitation-sep1330-enums`
- All tests pass (`rake test`), RuboCop is clean
- Verified form mode elicitation end-to-end with a sample MCP server
  running in Claude Code

## Breaking Changes

None. This is a new feature addition.
2026-04-13 15:21:45 +09:00
Koichi ITO
de38804ed7 Auto-set server.transport in Transport#initialize
## Motivation and Context

Transport subclasses (StreamableHTTPTransport, StdioTransport) already
receive the server in their constructor, but users must manually call
`server.transport = transport` after creating a transport instance.
This is boilerplate that can be eliminated by setting the connection
automatically in `Transport#initialize`.

## How Has This Been Tested?

All existing tests pass. Redundant `server.transport = transport` lines
were removed from tests, examples, README.md, and conformance server.

## Breaking Changes

None. Existing code that manually sets `server.transport = transport`
continues to work (idempotent assignment).
2026-04-09 12:08:55 +09:00
Koichi ITO
da5f9225ac Support sampling/createMessage per MCP specification
## Motivation and Context

The MCP specification defines `sampling/createMessage` to request LLM completions from the client.
The Ruby SDK did not yet support server-to-client requests,
so the server had no way to ask the client to sample an LLM.
This caused the `tools-call-sampling` conformance test to fail.

This adds `Server#create_sampling_message` to send a `sampling/createMessage` request
to the client via the transport layer. The method validates that the client declared
the `sampling` capability during initialization, and optionally checks the
`sampling.tools` capability when tools or tool_choice parameters are provided.

On the transport side, `Transport#send_request` is introduced as an abstract method,
with implementations in both `StdioTransport` and `StreamableHTTPTransport`.
Each transport assigns a unique request ID, sends the JSON-RPC request to the client,
and blocks until the client responds.

Client capabilities are stored per session via `ServerSession`, so validation is scoped
to the originating client. `ServerSession#create_sampling_message` uses
`build_sampling_params` for capability validation and sends the request directly through
the transport with the session ID, following the same pattern as `notify_progress`.

`Server#create_sampling_message` is the public API for single-client transports
(e.g., StdioTransport).

For multi-client transports (e.g., StreamableHTTPTransport),
`ServerSession#create_sampling_message` must be used to route requests to the correct client.
Tools using `server_context.create_sampling_message` automatically route to the correct session
regardless of transport.

`StreamableHTTPTransport#send_request` requires `session_id` to prevent broadcasting sampling requests
to all connected clients. Missing or invalid session IDs raise explicit errors instead of silently failing.

The `include_context` parameter is soft-deprecated in the MCP specification but is included
for compatibility with existing clients that declare the `sampling.context` capability.
Python and TypeScript SDKs also retain this parameter.

Ref: https://modelcontextprotocol.io/specification/latest/server/utilities/sampling

## How Has This Been Tested?

Added comprehensive tests in `test/mcp/server_sampling_test.rb` covering required and optional parameters,
capability validation, error handling, nil-param omission, per-session capability isolation,
and HTTP init capability scoping.

Added transport tests in `test/mcp/server/transports/stdio_transport_test.rb` and
`test/mcp/server/transports/streamable_http_transport_test.rb` for `send_request` round-trip behavior,
error responses, stateless mode rejection, missing session_id, invalid session_id, and no-active-stream error.

Conformance: `tools-call-sampling` is removed from expected failures. The conformance server's `TestSampling`
tool now calls `server_context.create_sampling_message` instead of returning a stub error.

This aligns the Ruby SDK with the MCP specification and other SDK implementations.

## Breaking Changes

None for end users.
`Transport#send_request` is a new abstract method. `ServerSession#create_sampling_message` is a new method.
2026-04-04 05:55:27 +09:00
Koichi ITO
88e1f9a840 Support completion/complete per MCP specification
## 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.
2026-04-03 01:35:07 +09:00
Koichi ITO
bbe637612b Support progress notifications per MCP specification
## 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.
2026-03-10 01:49:09 +09:00
Koichi ITO
13bbd2937d Support running MCP conformance tests against the Ruby SDK
## Motivation

To assess and track the Ruby SDK's compliance with the MCP
specification, integrate `@modelcontextprotocol/conformance` as the
standard evaluation harness. This makes it possible to measure
conformance objectively, catch regressions early, and produce the kind
of evaluation data needed for SDK tier assessments.

## Scope

Only server conformance is included at this point. Client conformance
requires protocol-level features that MCP::Client does not yet
support (initialize handshake, notifications, ping, etc.).

## Usage

Run conformance tests:

```console
bundle exec rake conformance
```

Start the conformance server in the foreground for iterating on a single
scenario without the per-run startup overhead:

```console
bundle exec rake conformance_server
```

## Default Rake Task (`bundle exec rake`)

It is configured to run in order of lower execution cost: `rubocop`, `test`, and then `conformance`.

## CI

`.github/workflows/conformance.yml` file has been added to run the conformance tests in CI.

## SDK Tier Report

The MCP SDK Tier system (https://modelcontextprotocol.io/community/sdk-tiers)
requires SDK maintainers to self-assess and report results to the SDK Working
Group via https://github.com/modelcontextprotocol/modelcontextprotocol/issues.

To generate a full tier assessment report, use the `/mcp-sdk-tier-audit`
slash command from the https://github.com/modelcontextprotocol/conformance repository with
the conformance server running:

```console
# Terminal 1 (this repository): start the conformance server
bundle exec rake conformance_server

# Terminal 2 (conformance repository): run the tier audit skill as a slash command in Claude Code
/mcp-sdk-tier-audit /path/to/modelcontextprotocol/ruby-sdk http://localhost:9292/mcp
```

The skill evaluates conformance pass rate, issue label taxonomy, triage
metrics, documentation coverage, and policy compliance, then produces a
markdown report suitable for tier advancement submissions.
2026-03-02 14:50:03 +09:00