## Motivation and Context
The runnable examples drifted from the SDK they ship with:
- POST requests whose `Accept` header does not cover both `application/json` and `text/event-stream` are answered 406,
so the cURL walkthroughs no longer worked as printed.
- `MCP::Client#connect` now defaults to `mode: :auto`, which adopts the sessionless modern lifecycle (2026-07-28)
against a dual-era server; the Streamable HTTP client example requires a session and broke against its own server.
- POST responses on an established session arrive as an SSE stream, which the hand-rolled HTTP client did not parse.
- `notification_tool` claimed to send SSE notifications but only returned a `Tool::Response`.
- `http_client.rb` read a resource URI the server never registered and skipped `notifications/initialized`.
Handshake examples now offer the latest handshake protocol version (`2025-11-25`) instead of `2024-11-05`,
the Streamable HTTP client pins `mode: :legacy` for its session-based demo, `notification_tool` reports
real `notifications/progress` events requested through `_meta.progressToken`, and the interactive client streams
those events from the POST response so they are visible on screen.
The modern lifecycle itself had no runnable example, so a new server and client pair (`modern_http_server.rb`,
`modern_http_client.rb`) shows the sessionless flow end to end: `server/discover` capability discovery,
the per-request `_meta` envelope with the `Mcp-Method` and `Mcp-Name` headers (stamped by the SDK client automatically),
`resultType` stamping, SEP-2549 cache hints, a SEP-2322 multi round-trip `deploy` tool resumed automatically by
the client's elicitation handler, and the removal of legacy-only methods such as `ping`. The same server keeps serving
the legacy `initialize` flow, routed per request by the `MCP-Protocol-Version` header. `examples/README.md` documents
the pair and adds a cURL walkthrough covering the modern headers, the envelope, and the multi round-trip exchange.
## How Has This Been Tested?
- `ruby examples/stdio_client.rb` against `stdio_server.rb`
- `ruby examples/http_client.rb` against `http_server.rb`
- `ruby examples/streamable_http_client.rb` against `streamable_http_server.rb`, including the progress stream
- `ruby examples/modern_http_client.rb` against `modern_http_server.rb` (discover, modern adoption, cache hints,
`resultType`, the multi round-trip resume, `ping` rejection)
- The cURL walkthroughs in the banners and `examples/README.md`, including the `requestState` echo and a legacy `initialize`
against the modern example server
## Breaking Changes
None.
## Motivation and Context
The Rack 3 SPEC requires response header names to be lowercase.
https://github.com/rack/rack/blob/main/SPEC.rdoc says in its "The Headers" section:
> Header keys must not contain uppercase ASCII characters (A-Z).
https://github.com/rack/rack/blob/main/SPEC.rdoc#the-headers
However, `StreamableHTTPTransport` emitted capitalized names such as `Content-Type` and `Mcp-Session-Id`.
Any `Rack::Lint`-wrapped stack, including the default development mode of rackup, rejects every response
from the transport with a `LintError` 500. Lowercasing the names restores Rack 3 compliance.
HTTP header names are case-insensitive on the wire, so HTTP clients are unaffected;
only in-process consumers that read the raw Rack headers hash see the new keys.
Lowercase is also the only representation valid across HTTP versions: RFC 9113 makes uppercase field names
a protocol error on HTTP/2, so proxies and h2-capable servers already require it.
The TypeScript SDK sets `headers['mcp-session-id']` and the Python SDK defines `MCP_SESSION_ID_HEADER = "mcp-session-id"`,
so lowercase also matches what the reference SDKs emit; the MCP spec's `Mcp-Session-Id` casing is prose convention,
not a wire requirement.
Examples and documentation deliberately keep the spec's `Mcp-Session-Id` casing where it does not name
a Rack response hash key: curl invocations and `examples/streamable_http_client.rb` set request headers,
which the Rack response rule does not govern; the rack-cors `expose:` entry carries a header name as
an `Access-Control-Expose-Headers` value, which browsers match case-insensitively;
and README/docs prose follows the MCP specification's documentation convention.
Only code reading the raw Rack response headers hash, a case-sensitive Hash lookup, had to switch to lowercase keys.
AGENTS.md now records the lowercase rule under the code style guidelines, since the MCP spec's `Mcp-Session-Id` prose casing
invites reintroducing capitalized names in future transport work.
## How Has This Been Tested?
Added a regression test asserting that header names are lowercase in initialize, GET SSE, DELETE, and parse error responses.
Existing tests that read the raw headers hash were updated to the new keys. Verified with rake test, rake rubocop,
and rake conformance.
## Breaking Changes
Code that reads the raw Rack response headers hash returned by `handle_request`, or by middleware around the mounted transport,
with exact-case keys such as `"Mcp-Session-Id"` must switch to the lowercase keys. Clients speaking HTTP are
unaffected because header names are case-insensitive on the wire.
## Motivation and Context
The README.md documents the "Rails (mount)" integration pattern, but the repository
had no runnable Rails example. This adds a minimal Rails application under examples/rails
that mounts StreamableHTTPTransport at /mcp and demonstrates class-based tools and
a resource with a read handler.
The MCP server is built in config/routes.rb rather than in an initializer because
Zeitwerk sets up autoloading after initializers run, so the tool classes in app/tools
cannot be referenced there.
Code reloading is disabled because the transport holds the server built at boot.
The app is booted with puma instead of rackup because rackup inserts `Rack::Lint`
in its development default, which rejects the capitalized response header names
that the transport currently emits.
Closes#44.
## How Has This Been Tested?
The curl walkthrough in examples/rails/README.md has been verified end to end against
the running app: initialize and the initialized notification, tools/list, tools/call for
both tools, resources/list, resources/read, the standalone SSE stream,
and session termination via DELETE.
## Breaking Changes
None. This only adds an example application and documentation links.
The `tools/call` cURL example in `examples/README.md` uses `"name": "ExampleTool"`, but a tool registered by class without an explicit `tool_name` gets a snake_case name derived from its class name (via `StringUtils.handle_from_class_name`), so the example fails with `Tool not found: ExampleTool`. The Tools/Prompts lists in the same file had the same mismatch (`ExampleTool`, `ExamplePrompt`, `NotificationTool`). All entries now use the registered callable names.
How Tested: Ran `ruby examples/http_server.rb` and exercised the cURL flow. `tools/list` returned `example_tool` and `echo`; `tools/call` with `"name": "ExampleTool"` returned `Tool not found: ExampleTool`; with `"name": "example_tool"` returned `The sum of 5 and 3 is 8`. `prompts/list` returned `example_prompt`. The full unit suite and RuboCop pass locally.
Breaking Changes: None (documentation only).
## Motivation and Context
This commit implements Phase 1 of 3 for #334.
The MCP specification requires an `initialize` request followed by
a `notifications/initialized` notification before any other interaction.
PR #327 added `MCP::Client#connect` and `MCP::Client::HTTP#connect`
to expose this handshake explicitly for the HTTP transport,
but stdio remained on its private `initialize_session` lazy-init path,
inconsistent with the Python SDK (`ClientSession.initialize()`) and
TypeScript SDK (`Client.connect(transport)`),
both of which require an explicit handshake call regardless of transport.
The implicit init path on the first `send_request` is
preserved as a non-breaking compatibility shim. Public API added on `Stdio`:
- `connect(client_info:, protocol_version:, capabilities:)` performs the
handshake, validates the negotiated `protocolVersion` against
`MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS`,
caches the `InitializeResult`, and is idempotent.
- `connected?` reports handshake completion.
- `server_info` exposes the cached `InitializeResult`.
Documentation and examples are updated so new users learn the explicit pattern
from the start.
## How Has This Been Tested?
Fifteen regression tests cover: explicit handshake, `server_info` caching,
idempotence, custom keyword arguments, JSON-RPC error response, missing
or non-Hash `result`, unsupported protocol version with state rollback,
state rollback when the `notifications/initialized` write fails,
retry after a failed handshake, and the `connected?` / `server_info` lifecycle.
The pre-existing stdio tests pass without modification, verifying that
the implicit-init compatibility path still works.
## Breaking Changes
None for the typical "implicit init via `client.tools` / `client.call_tool`" usage.
Existing stdio clients keep working unchanged.
Three observable behavior changes for code that inspects transport state directly:
- `Client#connected?` for a never-initialized stdio client returns `false`
(previously fell through to a `true` default via the `respond_to?` fallback).
- `Client#server_info` for stdio returns the cached `InitializeResult`
after the handshake (previously always `nil` via the fallback).
- The implicit handshake path now validates the server's negotiated
protocol version, raising `RequestHandlerError` for unsupported versions.
Production servers that return one of the supported versions are unaffected;
this closes a pre-existing spec-conformance gap on stdio.
## Motivation and Context
The example files `examples/http_client.rb` and `examples/streamable_http_client.rb` are
intended to be run as standalone scripts and are not designed to be reused as modules (e.g., via `require`).
The `if __FILE__ == $PROGRAM_NAME` guard therefore serves no purpose and is removed.
## How Has This Been Tested?
Manually verified that each example script can still be executed directly
(`ruby examples/http_client.rb`, `ruby examples/streamable_http_client.rb`).
No automated tests cover files under `examples/`.
## Breaking Changes
None. The files under `examples/` are standalone scripts and are not part of the gem's public API.
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>
## 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
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`.
## 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.
Even though the 'tools/call' request to 'notification_tool' is successful and an 'MCP::Tool::Response' is returned, the logger still prints 'Error:'.
```
% ruby examples/streamable_http_client.rb
=== MCP SSE Test Client ===
[CLIENT] INFO 15:41:04.696 - Initializing session...
[CLIENT] INFO 15:41:04.698 - Session initialized: SESSION_ID
[CLIENT] INFO 15:41:04.698 - Server info: {"name" => "sse_test_server", "version" => "0.1.0"}
[CLIENT] INFO 15:41:04.698 - Connecting to SSE stream...
[CLIENT] INFO 15:41:04.698 - SSE stream connected successfully
=== Available Actions ===
1. Send custom notification
2. Test echo
3. List tools
0. Exit
Choose an action: 1
Enter notification message: notification test
Enter delay in seconds (0 for immediate):
[CLIENT] INFO 15:41:11.248 - SSE data: {"jsonrpc":"2.0","id":"0f657eeb-3e05-47bb-b7b4-898e2cb503af","result":{"content":[{"type":"text","text":"Notification: notification test (timestamp: 2025-06-21T15:41:11+09:00)"}],"isError":false}}
[CLIENT] ERROR 15:41:11.249 - Error:
=== Available Actions ===
1. Send custom notification
2. Test echo
3. List tools
0. Exit
```
It is probably trying to output an MCP::Tool::Response, but these appear in the SSE stream. Therefore, the response it is handling this time should either be accepted or an error.
I modified it so that it could handle them.
Since the example files are not executable binaries, this PR removes the shebangs.
This ensures that all examples are consistently executed via `ruby` command.
It also removes the executable permission from `examples/stdio_server.rb` using `chmod 444`,
bringing it in line with the other example files.
This PR renamed the files related to the SSE example and fixed the README.md.
Because missing files were specified in README.md.
And also, I removed the word 'test' from the file names related to the SSE example.
Other examples file names do not contain the word `test`.
Follow-up to #27.
This PR fixes the following build error:
```console
$ bundle exec rake
/Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'Kernel.require': cannot load such file -- mcp/transports/stdio (LoadError)
Did you mean? mcp/transports/stdio_transport_test
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'block (2 levels) in Kernel#replace_require'
from /Users/koic/src/github.com/modelcontextprotocol/ruby-sdk/test/mcp/transports/stdio_transport_test.rb:4:in '<top (required)>'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'Kernel.require'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'block (2 levels) in Kernel#replace_require'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:21:in 'block in <main>'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:6:in 'Array#select'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:6:in '<main>'
rake aborted!
Command failed with status (1)
/Users/koic/.rbenv/versions/3.5-dev/bin/bundle:25:in 'Kernel#load'
/Users/koic/.rbenv/versions/3.5-dev/bin/bundle:25:in '<main>'
Tasks: TOP => default => test
(See full trace by running task with --trace)
```
https://github.com/modelcontextprotocol/ruby-sdk/actions/runs/15428236624/job/43420346011
The following is stated in #27.
> Breaking Changes
> `MCP::Transports::StdioTransport` is now `MCP::Server::Transports::StdioTransport`
What is shown here is the new name `MCP::Server::Transports::StdioTransport`, not `MCP::Server::Transports::Stdio`.
Therefore, the file path has been updated from `lib/mcp/server/transports/stdio.rb` to `lib/mcp/server/transports/stdio_transport.rb` to
match the module name. If `MCP::Server::Transports::Stdio` is preferred instead, please let me know and I will open a separate PR.
- add support for resource templates
- improve capability negotiation on handlers
- clean up readme
- reduce default protocol version to "2024-11-05"
- address spec compliance of resources
- add debug gem to development
- alias ModelContextProtocol to MCP
- move prompt subclasses to subdir
- improve tool and prompt definition
- improve passing of server context to tools
- improve tool input schema validation