Commit graph ruby-sdk/lib
Author SHA1 Message Date
Koichi ITO
aee8a0bfb6
Merge pull request #358 from koic/ping_server_to_client
Support server-to-client `ping` per MCP specification
2026-05-23 02:26:45 +09:00
Koichi ITO
ac7598a13d Support server-to-client ping per MCP specification
## Motivation and Context

The MCP specification defines `ping` as a bidirectional utility: either party may issue it.
https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping

The previous PR added `MCP::Client#ping`. This PR adds the symmetric server-side send capability,
`ServerSession#ping`.

This aligns the Ruby SDK with the Python SDK (`ServerSession.send_ping`) and
the TypeScript SDK (`Server.ping()`), both of which expose a server-initiated ping.
In practice this enables a Ruby MCP server to health-check non-Ruby clients
(e.g., Claude Desktop, the Python or TypeScript SDK client) during long-running tool operations.

Scope is intentionally server-only: this PR does not add client-side inbound request dispatch,
which is a larger refactor outside the ping utility. A Ruby `MCP::Client` therefore still cannot receive
a server-initiated ping; that gap is tracked as a separate follow-up.

## How Has This Been Tested?

New file `test/mcp/server_session_ping_test.rb` covers:

- success (empty `result` returned)
- `related_request_id` propagated through the transport when a session ID is set
- `ValidationError` when the transport returns `nil`
- `ValidationError` when the transport returns a non-Hash `result`
- transport-level errors propagate unchanged
- ping succeeds without any client capability declaration (per spec, ping has no capability gate)

`test/mcp/server_context_test.rb` adds two tests for `ServerContext#ping` delegation
(happy path and `NoMethodError` when the session lacks `#ping`).

The existing server-side `ping` handler tests continue to pass unchanged.

## Breaking Changes

None. `MCP::ServerContext#ping` and `MCP::Server::ValidationError` are purely additive.
2026-05-22 11:28:51 +09:00
Koichi ITO
999f8b3f39 Refactor handle_delete to use extract_session_id
## Motivation and Context

`handle_post` and `handle_get` already use this helper, but
`handle_delete` inlined `request.env["HTTP_MCP_SESSION_ID"]`.
This aligns all three handlers consistently.

## How Has This Been Tested?

It has passed existing tests.

## Breaking Changes

