## 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.
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
`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
Background threads simulating server responses could race with the `ensure` block
that closes IO pipes, intermittently producing this warning depending on thread scheduling:
```console
#<Thread:0x00007fb69290cbd0 test/mcp/client/stdio_test.rb:28 run>
terminated with exception (report_on_exception is true):
test/mcp/client/stdio_test.rb:55:in `write': stream closed in another thread (IOError)
```
Store each background thread in a `server_thread` variable and call `server_thread&.join(1)`
at the top of each `ensure` block to wait for the thread to finish before closing pipes.
## 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`.
- Set Accept header to request JSON or SSE responses
- Validate response Content-Type and raise error for non-JSON responses
- Add Accept header to test fixtures
- Use explicit hash syntax for Ruby 2.7 compatibility
This commit implements optional JSON Schema validation for tool outputs according to the MCP specification:
- Add MCP::Tool::OutputSchema class mirroring InputSchema functionality
- Support output_schema in class definitions, Tool.define, and OutputSchema objects
- Include validate_result method for runtime validation
- Update MCP::Client::Tool to track output schemas
- Add comprehensive test coverage with 249 passing tests
- Update README with dedicated Tool Output Schemas section
- Follow MCP spec requirements for server/client validation