Commit graph ruby-sdk/README.md
Author SHA1 Message Date
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
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
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
acdf85f039 [Doc] Restructure README Usage section to align heading granularity
Group the Rails (mount) and Rails (controller) examples under a new
`Streamable HTTP Transport` heading so they sit one level below the
transport-type headings instead of alongside `Stdio Transport`.

Also move the single-process and sticky-sessions caveat up to the
`Streamable HTTP Transport` section since it applies to both Rails
integration styles rather than only the controller approach.
2026-04-15 14:51:12 +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
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
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
Koichi ITO
fab6f81261 Add around_request hook for request instrumentation
## Motivation and Context

`instrumentation_callback` fires after request execution, making it impossible to wrap
requests with Application Performance Monitoring (APM) spans (e.g. Datadog tracing).
This adds `around_request`, which wraps request handling and allows executing code
before and after each request.

For example, to wrap requests with a Datadog trace:

```ruby
MCP.configure do |config|
  config.around_request = ->(data, &request_handler) {
    Datadog::Tracing.trace("mcp.#{data[:method]}") do
      request_handler.call
    end
  }
end
```

`instrumentation_callback` is soft-deprecated in favor of `around_request`.

## How Has This Been Tested?

Added tests for `around_request` in configuration, instrumentation,
and server test files. All existing tests continue to pass.

## Breaking Changes

`around_request` itself is optional and defaults to a pass-through,
and `instrumentation_callback` continues to work as before.

However, `instrument_call` now accepts a `server_context:` keyword and its block is invoked
without arguments (previously `yield block` passed the block Proc as an argument).
This affects anyone mixing `MCP::Instrumentation` into their own classes,
though the public `Server` API is unchanged.

Resolves #302.
2026-04-11 13:54:47 +09:00
Koichi ITO
a05d941edd
Merge pull request #306 from koic/support_customizing_faraday_client_in_mcp_client_http
Support customizing the Faraday client in `MCP::Client::HTTP`
2026-04-10 13:42:41 +09:00
Koichi ITO
af99a303e1 Make StreamableHTTPTransport a Rack application
## Motivation and Context

Add `call(env)` to `StreamableHTTPTransport`, making it a Rack application
that works with `mount`, `run`, and Rack middleware.

Refactor examples to use Rack middleware classes for MCP logging instead of proc wrappers,
demonstrating idiomatic Rack composition with the new `run(transport)` pattern.

Update README.md with mount and controller integration patterns.

Closes #59, #60

## How Has This Been Tested?

Added tests for `call(env)` as a Rack app. All tests pass.

## Breaking Change

No breaking changes. All existing APIs are preserved:

- `StreamableHTTPTransport.new(server)` continues to work as before.
- `handle_request(request)` is unchanged. The new `call(env)` is a purely additive public method.
2026-04-10 04:55:25 +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
f7952e8aaf Support customizing the Faraday client in MCP::Client::HTTP
## Motivation and Context

`MCP::Client::HTTP` builds its Faraday connection internally in a private `client` method
with no way to customize the middleware stack or adapter.
Users who need observability (request/response recording, failure logging) or
a different HTTP adapter must override a private method, coupling to an internal API.

This accepts an optional block in `MCP::Client::HTTP.new` that yields the Faraday builder
after default middleware is configured, allowing users to add custom middleware or swap
the HTTP adapter.

## How Has This Been Tested?

Added a test that verifies custom headers set via the block are included in requests.
All existing tests continue to pass.

## Breaking Changes

None. The block argument is optional, so existing code is unaffected.

Resolves #303
2026-04-09 05:08:49 +09:00
Koichi ITO
2822e00b62
Merge pull request #299 from koic/update_rails_controller_example_in_readme
[Doc] Update Rails controller example in README.md
2026-04-07 11:37:51 +09:00
Koichi ITO
0762879abe
Merge pull request #297 from koic/sticky_session_for_multi_server
[Doc] Note sticky session requirement for multi-instance deployments
2026-04-07 11:37:33 +09:00
Koichi ITO
17604beff9 [Doc] Update Rails controller example in README.md
The following two points have been updated:

## 1. Use ActionController::API in README controller example

`ActionController::Base` includes CSRF protection which rejects POST requests
without an authenticity token. MCP clients do not send CSRF tokens,
so the controller example should inherit from `ActionController::API` instead.

## 2. Use `stateless: true` for `StreamableHTTPTransport.new`

The controller creates a new transport per request,
so the session stored on the previous transport is lost.
Without `stateless: true`, the second request with `Mcp-Session-Id`
returns 404 because the new transport has an empty session map.

