Some 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.
## 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`.
## Motivation and Context
`handle_delete` had an inline error response for "Missing session ID" that
was identical to the existing `missing_session_id_response` helper method.
`handle_get` already uses this helper for the same purpose.
## How Has This Been Tested?
All existing tests pass. The behavior is unchanged.
## Breaking Change
None.
## Motivation and Context
`handle_regular_request` was checking `@sessions.key?(session_id)` directly
without holding `@mutex`, while concurrent threads could modify `@sessions`
via `cleanup_session` or `handle_delete`. This created a TOCTOU race where
the check could pass but the session could be deleted before subsequent use.
The class already provides a mutex-protected `session_exists?` helper, and
`handle_get` already uses it. This change makes `handle_regular_request`
consistent with `handle_get`.
## How Has This Been Tested?
Added a test that verifies `handle_regular_request` delegates to the
mutex-protected `session_exists?` helper instead of accessing
`@sessions` directly.
All existing tests pass.
## Breaking Change
None.
## Motivation and Context
The MCP specification requires that when a server receives a request containing
a session ID that is no longer valid, it MUST respond with HTTP 404 Not Found.
The `handle_get` path already returned 404 correctly, but `handle_regular_request`
(the POST path) was returning 400 "Invalid session ID" instead.
Ref: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
> The server MAY terminate the session at any time, after which it MUST respond
> to requests containing that session ID with HTTP 404 Not Found.
## How Has This Been Tested?
Added a test for POST requests with an invalid session ID, verifying that the
response is 404 with "Session not found" error message. All existing tests pass.
## Breaking Change
POST requests with an invalid session ID now return HTTP 404 "Session not found"
instead of HTTP 400 "Invalid session ID". Clients that match on the 400 status
code or the old error message will need to be updated.
However, this change is considered a bug fix because it brings the behavior into compliance with
the MCP specification.
Notifications never reach `handle_regular_request` because `handle_post`
checks `notification?(body)` first and returns 202 via `handle_accepted`.
Since only notifications cause `handle_json` to return `nil`, `response`
is always non-`nil` here.
## Motivation and Context
`lib/mcp.rb` currently declares all `require_relative` statements in one place.
As a result, Server and Client are both loaded, the stdio and Streamable HTTP
transports are both loaded, and Tool, Prompt, and Resource are all loaded,
regardless of which components are actually used.
These components are selected by the user depending on their use case,
so loading everything upfront is unnecessary.
Using `autoload` for selected constants defers loading until the constant
is actually referenced, so only the subsystems required by the user are loaded.
## How Has This Been Tested?
All existing tests pass unchanged.
## Breaking Change
None. This change only defers loading of unused components.
## 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.
## Summary
When using browser-based MCP clients such as MCP Inspector, connections to
the example HTTP server failed due to two issues:
1. CORS preflight (OPTIONS) requests returned 405 Method Not Allowed,
causing browsers to block all cross-origin requests.
2. `Accept: */*` (commonly sent by browsers and fetch API) returned 406 Not Acceptable
because the transport required the Accept header to explicitly list `application/json`
and `text/event-stream`.
Add `rack-cors` middleware to the example HTTP servers so that preflight requests are handled correctly
and `Mcp-Session-Id` is exposed to browser clients. Also fix `validate_accept_header` in `StreamableHTTPTransport`
to treat `*/*` as satisfying all required Accept types.
Fixes#141.
## Repro Steps
### 1. Start the example server (Terminal 1)
```console
$ bundle exec ruby examples/streamable_http_server.rb
```
### 2. Start MCP Inspector (Terminal 2)
```console
$ npx @modelcontextprotocol/inspector
```
### 3. Open the Inspector Web UI (http://localhost:6274) in a browser
- Set Transport Type to "Streamable HTTP"
- Set URL to http://localhost:9393
- Disable the Authorization header toggle (the example server does not require authentication)
- Click "Connect"
Before this change, the connection fails due to CORS or 406 errors.
After this change, the connection succeeds and tools are listed.
These steps have been added to examples/README.md.
Some fixes have been identified during the implementation of the conformance infrastructure.
It should be possible to proceed with a release at this stage.
## 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.
`Errno::ECONNRESET` is a `SystemCallError`, not an `IOError`, so it was
not caught by the existing `rescue IOError, Errno::EPIPE` clauses. When
a client disconnected with a TCP RST during SSE notification broadcast,
the unhandled exception aborted the entire broadcast loop, causing
notifications to be lost for all remaining sessions.
This caused the `tools-call-with-logging` conformance scenario to fail
in CI (#248), where stale sessions from prior scenarios triggered
`ECONNRESET` on the first notification write, aborting the broadcast
and losing log notifications.
Add `Errno::ECONNRESET` to all four rescue clauses in
`StreamableHTTPTransport`: `send_notification` (targeted and broadcast),
`send_response_to_stream`, and `send_keepalive_ping`.
`Prompt#validate_arguments!` called `required_args` which does `arguments_value.filter_map`,
crashing with `NoMethodError` when `arguments_value` is `nil` (the default for prompts without
declared arguments). Additionally, passing `nil` as args caused a crash on `args.keys`.
Fix by defaulting `arguments_value` to `[]` and guarding against `nil` args.
The no-op handlers for `completion/complete`, `resources/subscribe`,
and `resources/unsubscribe` returned `nil`.
When a client accesses the expected response structure (e.g., `result[:completion][:values]`),
a `nil` result raises `NoMethodError`.
The MCP spec requires specific response structures even for no-op handlers.
Returning spec-compliant defaults ensures that clients receive valid responses without
requiring servers to explicitly override each handler.
`completion/complete` now returns `{ completion: { values: [], hasMore: false } }`.
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion
`resources/subscribe` and `resources/unsubscribe` now return `{}` (empty result).
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/resources#subscriptions
This is a fix for a similar bug that remained, as in #235.
`Content::Image#to_h` returned `mime_type` (snake_case), but the MCP spec
requires `mimeType` (camelCase) for the MIME type field in image content.
`gem build mcp.gemspec` outputs the built gem into the working directory (e.g. mcp-0.7.1.gem)
rather than pkg/, which is already ignored.
Without this entry, the file shows up as an untracked file and risks being accidentally
staged and committed during release preparation or local development.
The current release package contains files that are not required at runtime,
such as `.github`, `docs`, and `examples`.
```ruby
Dir.chdir(File.expand_path("..", __FILE__)) do
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
=>
[".gitattributes",
".github/dependabot.yml",
".github/workflows/ci.yml",
".github/workflows/release.yml",
".gitignore",
".rubocop.yml",
"AGENTS.md",
"CHANGELOG.md",
"CODE_OF_CONDUCT.md",
"Gemfile",
"LICENSE",
(snip)
```
This PR removes unnecessary files where possible to reduce the size of the release package.
Due to overly aggressive detection by RuboCop Minitest, the distinction between `[]` and `{}` was uniformly replaced with `assert_empty`,
making it impossible to differentiate between them.
As a result of https://github.com/rubocop/rubocop-minitest/issues/344, where the `Minitest/AssertEmptyLiteral` cop was disabled,
the return value has been expressed using explicit literal forms.