Skip to content

Commit 09b4e30

Browse files
authored
Merge pull request #262 from koic/stdio_client_transport
Add `MCP::Client::Stdio` transport
2 parents 283c7a4 + 21eef8d commit 09b4e30

File tree

7 files changed

+1079
-4
lines changed

7 files changed

+1079
-4
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ AllCops:
1111
Gemspec/DevelopmentDependencies:
1212
Enabled: true
1313

14+
Lint/IncompatibleIoSelectWithFiberScheduler:
15+
Enabled: true
16+
1417
Minitest/LiteralAsActualArgument:
1518
Enabled: true

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,52 @@ class CustomTransport
10821082
end
10831083
```
10841084

1085+
### Stdio Transport Layer
1086+
1087+
Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
1088+
1089+
`MCP::Client::Stdio.new` accepts the following keyword arguments:
1090+
1091+
| Parameter | Required | Description |
1092+
|---|---|---|
1093+
| `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
1094+
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
1095+
| `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
1096+
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
1097+
1098+
Example usage:
1099+
1100+
```ruby
1101+
stdio_transport = MCP::Client::Stdio.new(
1102+
command: "bundle",
1103+
args: ["exec", "ruby", "path/to/server.rb"],
1104+
env: { "API_KEY" => "my_secret_key" },
1105+
read_timeout: 30
1106+
)
1107+
client = MCP::Client.new(transport: stdio_transport)
1108+
1109+
# List available tools.
1110+
tools = client.tools
1111+
tools.each do |tool|
1112+
puts "Tool: #{tool.name} - #{tool.description}"
1113+
end
1114+
1115+
# Call a specific tool.
1116+
response = client.call_tool(
1117+
tool: tools.first,
1118+
arguments: { message: "Hello, world!" }
1119+
)
1120+
1121+
# Close the transport when done.
1122+
stdio_transport.close
1123+
```
1124+
1125+
The stdio transport automatically handles:
1126+
1127+
- Spawning the server process with `Open3.popen3`
1128+
- MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
1129+
- JSON-RPC 2.0 message framing over newline-delimited JSON
1130+
10851131
### HTTP Transport Layer
10861132

10871133
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.

examples/README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,26 @@ $ ruby examples/stdio_server.rb
1515
{"jsonrpc":"2.0","id":0,"method":"tools/list"}
1616
```
1717

18-
### 2. HTTP Server (`http_server.rb`)
18+
### 2. STDIO Client (`stdio_client.rb`)
19+
20+
A client that connects to the STDIO server using the `MCP::Client::Stdio` transport.
21+
This demonstrates how to use the SDK's built-in client classes to interact with a server subprocess.
22+
23+
**Usage:**
24+
25+
```console
26+
$ ruby examples/stdio_client.rb
27+
```
28+
29+
The client will automatically launch `stdio_server.rb` as a subprocess and demonstrate:
30+
31+
- Listing and calling tools
32+
- Listing prompts
33+
- Listing and reading resources
34+
- Automatic MCP protocol initialization
35+
- Transport cleanup on exit
36+
37+
### 3. HTTP Server (`http_server.rb`)
1938

2039
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.
2140

@@ -41,7 +60,7 @@ The server will start on `http://localhost:9292` and provide:
4160
- **Prompts**: `ExamplePrompt` - echoes back arguments as a prompt
4261
- **Resources**: `test_resource` - returns example content
4362

44-
### 3. HTTP Client Example (`http_client.rb`)
63+
### 4. HTTP Client Example (`http_client.rb`)
4564

4665
A client that demonstrates how to interact with the HTTP server using all MCP protocol methods.
4766

@@ -67,7 +86,7 @@ The client will demonstrate:
6786
- Listing and reading resources
6887
- Session cleanup
6988

70-
### 4. Streamable HTTP Server (`streamable_http_server.rb`)
89+
### 5. Streamable HTTP Server (`streamable_http_server.rb`)
7190

7291
A specialized HTTP server designed to test and demonstrate Server-Sent Events (SSE) functionality in the MCP protocol.
7392

@@ -90,7 +109,7 @@ $ ruby examples/streamable_http_server.rb
90109

91110
The server will start on `http://localhost:9393` and provide detailed instructions for testing SSE functionality.
92111

93-
### 5. Streamable HTTP Client (`streamable_http_client.rb`)
112+
### 6. Streamable HTTP Client (`streamable_http_client.rb`)
94113

95114
An interactive client that connects to the SSE stream and provides a menu-driven interface for testing SSE functionality.
96115

examples/stdio_client.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4+
require "mcp"
5+
require "json"
6+
7+
# Simple stdio client example that connects to the stdio_server.rb example
8+
# Usage: ruby examples/stdio_client.rb
9+
10+
server_script = File.expand_path("stdio_server.rb", __dir__)
11+
12+
transport = MCP::Client::Stdio.new(command: "ruby", args: [server_script])
13+
client = MCP::Client.new(transport: transport)
14+
15+
begin
16+
# List available tools
17+
puts "=== Listing tools ==="
18+
tools = client.tools
19+
tools.each do |tool|
20+
puts " Tool: #{tool.name} - #{tool.description}"
21+
end
22+
23+
# Call the example_tool (adds two numbers)
24+
puts "\n=== Calling tool: example_tool ==="
25+
tool = tools.find { |t| t.name == "example_tool" }
26+
response = client.call_tool(tool: tool, arguments: { a: 5, b: 3 })
27+
puts " Response: #{JSON.pretty_generate(response.dig("result", "content"))}"
28+
29+
# Call the echo tool
30+
puts "\n=== Calling tool: echo ==="
31+
tool = tools.find { |t| t.name == "echo" }
32+
response = client.call_tool(tool: tool, arguments: { message: "Hello from stdio client!" })
33+
puts " Response: #{JSON.pretty_generate(response.dig("result", "content"))}"
34+
35+
# List prompts
36+
puts "\n=== Listing prompts ==="
37+
prompts = client.prompts
38+
prompts.each do |prompt|
39+
puts " Prompt: #{prompt["name"]} - #{prompt["description"]}"
40+
end
41+
42+
# List resources
43+
puts "\n=== Listing resources ==="
44+
resources = client.resources
45+
resources.each do |resource|
46+
puts " Resource: #{resource["name"]} (#{resource["uri"]})"
47+
end
48+
49+
# Read a resource
50+
puts "\n=== Reading resource: https://test_resource.invalid ==="
51+
contents = client.read_resource(uri: "https://test_resource.invalid")
52+
puts " Response: #{JSON.pretty_generate(contents)}"
53+
rescue => e
54+
puts "Error: #{e.message}"
55+
puts e.backtrace.first(5).join("\n")
56+
ensure
57+
transport.close
58+
end

lib/mcp/client.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative "client/stdio"
34
require_relative "client/http"
45
require_relative "client/tool"
56

0 commit comments

Comments
 (0)