None.
2026-05-21 15:42:51 +09:00
Koichi ITO
2efd0c32e1 Release 0.17.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.
2026-05-20 12:31:59 +09:00
Koichi ITO
2832bc0984
Merge pull request #353 from koic/add_oauth_2_1_client_support_for_mcp_authorization_flow
Add OAuth 2.1 client support for MCP authorization flow
2026-05-19 11:45:52 +09:00
Koichi ITO
7db22f4d5b
Reject duplicate initialize requests (#350)
## Motivation and Context

The Ruby SDK accepted duplicate `initialize` requests after a session was already initialized.
On stdio, a second `initialize` silently overwrote the per-session `clientInfo` and `clientCapabilities`
(including with an older `protocolVersion`). On Streamable HTTP, every `initialize` minted a fresh
`Mcp-Session-Id` and `ServerSession`, abandoning the originally negotiated session.

MCP specification (`2025-06-18` / `2025-11-25` lifecycle) states that the initialization phase
MUST be the first interaction between client and server;
re-initialization on an established session is not part of the defined lifecycle.
TypeScript SDK rejects duplicate `initialize` on a live session with HTTP 400 + JSON-RPC `-32600`
("Invalid Request: Server already initialized"), and Python SDK does not mint a new session
on duplicate `initialize`. The Ruby SDK was the outlier; this change aligns it with the TypeScript SDK.

- `ServerSession` tracks an `@initialized` flag, exposed via `initialized?` and
  set by `mark_initialized!` after a successful `initialize` response.
- `Server#init` raises `RequestHandlerError(error_type: :invalid_request)` when
  the session is already initialized, which the existing error mapping converts
  to JSON-RPC `-32600 Invalid Request`.
- `StreamableHTTPTransport#handle_post` short-circuits at the transport layer:
  duplicate `initialize` against a live session returns HTTP 400 + JSON-RPC
  `-32600`; a stale or expired `Mcp-Session-Id` returns 404 (evicting the expired entry
  instead of misreporting it as a duplicate).
- `handle_initialization` evicts the registered session and omits the `Mcp-Session-Id`
  header when the first `initialize` fails before `mark_initialized!` is reached,
  so retries do not collide with an orphaned ID.
- Non-Hash JSON-RPC POST bodies (e.g. batched arrays, which are not supported in `2025-11-25`)
  are explicitly rejected with HTTP 400 + JSON-RPC `-32600` rather than falling through to
  an unparseable Rack response.

## How Has This Been Tested?

- Server tests: a second `initialize` on the same `ServerSession` returns
  `code: -32600` and the original `clientInfo` is preserved.
- Streamable HTTP tests: duplicate `initialize` with a live `Mcp-Session-Id`
  returns HTTP 400 + `-32600` and the original session remains usable for
  subsequent `ping`; stale `Mcp-Session-Id` returns 404; an idle-expired
  session is evicted on duplicate `initialize` and returns 404; a failed
  `initialize` (invalid `jsonrpc` envelope) does not leak `Mcp-Session-Id` and
  leaves `@sessions` empty; an array body is rejected with HTTP 400 + `-32600`.
- Stdio tests: a second `initialize` on the same stdio session returns
  `code: -32600` and the original `clientInfo` is preserved.

## Breaking Changes

Clients that previously sent `initialize` more than once on the same session
now receive a JSON-RPC error with `code: -32600` for the second and later requests
instead of silently overwriting session state (stdio) or being re-issued
a new `Mcp-Session-Id` (Streamable HTTP). Clients that follow the MCP specification
(single `initialize` per session) are unaffected.

Additionally, non-Hash JSON-RPC POST bodies on Streamable HTTP now return
HTTP 400 + JSON-RPC `-32600` rather than falling through to a broken Rack response.
The previous behavior produced an unparseable response, so this is unlikely to
affect any working client.

Closes #349.
2026-05-18 00:00:23 -04:00
Koichi ITO
9caba3085d
Merge pull request #354 from koic/reject_non_hash_post_body_in_streamable_http_transport
Reject non-Hash JSON-RPC bodies in `StreamableHTTPTransport`
2026-05-18 09:23:57 +09:00
Koichi ITO
f62d813625 Reject non-Hash JSON-RPC bodies in StreamableHTTPTransport
## Motivation and Context

MCP 2025-11-25 does not support JSON-RPC batch (batch support was removed in 2025-06-18),
so request bodies are expected to be a single JSON-RPC message object. The previous code path:

```ruby
body = parse_request_body(body_string)
return body unless body.is_a?(Hash)
```

returned the parsed JSON value as if it were a Rack response when
the body was a JSON array (former batch shape), a string, a number,
or any other non-Hash JSON value. This produced a malformed Rack response
instead of a proper HTTP 400.

This was a pre-existing bug exposed during follow-up review of #347
(`MCP-Protocol-Version` header validation), which surfaced it but did not fix
the broken response shape for non-Hash bodies sent with a valid header.

### Behavior

- `parse_request_body` now raises a private `InvalidJsonError` on parse
  failure instead of returning a Rack response tuple. The overloaded
  return type and the `parse_error_tuple?` discriminator are gone.
- `handle_post` rescues `InvalidJsonError` and returns the same plain
  JSON 400 response as before (`{"error":"Invalid JSON"}`), so parse
  error handling is observably unchanged.
- Non-Hash parsed bodies (arrays, strings, numbers, booleans, null) now
  return HTTP 400 with a plain JSON body explaining that the body must
  be a single JSON-RPC message object, instead of producing a malformed
  Rack response.

The error response shape stays in the existing plain JSON style for
consistency with `validate_content_type`, `not_acceptable_response`,
and the other transport-level error helpers. Unifying all of these to
a JSON-RPC error envelope (matching the Python and TypeScript SDKs) is
deferred to a separate follow-up.

### SDK Comparison

- Python SDK (`src/mcp/server/streamable_http.py`): Already rejects
  non-Hash bodies (Array, primitive) with HTTP 400. Uses a JSON-RPC
  error envelope with `INVALID_PARAMS`.
- TypeScript SDK (`packages/server/src/server/streamableHttp.ts`):
  As of `main`, still processes JSON arrays as JSON-RPC batches and
  has not yet caught up with the 2025-06-18 batch removal. Primitives
  fail schema validation and return 400 with a JSON-RPC error envelope.

This change brings the Ruby SDK in line with the current MCP spec
(no batch). The Ruby SDK is now closer to the spec than the TypeScript
SDK's `main` branch on this specific point.

## How Has This Been Tested?

Existing regression test `test "handles POST request with invalid
JSON"` continues to pass unchanged (plain JSON response preserved).
Added new regression tests:

- POST with a JSON array body returns 400 with a clear error message
- POST with a non-object JSON body (e.g. `"foo"`) returns 400

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

None for compliant clients sending single JSON-RPC message objects.
Clients that were sending JSON arrays (batch requests, no longer
supported as of MCP 2025-11-25) or other non-object bodies will now
receive a proper HTTP 400 with a JSON error body instead of
the previous malformed Rack response; no client could have relied on
the broken pre-existing behavior.
2026-05-18 00:56:28 +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
711f064088 Document initialize body protocolVersion is authoritative on Streamable HTTP
## Motivation and Context

When an `initialize` POST carries an `MCP-Protocol-Version` HTTP header that disagrees with
`params.protocolVersion` in the JSON-RPC body, the Streamable HTTP server accepts the request
and negotiates using the body value. The behavior was already encoded in `handle_post`
(the header check is skipped on `initialize`) but it was not stated in the code and there was
no regression test for the specific mismatch case.

MCP 2025-06-18 / 2025-11-25 only requires `MCP-Protocol-Version` on requests subsequent to
`initialize`, and the spec does not require the header and body to agree on the initial `initialize`.
All official MCP SDKs (TypeScript, Python, Rust, Java, Go, PHP) accept the mismatch and
treat the JSON-RPC body as authoritative.

## How Has This Been Tested?

Streamable HTTP tests: two new regression tests confirm that on an `initialize` POST
with a supported but mismatched `MCP-Protocol-Version` header, the server returns
HTTP 200 and the response's `result.protocolVersion` matches the JSON-RPC body value,
in both mismatch directions (header older / body newer, and header newer / body older).

## Breaking Changes

None. The behavior is unchanged: `initialize` requests with a mismatched `MCP-Protocol-Version` header
are still accepted, and the JSON-RPC body `params.protocolVersion` continues to drive negotiation.
This commit only adds an explanatory comment and regression tests.

Closes #351.
2026-05-17 00:03:49 +09:00
Koichi ITO
d6b5392831
Merge pull request #347 from koic/validate_mcp-protocol-version_header_in_streamable_http_transport
Validate `MCP-Protocol-Version` header in StreamableHTTPTransport
2026-05-15 00:45:51 +09:00
Koichi ITO
c7b059cb91 Release 0.16.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.
2026-05-14 12:02:17 +09:00
Koichi ITO
9c2e438d4a Validate MCP-Protocol-Version header in StreamableHTTPTransport
## Motivation and Context

MCP 2025-11-25 (basic/transports#protocol-version-header) requires servers
to respond with `400 Bad Request` when receiving a request with an invalid
or unsupported `MCP-Protocol-Version` header:

> If the server receives a request with an invalid or unsupported
> `MCP-Protocol-Version`, it MUST respond with `400 Bad Request`.

The Ruby SDK was not reading the header at all, accepting any value
(including malformed strings like `not-a-version` and unsupported versions
like `1900-01-01`) and dispatching requests normally with HTTP 200.

## Behavior

`StreamableHTTPTransport` now validates the `MCP-Protocol-Version` header
on POST (non-initialize), GET, and DELETE requests. Missing headers are
accepted as before; the spec SHOULD requirement to default to `2025-03-26`
is intentionally left for a follow-up. The `initialize` POST is exempt
because the client does not know the negotiated version until the response
arrives, matching the Python (`src/mcp/server/streamable_http.py`) and
TypeScript (`packages/server/src/server/streamableHttp.ts`) SDKs.

The error response uses a JSON-RPC envelope with `code: -32600`
(`INVALID_REQUEST`) to match the Python SDK shape:

```json
{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32600,
    "message": "Bad Request: Unsupported protocol version: <value>. Supported versions: ..."
  }
}
```

Validation order is session then protocol version, matching the Python and
TypeScript SDKs.

`handle_post` was reorganized so that non-Hash request bodies (e.g., JSON
arrays, which MCP 2025-11-25 no longer supports as batches) also pass
through the header check. The pre-existing broken response for Array
bodies sent with a valid header is unchanged and is tracked as a separate
follow-up.

## How Has This Been Tested?

Added regression tests covering:

- POST `initialize` ignores the header (bypass)
- POST non-initialize with unsupported / malformed / valid / missing values
- POST array body with unsupported value returns 400
- GET with unsupported / missing values
- DELETE with unsupported value in both stateful and stateless modes
- DELETE validates session before protocol version

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Strictly additive for compliant clients. Third-party clients sending stale
or malformed `MCP-Protocol-Version` values will now receive `400` instead
of `200`, which is the intended spec behavior.

Fixes #346.
2026-05-14 01:03:21 +09:00
Koichi ITO
9d2fdd02ed
Merge pull request #342 from koic/json_schema_2020_12
Advertise JSON Schema 2020-12 dialect on emitted tool schemas
2026-05-13 10:38:26 +09:00
Ates Goral
407b064140 feat: add opt-in tool output validation 2026-05-12 16:20:43 -04:00
Ates Goral
f67cc745c3 fix: preserve client tool output schemas 2026-05-11 21:44:26 -04:00
Koichi ITO
1fc698bb8e Advertise JSON Schema 2020-12 dialect on emitted tool schemas
## Motivation and Context

MCP 2025-11-25 (SEP-1613) establishes JSON Schema 2020-12 as the default dialect
for MCP schema definitions.
 https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1613

The Ruby SDK was emitting tool input and output schemas with no `$schema` field,
leaving the dialect implicit and inconsistent with the TypeScript SDK
(`packages/core/src/util/standardSchema.ts`, which emits with `target: 'draft-2020-12'`)
and the Python SDK (which preserves `$schema` in its types and conformance fixtures).

`Tool::Schema#to_h` now includes `"$schema": "https://json-schema.org/draft/2020-12/schema"`
unless the user supplied a different `$schema` value, so clients see the canonical dialect URI
on the wire.

Runtime metaschema and argument validation remain on draft-04 because the `json-schema` gem
does not yet support 2020-12. Switching the runtime validator is left as a separate change
because it requires either replacing the gem or waiting for upstream 2020-12 support.

## How Has This Been Tested?

Updated `Tool::Schema#to_h` expectations across `input_schema_test`, `output_schema_test`,
`tool_test`, and the `tools/list` server tests to reflect the new `$schema` field.
Added focused regression tests:

- `to_h` preserves a user-supplied `$schema` URI rather than overwriting it
  (covered for both symbol-keyed input and the string-keyed Hash form that
  goes through the initializer's `symbolize_names` normalization)
- `validate_arguments` / `validate_result` continue to work when the user supplies
  a 2020-12 `$schema` URI on the schema (the validator strips the unknown top-level
  dialect URI before invoking the draft-04 validator, instead of raising `JSON::Schema::SchemaError`)
- `tools/list` JSON-RPC response includes the 2020-12 `$schema` on both `inputSchema`
  and `outputSchema` (transport-boundary assertion)

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Additive change. Tolerant consumers of the `tools/list` payload are unaffected.
Consumers that strictly assert hash equality on the emitted `inputSchema` / `outputSchema`
need to account for the new `$schema` key, but no schema semantics change.

Schemas using 2020-12-only keywords (e.g. `prefixItems`, nested `$schema` on subschemas)
are still rejected by the draft-04 metaschema validator, as before.
Lifting that restriction depends on adopting a 2020-12-capable validator and is out of scope
for this change.
2026-05-11 13:44:30 +09:00
Koichi ITO
d18698890a
Merge pull request #301 from amorde/define-tool-output-schema
Fix missing output_schema argument in 'define_tool' API
2026-05-07 11:56:29 +09:00
Koichi ITO
32b6c9b646 Release 0.15.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.

It would be best to make the usage from #336 available sooner rather than later.
2026-05-05 08:14:35 +09:00
Koichi ITO
46b738e945 Add client-level connect handshake to stdio transport
## Motivation and Context

This commit implements Phase 1 of 3 for #334.

The MCP specification requires an `initialize` request followed by
a `notifications/initialized` notification before any other interaction.
PR #327 added `MCP::Client#connect` and `MCP::Client::HTTP#connect`
to expose this handshake explicitly for the HTTP transport,
but stdio remained on its private `initialize_session` lazy-init path,
inconsistent with the Python SDK (`ClientSession.initialize()`) and
TypeScript SDK (`Client.connect(transport)`),
both of which require an explicit handshake call regardless of transport.

The implicit init path on the first `send_request` is
preserved as a non-breaking compatibility shim. Public API added on `Stdio`:

- `connect(client_info:, protocol_version:, capabilities:)` performs the
  handshake, validates the negotiated `protocolVersion` against
  `MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS`,
  caches the `InitializeResult`, and is idempotent.
- `connected?` reports handshake completion.
- `server_info` exposes the cached `InitializeResult`.

Documentation and examples are updated so new users learn the explicit pattern
from the start.

## How Has This Been Tested?

Fifteen regression tests cover: explicit handshake, `server_info` caching,
idempotence, custom keyword arguments, JSON-RPC error response, missing
or non-Hash `result`, unsupported protocol version with state rollback,
state rollback when the `notifications/initialized` write fails,
retry after a failed handshake, and the `connected?` / `server_info` lifecycle.

The pre-existing stdio tests pass without modification, verifying that
the implicit-init compatibility path still works.

## Breaking Changes

None for the typical "implicit init via `client.tools` / `client.call_tool`" usage.
Existing stdio clients keep working unchanged.

Three observable behavior changes for code that inspects transport state directly:

- `Client#connected?` for a never-initialized stdio client returns `false`
  (previously fell through to a `true` default via the `respond_to?` fallback).
- `Client#server_info` for stdio returns the cached `InitializeResult`
  after the handshake (previously always `nil` via the fallback).
- The implicit handshake path now validates the server's negotiated
  protocol version, raising `RequestHandlerError` for unsupported versions.
  Production servers that return one of the supported versions are unaffected;
  this closes a pre-existing spec-conformance gap on stdio.
2026-05-03 15:57:22 +09:00
Koichi ITO
3d9437736b
Merge pull request #332 from koic/feature_cancellation
Support `notifications/cancelled` per MCP specification
2026-05-03 15:56:55 +09:00
Koichi ITO
a97e391353 Support notifications/cancelled per MCP specification
## Motivation and Context

The MCP specification defines `notifications/cancelled` so either party can stop
a previously-issued in-flight request. The Ruby SDK declared the method constant
but had no receive logic, no in-flight tracking, and no way for handlers to participate
in cancellation; cancellation notifications fell through to the unsupported-method path.

This PR implements the **server-side** half of the spec. The server stops processing
the targeted request cooperatively and suppresses its JSON-RPC response, matching
the Python SDK's anyio `CancelScope` and the TypeScript SDK's `RequestHandlerExtra.signal`.
Cancellation is observable by every user-overridable request handler:

- `Tool.call` (tools/call)
- prompt templates (prompts/get)
- blocks registered via `resources_read_handler` / `completion_handler` /
  `resources_subscribe_handler` / `resources_unsubscribe_handler` / `define_custom_method`

Each handler opts in by declaring a `server_context:` keyword and polls `server_context.cancelled?`
or calls `server_context.raise_if_cancelled!` (raising `MCP::CancelledError`) inside long-running work.
Handlers that keep their existing `|params|` (or `|args|`) signature continue to work unchanged.

On `StreamableHTTPTransport`, cancelling a parent `tools/call` automatically cancels n
ested server-to-client requests (`sampling/createMessage`, `elicitation/create`);
the nested `send_request` raises `MCP::CancelledError` and a cancel notification is routed to
the peer on the parent's POST response stream. `StdioTransport` is single-threaded and blocks on `$stdin.gets`,
so it deliberately does not propagate nested cancellation.
Tools running on stdio still observe cancellation between calls via `server_context.cancelled?`.

The design is cooperative-only (no `Thread#raise`) because preemptive cancellation is unsafe
across Rack adapters and arbitrary handler code. The `initialize` request is never cancellable,
satisfying the spec rule that it MUST NOT be cancelled. Unknown / completed / duplicate cancel notifications
are silently ignored per the spec.

`MCP::Client#cancel` (an equivalent that aborts the calling thread's synchronous wait) is deferred to a follow-up PR.

Ref: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation

## How Has This Been Tested?

`test/mcp/cancellation_test.rb` covers the `MCP::Cancellation` token contract.

`test/mcp/server_cancellation_test.rb` covers end-to-end cancellation:

- A handler spins on `cancelled?` in a background thread; the test sends `notifications/cancelled`
  and asserts the handler observed cancellation and the JSON-RPC response was suppressed.
  Each user-overridable handler type (tool, prompt template, resources/read, completion, custom method)
  has its own regression test.
- `initialize` is not cancellable; unknown / duplicate / late-after-completion cancels are silently ignored.
- The `reason` propagates into the `cancellation_reason` instrumentation field.
- Custom transports implementing only the abstract `(method, params = nil)` contract keep working;
  the new kwargs (`session_id:`, `related_request_id:`, `parent_cancellation:`, `server_session:`)
  are silently dropped when the transport's signature does not declare them.

`StreamableHTTPTransport` tests cover nested cancellation, hook deregistration after normal completion
(so a late parent cancel does not emit a stray `notifications/cancelled`), and the first-writer-wins race
when a real response and a cancel arrive concurrently.

## Breaking Change

None. `MCP::ServerContext#initialize` gains an optional `cancellation:` keyword (defaults to `nil`),
and `StreamableHTTPTransport#send_request` / `#send_notification` gain optional cancellation kwargs.
The abstract `Transport` base class and `StdioTransport` keep their existing `(method, params = nil)` signatures,
and custom transports following those signatures continue to work. `StreamableHTTPTransport` now dispatches
client-originated notifications through `ServerSession#handle_json` before returning 202;
previously these were accepted without dispatch, but the existing handlers for `notifications/initialized`
and `notifications/progress` were already no-ops, so no user-visible effect is expected.
Adding `server_context:` to a handler block is strictly opt-in.
2026-05-03 04:02:22 +09:00
Koichi ITO
0c46dcd0ab
Merge pull request #327 from atesgoral/ag/client-connect-handshake
Add client-level connect for initialize handshake
2026-05-02 15:40:55 +09:00
Ates Goral
ddfbfad81e Add client-level connect for initialize handshake 2026-05-01 20:17:45 -04:00
Koichi ITO
48d4fb9272 Return tool argument validation failures as tool execution errors
## Motivation and Context

MCP 2025-11-25 (SEP-1303) clarifies that input validation errors for `tools/call`
should be returned as Tool Execution Errors (`{ content: [...], isError: true }`)
rather than as JSON-RPC `-32602` protocol errors, so models can observe
the validation message and self-correct on a follow-up call.
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1303

This is a clarification rather than a brand-new requirement: the 2024-11-05, 2025-03-26,
and 2025-06-18 specifications all defined two error categories with overlapping language
("Invalid arguments" listed under Protocol Errors *and* "Invalid input data" listed under
Tool Execution Errors), leaving the routing of JSON-Schema validation failures ambiguous.
The Ruby SDK had selected the "Protocol Errors / Invalid arguments" interpretation,
while the TypeScript SDK (`packages/server/src/server/mcp.ts`, which wraps
`validateToolInput` errors via `createToolError`) and Python SDK's FastMCP
(which routes through the generic `is_error=True` path) had selected the other.
SEP-1303 in 2025-11-25 disambiguates this by replacing the bullets with
"Malformed requests (requests that fail to satisfy CallToolRequest schema)"
under Protocol Errors and "Input validation errors (e.g., date in wrong format,
value out of range)" under Tool Execution Errors, and explicitly states that the latter
"contain actionable feedback that language models can use to self-correct".

`tool_not_found` continues to be returned as a JSON-RPC `-32602` protocol error since
the spec change only covers input validation.

## How Has This Been Tested?

Updated existing tests that previously asserted `-32602` and "Invalid arguments" /
"Missing required arguments" in the JSON-RPC error data to instead
assert `result[:isError] == true` with the same text in the `content` block.
Added new regression tests covering:

- Instrumentation still records `:missing_required_arguments` and
  `:invalid_schema` on the new non-raising path
- Nested schema validation failure (deep arrays of objects with
  required fields) returns a tool execution error rather than a
  protocol error
- `tool_not_found` continues to return JSON-RPC `-32602` (regression
  guard against accidentally widening the change)

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Yes. Clients that detect tool argument validation errors via `error.code == -32602`
will need to switch to inspecting `result.isError == true` and reading `result.content[].text`.

Note that the Ruby SDK's previous behavior was a defensible reading of the 2024-11-05 /
2025-03-26 / 2025-06-18 spec wording. The 2025-11-25 disambiguation is what makes
the previous behavior non-conforming; the TypeScript and Python SDKs already shipped
the new behavior.
2026-05-01 03:18:29 +09:00
Koichi ITO
2ad3d21492
Merge pull request #331 from koic/remove_obsolete_transport_module
Remove obsolete `MCP::Transports` module
2026-04-29 00:20:50 +09:00
Koichi ITO
ceea440ca5 Remove obsolete MCP::Transports module
## Motivation and Context

`lib/mcp/transports/stdio.rb` was a deprecated shim that redirected users
from `MCP::Transports::StdioTransport` to `MCP::Server::Transports::StdioTransport`.
It has been 10 months since the migration path was introduced, providing sufficient time
for users to update their code.

## How Has This Been Tested?

All tests pass (`rake test`), and no code in the repository references `MCP::Transports`
or `mcp/transports/stdio`.

## Breaking Changes

`require "mcp/transports/stdio"` and `MCP::Transports::StdioTransport` are removed.
Use `require "mcp/server/transports/stdio_transport"` and `MCP::Server::Transports::StdioTransport` instead.
2026-04-26 15:54:07 +09:00
Koichi ITO
b03ec40d23 Release 0.14.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.
2026-04-24 11:27:41 +09:00
Koichi ITO
a89047af60
Merge pull request #328 from koic/support_json_response_mode
Support JSON response mode for `StreamableHTTPTransport`
2026-04-23 14:05:50 +09:00
Koichi ITO
017b0bb4c9 Support JSON response mode for StreamableHTTPTransport
## Motivation and Context

The MCP Streamable HTTP specification allows servers to return POST responses as either
`text/event-stream` (SSE) or `application/json`:

> If the input is a JSON-RPC request, the server MUST either return `Content-Type: text/event-stream`,
> to initiate an SSE stream, or `Content-Type: application/json`, to return one JSON object.

See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server

The TypeScript and Python SDKs support a configurable JSON response mode
via `enableJsonResponse` / `is_json_response_enabled`.

JSON response mode is suitable for simple tool servers that do not need server-initiated requests.
It avoids SSE framing overhead and returns a single JSON object for the POST response.

## Behavior

- POST responses use `Content-Type: application/json` and return a single JSON object.
- The POST `Accept` header requirement is relaxed to `application/json` only
  (matching the Python SDK's lenient behavior).
- Request-scoped notifications (`progress`, `log`) cannot ride along with the single-object response
  and are silently dropped.
- Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`)
  and broadcast notifications (`tools/list_changed`, etc.) continue to flow to clients connected
  to the GET SSE stream.
- All server-to-client requests (`sampling/createMessage`, `roots/list`, `elicitation/create`)
  raise an error in JSON response mode.
- Combines with `stateless: true` for simple single-response servers without session tracking.

## How Has This Been Tested?

Added tests for JSON response mode:

- POST request returns `application/json` response
- Accept header validation requires only `application/json`
- Returns 406 when Accept header is missing
- Accepts wildcard `*/*` in Accept header
- Request-scoped notifications (progress, log) during tool execution are silently dropped
- Request-scoped notifications do not leak to GET SSE even when connected
- Session-scoped standalone notifications are delivered via GET SSE
- Broadcast notifications are delivered via GET SSE
- `send_request` raises for `sampling/createMessage`, `roots/list`, `elicitation/create`
- Combined with `stateless: true`: POST returns JSON without a session ID, GET returns 405

## Breaking Changes

None. JSON response mode is opt-in via `enable_json_response: true`.
The default behavior (SSE responses) is unchanged.
2026-04-22 08:30:28 +09:00
Ates Goral
ff3a42f3fb Add HTTP client close for explicit session termination
Per MCP spec, clients that no longer need a session SHOULD send an HTTP
DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
terminate it. Servers MAY respond with 405 Method Not Allowed when they
don't support client-initiated termination.

`MCP::Client::HTTP#close` sends the DELETE with session headers, clears
local session state regardless of outcome, and is a no-op when no
session has been established.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
2026-04-20 13:58:41 -04:00
Ates Goral
07226b7cf1 Track Mcp-Session-Id and protocol version in HTTP client
Per the Streamable HTTP spec, if the server returns an Mcp-Session-Id
header during `initialize`, the client MUST include it on subsequent
requests. Capture the session ID and protocol version from the
`initialize` response and attach them automatically on subsequent
POSTs.

Per the Protocol Version Header section of the spec, the client MUST
also include `MCP-Protocol-Version` on all subsequent requests so the
server can respond based on the negotiated protocol version.

Map 404 responses to a new `SessionExpiredError` (a subclass of
`RequestHandlerError` for backward compatibility) and clear local
session state so callers can start a fresh session with a new
`initialize` request, as required by the spec.

- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:23:07 -04:00
Koichi ITO
baa093dfd5
Merge pull request #324 from koic/ping_client_to_server
Support `ping` client API per MCP specification
2026-04-20 12:15:21 +09:00
Ates Goral
cfc42dbfe7 Handle 202 Accepted response in HTTP client
Per the Streamable HTTP spec, a server returns 202 Accepted with no
body as the ACK for a JSON-RPC notification or response. The client
previously errored on 202 because it fell through Content-Type
dispatch. Short-circuit to return nil before Content-Type handling so
fire-and-forget messages (e.g. notifications/initialized) no longer
raise.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 14:06:07 -04:00
Koichi ITO
5c0fee309f
Merge pull request #322 from atesgoral/ag/client-sse-parser
Parse SSE responses in HTTP client via event_stream_parser
2026-04-20 00:58:59 +09:00
Ates Goral
b832933f9f Parse SSE responses in HTTP client via event_stream_parser
The Streamable HTTP spec allows servers to respond with either
`application/json` or `text/event-stream`. The client previously
rejected the latter. Add SSE parsing via the optional
`event_stream_parser` gem (users add it to their Gemfile when their
server uses SSE), dispatch on the response Content-Type, and scan the
event stream for the first JSON-RPC response message.

Document the new optional dependency in both the README's HTTP
Transport Layer section and docs/building-clients.md.

No other Streamable HTTP features (202 handling, session IDs,
`connect`, DELETE termination) are included here; those will land in
follow-up PRs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:54:03 -04:00
Koichi ITO
493321ca46 Support ping client API per MCP specification
## 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.
2026-04-19 23:14:50 +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
b97b922a36 Support pagination per MCP specification
## 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.
2026-04-18 18:23:30 +09:00
Koichi ITO
7b5533c809
Merge pull request #315 from koic/support_roots
Add `roots/list` and `notifications/roots/list_changed` support
2026-04-18 02:06:32 +09:00
Koichi ITO
52f8384e9d
Merge pull request #314 from koic/remove_redundant_respond_to_flush
Remove redundant `respond_to?(:flush)` guard in `StreamableHTTPTransport`
2026-04-18 00:43:42 +09:00
Koichi ITO
954078d90c Release 0.13.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.

In particular, releasing "Make `StreamableHTTPTransport` a Rack application"
soon would help keep it consistent with the README.md for Rails integration.
2026-04-16 03:45:51 +09:00
Koichi ITO
66d0722e41
Merge pull request #263 from koic/make_streamable_http_transport_a_standard_rack_app
Make `StreamableHTTPTransport` a Rack application
2026-04-15 11:01:40 +09:00
Koichi ITO
eeb130171d Add roots/list and notifications/roots/list_changed support
## Motivation and Context

MCP spec (2025-11-25) defines Roots as a mechanism for clients to expose filesystem boundaries
(directories and files) to servers:
https://modelcontextprotocol.io/specification/2025-11-25/client/roots

This enables servers to understand which parts of the filesystem they can operate on.

This adds support for the two roots protocol interactions:

- `roots/list` -- server-to-client request to retrieve the client's filesystem roots
- `notifications/roots/list_changed` -- client-to-server notification when roots change

`roots/list` is a one-to-one request between a server and a specific client,
so `list_roots` is exposed only at the session level
(`ServerSession#list_roots`), matching the Python SDK approach and aligning with PR #311 for sampling.

`Server#on_roots_list_changed` registers a callback for the client notification,
which is server-wide and does not require session scoping.

Also fixes `notifications/roots/list_changed` capability checking: as a client-originated notification,
it should not require server-side capability declaration.

## How Has This Been Tested?

New test file `test/mcp/server_roots_test.rb` with 8 test cases covering session-scoped `list_roots`,
capability validation and fallback, `on_roots_list_changed` callback registration, and client capability storage.
Updated `test/mcp/methods_test.rb` to reflect the corrected capability checks. All tests pass (`rake test`),
RuboCop is clean.

## Breaking Changes

None.
2026-04-15 02:39:14 +09:00
Koichi ITO
7a3259d4da
Merge pull request #311 from koic/remove_server_sampling_direct_call
Remove `Server#create_sampling_message` direct call
2026-04-15 02:33:26 +09:00
Koichi ITO
3b1777adb3
Merge pull request #312 from koic/support_elicitation
Support `elicitation/create` per MCP specification
2026-04-15 02:22:55 +09:00
Koichi ITO
1ddd26b10b Remove redundant respond_to?(:flush) guard in StreamableHTTPTransport
## Motivation and Context

The stream object always responds to flush, so the conditional check is unnecessary.

## How Has This Been Tested?

All existing tests are passing.

## Breaking Changes

None.
2026-04-14 10:14:58 +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
f41272f2a0
Merge pull request #309 from koic/add_around_request_hook_for_request_instrumentation
Add `around_request` hook for request instrumentation
2026-04-13 01:15:19 +09:00
Koichi ITO
a235572777 Remove Server#create_sampling_message direct call
## Motivation and Context

Sampling is a one-to-one request/response interaction between a server and a specific client.
`Server#create_sampling_message` does not scope requests to a specific session, which could
lead to unintended broadcasts on multi-client transports. `ServerSession#create_sampling_message`
should be used instead, as it routes the request to the correct client session.

This aligns with the Python SDK, which also exposes sampling only at the session level.

## How Has This Been Tested?

Existing tests updated to call `ServerSession#create_sampling_message` instead of
`Server#create_sampling_message`. All tests pass (`rake test`), RuboCop is clean,
and conformance tests pass.

## Breaking Changes

`Server#create_sampling_message` is removed without a deprecation warning.
The sampling feature was released recently and the method was not intended
to be the primary API for this feature.
2026-04-12 17:05:46 +09:00