To share sessions via `Mcp-Session-Id` across requests, there are two approaches.
One is persisting the transport in a class variable. The other is mounting
the transport as a Rack app via https://github.com/modelcontextprotocol/ruby-sdk/pull/263.

Both approaches maintain sessions, so features that depend on `server_context`
within the SDK (Progress, Sampling) work correctly. However, per-request
user-specific context such as `server_context: { user_id: current_user.id }`
cannot be passed since the server is shared across all requests.
2026-04-06 03:22:59 +09:00
Koichi ITO
1b45c4382b [Doc] Note sticky session requirement for multi-instance deployments
## Motivation and Context

PR #290 documented that `StreamableHTTPTransport` requires a single-process server.
However, when running multiple server instances behind a load balancer, requests for
the same session must be routed to the same instance since session state is stored in memory.

This adds a note about using sticky sessions (session affinity) based on the `Mcp-Session-Id` header.

## How Has This Been Tested?

Documentation-only change.

## Breaking Changes

None.
2026-04-06 03:20:46 +09:00
Koichi ITO
3f43b1feac [Doc] Restructure sampling section to lead with tool usage
Sampling is primarily used within tool execution via `server_context.create_sampling_message`,
but the documentation led with the less common direct `Server#create_sampling_message` usage.
This ordering could mislead readers into thinking direct usage is the recommended approach.
2026-04-06 03:17:18 +09:00
Koichi ITO
f9096347bc [Doc] Move stdio before Rails Controller in Usage
For Usage, stdio is easier to try first than a Rails controller.
Moved stdio to the top and placed Rails Controller after it.

Only sections were moved. No content was changed.
2026-04-05 13:29:47 +09:00
Koichi ITO
63f51bbc26 [Doc] Move Usage to the top of the README.md
Users are more likely to want to see Usage first, rather than a feature introduction like Sampling.
Sampling has been moved below Resource Templates so that Usage appears at the top.

Only sections were moved. No content was changed.
2026-04-05 13:29:47 +09:00
Koichi ITO
ee4a7bb4df [Doc] Move Custom methods to the bottom of the README
Custom methods are an advanced feature and may not be appropriate for the top of the README.md.
Move them to the bottom of the page.

Only sections were reorganized with headings. No content was changed.
2026-04-05 13:29:30 +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
6f94d64ece
Merge pull request #290 from koic/thread_model_requirement
[Doc] Clarify that `StreamableHTTPTransport` requires a single-process server
2026-04-04 05:51:58 +09:00
Koichi ITO
9ebb42a536 [Doc] Clarify that StreamableHTTPTransport requires a single-process server
## Motivation and Context

`StreamableHTTPTransport` stores session and SSE stream state in memory using
instance variables (`@sessions`, `@mutex`). This design requires all HTTP requests
to be handled within the same process to share state.

Process-based servers like Unicorn fork separate worker processes that do not share memory,
which breaks session management and SSE connections.
This requirement is not obvious from the current documentation.

## How Has This Been Tested?

Documentation-only change.

## Breaking Changes

None.
2026-04-04 02:08:01 +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
f0186d08b0
Merge pull request #268 from koic/security__missing_session_expiry_controls
Support session expiry controls for `StreamableHTTPTransport`
2026-03-30 05:46:07 +09:00
Koichi ITO
b89f6e315d Remove Server#notify_progress broadcast API
## Motivation and Context

`Server#notify_progress` broadcasts progress notifications to all connected clients.
Progress notifications are tied to a specific request's `progressToken` and have no
meaningful broadcast use case. Neither the Python SDK nor the TypeScript SDK provides
a server-level progress broadcast capability.

The MCP specification requires that progress notifications only reference tokens
provided in an active request:

> Progress notifications MUST only reference tokens that:
> - Were provided in an active request
> - Are associated with an in-progress operation

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

Broadcasting progress to all clients does not comply with this requirement,
since clients that did not originate the request have no matching `progressToken`.

