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`.
This commit is contained in:
Koichi ITO 2026-03-18 03:37:27 +09:00
commit 21eef8df9a

View file

@ -1082,6 +1082,52 @@ class CustomTransport
end
```
### Stdio Transport Layer
Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
`MCP::Client::Stdio.new` accepts the following keyword arguments:
| Parameter | Required | Description |
|---|---|---|
| `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
| `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
Example usage:
```ruby
stdio_transport = MCP::Client::Stdio.new(
command: "bundle",
args: ["exec", "ruby", "path/to/server.rb"],
env: { "API_KEY" => "my_secret_key" },
read_timeout: 30
)
client = MCP::Client.new(transport: stdio_transport)
# List available tools.
tools = client.tools
tools.each do |tool|
puts "Tool: #{tool.name} - #{tool.description}"
end
# Call a specific tool.
response = client.call_tool(
tool: tools.first,
arguments: { message: "Hello, world!" }
)
# Close the transport when done.
stdio_transport.close
```
The stdio transport automatically handles:
- Spawning the server process with `Open3.popen3`
- MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
- JSON-RPC 2.0 message framing over newline-delimited JSON
### HTTP Transport Layer
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.