## 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.
## 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.
## 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.
## 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.
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
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>
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>
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>
## Motivation and Context
The MCP specification defines the `ping` utility for verifying that the peer of
a connection is still responsive. The Ruby SDK server side already replies to
incoming `ping` requests with an empty result, but `MCP::Client` had no public API
to send a `ping` request. Users had to build the JSON-RPC payload themselves and
call the transport directly.
This adds a client-side counterpart to the existing server handler and aligns
the Ruby SDK in spirit with the Python and TypeScript SDKs, which expose
`ClientSession.send_ping` and `Client.ping` respectively. Those SDKs validate
the response envelope via type schemas (`EmptyResult` / `EmptyResultSchema`) and raise
their validation library's generic exception (Pydantic's `ValidationError` / Zod's `ZodError`).
The Ruby client applies a Hash type check on `result`
(matching Python's behavior of requiring a dict while allowing arbitrary contents)
and raises a new `MCP::Client::ValidationError` for malformed responses,
keeping `MCP::Client::ServerError` reserved for JSON-RPC error responses.
## How Has This Been Tested?
Client tests cover:
- request structure (method is `ping`, no `params`)
- empty result on success
- `ServerError` on JSON-RPC error response
- `ValidationError` on a response missing `result`
- `ValidationError` on a `result` of the wrong type (e.g., String)
- propagation of transport-level errors raised by `transport.send_request`
The existing server-side `ping` handler tests continue to pass unchanged.
## Breaking Changes
None. `MCP::Client#ping` and `MCP::Client::ValidationError` are purely additive.
The server-side `ping` handler behavior is unchanged.
## Motivation and Context
The MCP specification defines `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.
## 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.
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.
## 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.
## 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.
## 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.
## 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.
## Motivation and Context
MCP 2025-11-25 spec defines a `_meta` general field on many types,
but the Ruby SDK only exposed it on `Tool` and `Prompt`.
To attach metadata (e.g. MCP Apps CSP hints on resource contents),
consumers had to subclass existing classes and override `to_h`.
Add a `meta:` keyword argument and `_meta` serialization to:
- `Resource::Contents`, `Resource::TextContents`, `Resource::BlobContents`
- `Resource`, `ResourceTemplate`
- `Content::Text`, `Content::Image`, `Content::Audio`, `Content::EmbeddedResource`
- `Tool::Response` (`CallToolResult`)
- `Prompt::Result` (`GetPromptResult`)
This aligns the Ruby SDK with the Python and TypeScript SDKs, which both expose `_meta` on these types.
Resolves#308.
## How Has This Been Tested?
Added unit tests for each updated class covering two patterns: `_meta` present
(asserts the value is reflected in `to_h[:_meta]`) and `_meta` omitted
(asserts no `:_meta` key in `to_h`, i.e., `.compact` behavior).
All existing tests continue to pass, and `rake rubocop` is clean.
## Breaking Changes
None. All new `meta:` parameters are optional keyword arguments defaulting to `nil`.
The `to_h` output stays backward-compatible because `nil` values are removed by the existing `.compact` call,
so existing callers require no changes.
## 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.
## 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.
## 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).
## 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
## Motivation and Context
The MCP Streamable HTTP specification requires POST request bodies to
be JSON-RPC messages with `Content-Type: application/json`. The Ruby
SDK did not validate Content-Type, accepting requests with any or no
Content-Type. The Python SDK validates this and returns 415
Unsupported Media Type for non-JSON Content-Types.
## How Has This Been Tested?
Added tests for Content-Type validation:
- POST without Content-Type returns 415
- POST with `Content-Type: text/plain` returns 415
- POST with `Content-Type: application/json; charset=utf-8` succeeds
## Breaking Changes
POST requests without `Content-Type: application/json` now return
415 Unsupported Media Type. This is a spec compliance fix.
New features and fixes have been added, and it seems like a good time to cut a release.
Subsequent feature proposals can continue to be incorporated into future releases.
Follow-up to #294.
The generic `:stream` key does not convey that it refers to the GET SSE stream,
making it harder to distinguish from `session[:post_request_streams]` introduced in #294.
## Motivation and Context
The MCP Streamable HTTP specification defines that servers can return POST responses
as SSE streams and send server-to-client JSON-RPC requests and notifications through them:
> 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.
If the server initiates an SSE stream:
> The server MAY send JSON-RPC requests and notifications before
> sending the JSON-RPC response. These messages SHOULD relate to the
> originating client request.
See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
Previously, when a GET SSE stream was connected, the Ruby SDK sent POST response bodies through
the GET stream and returned a 202 HTTP status for the POST itself. When no GET SSE stream was
connected, the SDK correctly returned `application/json` responses. The 202 behavior with GET SSE
was non-compliant with the specification, which requires POST requests to return `text/event-stream`
or `application/json`. Additionally, since GET SSE is optional per the specification, clients that
did not establish a GET SSE connection could not receive server-to-client messages (e.g.,
`sampling/createMessage`, log notifications) during request processing.
With this change, `handle_regular_request` always returns the POST response as an SSE stream for stateful sessions.
Each POST response stream is stored in `session[:post_request_streams]` keyed by `related_request_id` (the JSON-RPC request ID),
enabling correct routing when multiple POST requests are processed concurrently on the same session.
Server-to-client messages with a `related_request_id` are routed to the originating POST response stream only;
when `related_request_id` is nil, the GET SSE stream is used.
The TypeScript and Python SDKs already support this pattern.
### Internal Changes
`JsonRpcHandler.handle` and `JsonRpcHandler.handle_json` now pass both `method_name` and `request_id` to the method finder block.
This allows `Server#handle_request` to receive `related_request_id` directly from the protocol layer.
Without this, `related_request_id` would need to be relayed as a keyword argument through `Server#handle_json`,
`ServerSession#handle_json`, and `dispatch_handle_json`, unnecessarily exposing it on public method signatures.
This follows the same design as the TypeScript and Python SDKs, where the protocol layer extracts the request ID
and propagates it to the handler context.
## How Has This Been Tested?
Added tests for POST response stream:
- `send_request` via POST response stream (sampling with and without GET SSE)
- `send_notification` via POST response stream (logging without GET SSE)
- `progress` notification via POST response stream (without GET SSE)
- POST request returns SSE response even with GET SSE connected
- Session-scoped notifications (log, progress) are sent to POST
response stream, not GET SSE stream
- `active_stream` does not fall back to GET SSE when
`related_request_id` is given but request stream is missing
Updated existing tests to handle SSE response format where applicable.
## Breaking Changes (spec compliance fix)
POST responses for JSON-RPC requests in stateful sessions now return `Content-Type: text/event-stream`
instead of being sent through the GET SSE stream with a 202 HTTP status. Clients that relied on receiving responses
via the GET SSE stream will need to read the POST response body instead. This is a spec compliance fix:
the MCP specification requires POST requests to return `text/event-stream` or `application/json`,
not 202 with the response on a separate GET stream.
## 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.
## Motivation and Context
`cleanup_session_unsafe` was closing streams inside the mutex, which could block all session
operations if `stream.close` is slow or blocks (e.g., waiting for network I/O).
The original `reap_expired_sessions` avoided this by closing streams outside the mutex,
but the other 6 call sites of `cleanup_session_unsafe` did not, creating an inconsistency.
This change separates stream closing from session deletion. `cleanup_session_unsafe`
now only removes the session from `@sessions` and returns it.
All callers close streams outside the mutex via `close_stream_safely`,
ensuring consistent behavior and preventing mutex contention from blocking I/O.
## How Has This Been Tested?
Added tests that verify streams are closed outside the mutex by using `Mutex#try_lock`
inside mock stream close callbacks:
- `reap_expired_sessions closes stream outside mutex`
- `send_notification closes stream outside mutex on write error`
## Breaking Changes
None. This is an internal improvement with no change to public API behavior.
## Motivation and Context
The MCP specification defines `completion/complete` for providing autocompletion
suggestions for prompt arguments and resource template URIs.
The Ruby SDK previously had only a no-op handler that returned empty results,
with no way for users to define custom completion logic.
This aligns the Ruby SDK with the Python and TypeScript SDKs,
both of which support user-defined completion handlers.
## How Has This Been Tested?
Server tests cover: default handler, custom handler for `ref/prompt` and `ref/resource`,
context argument passing, 100-item truncation, and error responses for nonexistent prompts,
nonexistent resource templates, and invalid ref types.
Client tests cover: request structure, context parameter inclusion,
and fallback when result is missing.
## Breaking Changes
None. The existing default no-op handler behavior is preserved.
The `completion_handler` method is purely additive. The only behavioral change
is that `completion/complete` requests now validate that the referenced prompt
or resource template exists before calling the handler,
returning an `invalid_params` error for unknown references.
## Motivation and Context
Replace bare `rescue` with explicit `rescue StandardError` in stream cleanup to clarify intent,
with a comment explaining why.
In Ruby, `rescue` without an exception class is equivalent to `rescue StandardError`,
so this is a readability improvement with no behavior change.
## How Has This Been Tested?
Existing tests pass.
## Breaking Changes
None.
Per the MCP spec, invalid arguments and server errors are protocol
errors (JSON-RPC error responses), not tool execution errors
(isError: true). PR #165 moved all error cases to isError: true,
and PR #231 only restored protocol errors for unknown tools.
This fixes the remaining two cases: invalid arguments (-32602) and
server errors (-32603).
Ref: https://modelcontextprotocol.io/specification/2024-11-05/server/tools#error-handling