The introduction of `ServerSession` for per-connection state (#275) made
this removal possible: `Progress` and `ServerContext` now use `notification_target:`
(a `ServerSession`) to route notifications to the originating session only.

### Changes

- Removed `Server#notify_progress` entirely. The method is no longer needed as
  `notification_target` in `call_tool_with_args` now passes `session` directly
  (nil when no session is available, in which case `Progress#report` and
  `ServerContext#notify_log_message` are no-ops).
- Removed `ServerContext#notify_progress` as well. Progress notifications should
  only be sent via `server_context.report_progress`, which enforces the correct
  `progressToken` from the originating request.
- Added nil guards to `Progress#report` and `ServerContext#notify_log_message`
  for when no session is available.
- Rewrote progress tests to use `ServerSession#handle` instead of
  `Server#handle`, reflecting that progress notifications are always session-scoped.
- Removed 4 tests that called `server.notify_progress` directly as a public broadcast API.
- Removed `notify_progress` from the README.md notification methods list.
- Removed the "Server-Side: Direct `notify_progress` Usage" section from the README.md.

Progress notifications should be sent via `server_context.report_progress` inside tool handlers,
which automatically scopes them to the originating client session.

## Breaking Changes

Progress notifications are scoped to a specific request via `progressToken` per the MCP specification,
so the broadcast behavior of `Server#notify_progress` was a spec violation.
This is treated as a bug fix and is made without a deprecation period.
This aligns the Ruby SDK with the Python and TypeScript SDKs, neither of which provides
a server-level progress broadcast API.

`Server#notify_progress` and `ServerContext#notify_progress` are no longer available.
Users should use `server_context.report_progress` inside tool handlers instead, which
provides session-scoped delivery with the correct `progressToken`.

This feature was only recently introduced in mcp 0.9.0:
https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.9.0

So an early release should help limit its impact on adoption.
2026-03-29 15:11:28 +09:00
Koichi ITO
49fc501abc Support session expiry controls for StreamableHTTPTransport
## Motivation and Context

The MCP specification recommends expiring session IDs to reduce session hijacking risks:
https://modelcontextprotocol.io/specification/latest/basic/security_best_practices#session-hijacking

Several other SDKs (Go, C#, PHP, Python) already implement session expiry controls, but
the Ruby SDK did not. Sessions persisted indefinitely unless explicitly deleted or
a stream error occurred, leaving abandoned sessions to accumulate in memory.

This adds a `session_idle_timeout:` option to `StreamableHTTPTransport#initialize`. When
set, sessions that receive no HTTP requests for the specified duration (in seconds) are
automatically expired. Expired sessions return 404 on subsequent requests (GET and POST),
matching the MCP specification's behavior for terminated sessions. Each request resets
the idle timer, so actively used sessions are not interrupted.

A background reaper thread periodically cleans up expired sessions to handle orphaned
sessions that receive no further requests. The reaper only starts when
`session_idle_timeout` is configured.

The default is `nil` (no expiry) for backward compatibility, consistent with the Python
SDK's approach. The Python SDK recommends 1800 seconds (30 minutes) for most deployments:
https://github.com/modelcontextprotocol/python-sdk/pull/2022

Resolves #265.

## How Has This Been Tested?

Added tests for session expiry, idle timeout reset, reaper thread lifecycle, input
validation, and default behavior in `streamable_http_transport_test.rb`. All existing
tests continue to pass.

## Breaking Change

None. The default value of `session_idle_timeout` is `nil`, which preserves the existing
behavior of sessions never expiring. The new `last_active_at` field in the internal
session hash is not part of the public API. Existing code that instantiates
`StreamableHTTPTransport.new(server)` or
`StreamableHTTPTransport.new(server, stateless: true)` continues to work without changes.
2026-03-29 12:34:58 +09:00
Koichi ITO
719e5e7fee Introduce ServerSession for per-connection state
## Motivation and Context

The Ruby SDK uses a 1-Server-to-many-sessions model where session-specific state
(`@client`, `@logging_message_notification`) is stored on the shared `Server` instance.
This causes the last client to connect to overwrite state for all sessions,
and requires `session_id` to be threaded through method signatures for session-scoped notifications.

The Python SDK avoids this by creating a `ServerSession` per connection that wraps a shared `Server`.
Each session holds its own state and naturally scopes notifications through its own stream.

### Changes

- Added `MCP::ServerSession` class holding per-session state: client info,
  logging configuration, and a `transport` reference for session-scoped
  notification delivery.
- `StreamableHTTPTransport#handle_initialization` creates a `ServerSession`
  per `initialize` request and stores it in the session hash.
- `handle_regular_request` routes requests through the session's
  `ServerSession` instead of the shared `Server` directly.
- `Server#handle` and `Server#handle_json` accept an optional `session:`
  keyword (`ServerSession` instance).
- `Server#init` stores client info on the session when available.
- `Server#configure_logging_level` stores logging config on the session.
- `ServerContext` and `Progress` accept a `notification_target:` which
  can be either a `ServerSession` (session-scoped) or a `Server` (broadcast).
  No `session_id:` parameter needed.
- `ServerContext#notify_progress` and `#notify_log_message` delegate to
  the `notification_target` without `session_id` threading.
- `StdioTransport` creates a single `ServerSession` on `open`,
  making the session model transparent across both transports.

### Design Decision

This follows the Python SDK's "shared `Server` + per-connection `Session`" pattern.
The `Server` holds configuration (tools, prompts, resources, handlers).
Each `ServerSession` holds per-connection state (client info, logging level, stream writer).
Notifications from `ServerSession` go only to that session's stream.

## How Has This Been Tested?

All existing tests pass. All conformance tests pass. Added tests verifying:

- Session-scoped log notification is sent only to the originating session.
- Session-scoped progress notification is sent only to the originating session.
- Each session stores its own client info independently.
- Each session stores its own logging level independently.
- `StdioTransport#open` creates a `ServerSession` and stores client info on it.

## Breaking Change

None for end users.

The public API (`Server.new`, `define_tool`,`server_context.report_progress`,
`server_context.notify_log_message`, etc.) is unchanged. The following internal API
changes affect only SDK internals:

- `ServerContext.new` now requires `notification_target:` instead of just`progress:`.
- `Progress.new` now takes `notification_target:` instead of `server:`.
- `Server#handle` and `Server#handle_json` accept an optional `session:` keyword.
2026-03-29 08:04:56 +09:00
Koichi ITO
21eef8df9a Add MCP::Client::Stdio transport
## Motivation and Context

The `MCP::Client` currently only supports HTTP transport (`MCP::Client::HTTP`).
To achieve parity with other MCP SDKs (Python, TypeScript, C#) which provide
stdio client transports for spawning and communicating with server subprocesses,
the Ruby SDK needs a stdio client transport.
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio

This enables building MCP clients that launch server processes and communicate
via stdin/stdout using newline-delimited JSON-RPC 2.0 messages.

## How Has This Been Tested?

Added tests for process spawning, protocol initialization handshake,
notification handling, error handling, and transport close lifecycle.

## Additional context

Enabled the `Lint/IncompatibleIoSelectWithFiberScheduler` cop in `.rubocop.yml`
to detect accidental use of `IO.select` instead of `IO#wait_readable`.
2026-03-19 12:41:27 +09:00
Koichi ITO
a2514a0ef2
Merge pull request #254 from koic/support_progress_notification_per_mcp_spec
Support progress notifications per MCP specification
2026-03-12 12:26:39 +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
8a97cebc33
Merge pull request #252 from koic/fix_rails_controller_example_to_return_202_for_notifications
[Doc] Fix Rails controller example to return 202 for notifications
2026-03-08 03:06:19 +09:00
Koichi ITO
7e0a339681 [Doc] Fix Rails controller example to return 202 for notifications
The previous example used `Server#handle_json` directly, which does not control HTTP status codes.
Notifications such as `notifications/initialized` require 202 Accepted per the MCP spec,
but the example always returned 200.

Use `StreamableHTTPTransport#handle_request` instead, which returns the correct HTTP status code
for each request type.

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

Closes #158
2026-03-06 03:21:25 +09:00
Eric Kreutzer
50cd86c01c
Add automatic _meta parameter extraction support 2026-03-04 10:11:07 -07: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
Koichi ITO
1a2750d6d7
Merge pull request #222 from koic/update_readme_for_instrumentation_callback
[Doc] Add `tool_arguments` to instrumentation callback documentation
2026-02-09 00:48:46 +09:00
Koichi ITO
0e30dff80b
Merge pull request #225 from sjrct/add-client-resource-templates
Add resource_templates to MCP::Client
2026-02-07 02:58:52 +09:00
Chris Harding
c145fe39df Add resource_templates to MCP::Client
Includes relevant README addition and tests
2026-02-05 16:02:07 -05:00
Mikhail Dubov
2d926ab386 Add client info to instrumentation callback 2026-02-04 12:03:03 +00:00
Koichi ITO
1c449ae549 [Doc] Add tool_arguments to instrumentation callback documentation
Follow-up to #218, which added the `tool_arguments` feature to the instrumentation callback.
This updates README.md to document the new `tool_arguments` key in both instrumentation callback sections,
and removes redundant instrumentation documentation.
2026-01-31 15:47:56 +09:00
dak2
afa2fb4077 Add logging support
A server can send structured logging messages to the client.
https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#logging

Logging was specified in the 2024-11-05 specification, but since it was not supported in ruby-sdk, I implemented it.
https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging

I also made it possible to output a simple notification message in the examples.
2026-01-26 22:23:46 +09:00
Adam Jones
685a4913d4
chore: update licensing to Apache 2.0 for new contributions 2026-01-12 22:49:19 +00:00