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
2 changed files with 1079 additions and 4 deletions

View file

@ -15,7 +15,26 @@ $ ruby examples/stdio_server.rb
{"jsonrpc":"2.0","id":0,"method":"tools/list"}
```
### 2. HTTP Server (`http_server.rb`)
### 2. STDIO Client (`stdio_client.rb`)
A client that connects to the STDIO server using the `MCP::Client::Stdio` transport.
This demonstrates how to use the SDK's built-in client classes to interact with a server subprocess.
**Usage:**
```console
$ ruby examples/stdio_client.rb
```
The client will automatically launch `stdio_server.rb` as a subprocess and demonstrate:
- Listing and calling tools
- Listing prompts
- Listing and reading resources
- Automatic MCP protocol initialization
- Transport cleanup on exit
### 3. HTTP Server (`http_server.rb`)
A standalone HTTP server built with Rack that implements the MCP Streamable HTTP transport protocol. This demonstrates how to create a web-based MCP server with session management and Server-Sent Events (SSE) support.
@ -41,7 +60,7 @@ The server will start on `http://localhost:9292` and provide:
- **Prompts**: `ExamplePrompt` - echoes back arguments as a prompt
- **Resources**: `test_resource` - returns example content
### 3. HTTP Client Example (`http_client.rb`)
### 4. HTTP Client Example (`http_client.rb`)
A client that demonstrates how to interact with the HTTP server using all MCP protocol methods.
@ -67,7 +86,7 @@ The client will demonstrate:
- Listing and reading resources
- Session cleanup
### 4. Streamable HTTP Server (`streamable_http_server.rb`)
### 5. Streamable HTTP Server (`streamable_http_server.rb`)
A specialized HTTP server designed to test and demonstrate Server-Sent Events (SSE) functionality in the MCP protocol.
@ -90,7 +109,7 @@ $ ruby examples/streamable_http_server.rb
The server will start on `http://localhost:9393` and provide detailed instructions for testing SSE functionality.
### 5. Streamable HTTP Client (`streamable_http_client.rb`)
### 6. Streamable HTTP Client (`streamable_http_client.rb`)
An interactive client that connects to the SSE stream and provides a menu-driven interface for testing SSE functionality.

58
examples/stdio_client.rb Normal file
View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "mcp"
require "json"
# Simple stdio client example that connects to the stdio_server.rb example
# Usage: ruby examples/stdio_client.rb
server_script = File.expand_path("stdio_server.rb", __dir__)
transport = MCP::Client::Stdio.new(command: "ruby", args: [server_script])
client = MCP::Client.new(transport: transport)
begin
# List available tools
puts "=== Listing tools ==="
tools = client.tools
tools.each do |tool|
puts " Tool: #{tool.name} - #{tool.description}"
end
# Call the example_tool (adds two numbers)
puts "\n=== Calling tool: example_tool ==="
tool = tools.find { |t| t.name == "example_tool" }
response = client.call_tool(tool: tool, arguments: { a: 5, b: 3 })
puts " Response: #{JSON.pretty_generate(response.dig("result", "content"))}"
# Call the echo tool
puts "\n=== Calling tool: echo ==="
tool = tools.find { |t| t.name == "echo" }
response = client.call_tool(tool: tool, arguments: { message: "Hello from stdio client!" })
puts " Response: #{JSON.pretty_generate(response.dig("result", "content"))}"
# List prompts
puts "\n=== Listing prompts ==="
prompts = client.prompts
prompts.each do |prompt|
puts " Prompt: #{prompt["name"]} - #{prompt["description"]}"
end
# List resources
puts "\n=== Listing resources ==="
resources = client.resources
resources.each do |resource|
puts " Resource: #{resource["name"]} (#{resource["uri"]})"
end
# Read a resource
puts "\n=== Reading resource: https://test_resource.invalid ==="
contents = client.read_resource(uri: "https://test_resource.invalid")
puts " Response: #{JSON.pretty_generate(contents)}"
rescue => e
puts "Error: #{e.message}"
puts e.backtrace.first(5).join("\n")
ensure
transport.close
end