Skip to content

Commit dcc31ca

Browse files
Merge pull request #27 from jcat4/refactor-for-client-support
Refactor in preparation for client support
2 parents 08abc3e + 0fbd229 commit dcc31ca

File tree

6 files changed

+183
-51
lines changed

6 files changed

+183
-51
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ If you want to build a local command-line application, you can use the stdio tra
8686
```ruby
8787
#!/usr/bin/env ruby
8888
require "mcp"
89-
require "mcp/transports/stdio"
89+
require "mcp/server/transports/stdio"
9090

9191
# Create a simple tool
9292
class ExampleTool < MCP::Tool
@@ -115,7 +115,7 @@ server = MCP::Server.new(
115115
)
116116

117117
# Create and start the transport
118-
transport = MCP::Transports::StdioTransport.new(server)
118+
transport = MCP::Server::Transports::StdioTransport.new(server)
119119
transport.open
120120
```
121121

examples/stdio_server.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
55
require "mcp"
6-
require "mcp/transports/stdio"
6+
require "mcp/server/transports/stdio"
77

88
# Create a simple tool
99
class ExampleTool < MCP::Tool
@@ -91,5 +91,5 @@ def template(args, server_context:)
9191
end
9292

9393
# Create and start the transport
94-
transport = MCP::Transports::StdioTransport.new(server)
94+
transport = MCP::Server::Transports::StdioTransport.new(server)
9595
transport.open

lib/mcp.rb

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
# frozen_string_literal: true
22

3-
require_relative "mcp/server"
4-
require_relative "mcp/string_utils"
5-
require_relative "mcp/tool"
6-
require_relative "mcp/tool/input_schema"
7-
require_relative "mcp/tool/annotations"
8-
require_relative "mcp/tool/response"
3+
require_relative "mcp/configuration"
94
require_relative "mcp/content"
10-
require_relative "mcp/resource"
11-
require_relative "mcp/resource/contents"
12-
require_relative "mcp/resource/embedded"
13-
require_relative "mcp/resource_template"
5+
require_relative "mcp/instrumentation"
6+
require_relative "mcp/methods"
147
require_relative "mcp/prompt"
158
require_relative "mcp/prompt/argument"
169
require_relative "mcp/prompt/message"
1710
require_relative "mcp/prompt/result"
11+
require_relative "mcp/resource"
12+
require_relative "mcp/resource/contents"
13+
require_relative "mcp/resource/embedded"
14+
require_relative "mcp/resource_template"
15+
require_relative "mcp/server"
16+
require_relative "mcp/server/transports/stdio"
17+
require_relative "mcp/string_utils"
18+
require_relative "mcp/tool"
19+
require_relative "mcp/tool/input_schema"
20+
require_relative "mcp/tool/response"
21+
require_relative "mcp/tool/annotations"
22+
require_relative "mcp/transport"
1823
require_relative "mcp/version"
19-
require_relative "mcp/configuration"
20-
require_relative "mcp/methods"
2124

2225
module MCP
2326
class << self

lib/mcp/server/transports/stdio.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../transport"
4+
require "json"
5+
6+
module MCP
7+
class Server
8+
module Transports
9+
class StdioTransport < Transport
10+
def initialize(server)
11+
@server = server
12+
@open = false
13+
$stdin.set_encoding("UTF-8")
14+
$stdout.set_encoding("UTF-8")
15+
super
16+
end
17+
18+
def open
19+
@open = true
20+
while @open && (line = $stdin.gets)
21+
handle_json_request(line.strip)
22+
end
23+
end
24+
25+
def close
26+
@open = false
27+
end
28+
29+
def send_response(message)
30+
json_message = message.is_a?(String) ? message : JSON.generate(message)
31+
$stdout.puts(json_message)
32+
$stdout.flush
33+
end
34+
end
35+
end
36+
end
37+
end

lib/mcp/transports/stdio.rb

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "mcp/server/transports/stdio"
5+
require "json"
6+
7+
module MCP
8+
class Server
9+
module Transports
10+
class StdioTest < ActiveSupport::TestCase
11+
include InstrumentationTestHelper
12+
13+
setup do
14+
configuration = MCP::Configuration.new
15+
configuration.instrumentation_callback = instrumentation_helper.callback
16+
@server = Server.new(name: "test_server", configuration: configuration)
17+
@transport = StdioTransport.new(@server)
18+
end
19+
20+
test "initializes with server and closed state" do
21+
server = @transport.instance_variable_get(:@server)
22+
assert_equal @server.object_id, server.object_id
23+
refute @transport.instance_variable_get(:@open)
24+
end
25+
26+
test "processes JSON-RPC requests from stdin and sends responses to stdout" do
27+
request = {
28+
jsonrpc: "2.0",
29+
method: "ping",
30+
id: "123",
31+
}
32+
input = StringIO.new(JSON.generate(request) + "\n")
33+
output = StringIO.new
34+
35+
original_stdin = $stdin
36+
original_stdout = $stdout
37+
38+
begin
39+
$stdin = input
40+
$stdout = output
41+
42+
thread = Thread.new { @transport.open }
43+
sleep(0.1)
44+
@transport.close
45+
thread.join
46+
47+
response = JSON.parse(output.string, symbolize_names: true)
48+
assert_equal("2.0", response[:jsonrpc])
49+
assert_equal("123", response[:id])
50+
assert_empty(response[:result])
51+
refute(@transport.instance_variable_get(:@open))
52+
ensure
53+
$stdin = original_stdin
54+
$stdout = original_stdout
55+
end
56+
end
57+
58+
test "sends string responses to stdout" do
59+
output = StringIO.new
60+
original_stdout = $stdout
61+
62+
begin
63+
$stdout = output
64+
@transport.send_response("test response")
65+
assert_equal("test response\n", output.string)
66+
ensure
67+
$stdout = original_stdout
68+
end
69+
end
70+
71+
test "sends JSON responses to stdout" do
72+
output = StringIO.new
73+
original_stdout = $stdout
74+
75+
begin
76+
$stdout = output
77+
response = { key: "value" }
78+
@transport.send_response(response)
79+
assert_equal(JSON.generate(response) + "\n", output.string)
80+
ensure
81+
$stdout = original_stdout
82+
end
83+
end
84+
85+
test "handles valid JSON-RPC requests" do
86+
request = {
87+
jsonrpc: "2.0",
88+
method: "ping",
89+
id: "123",
90+
}
91+
output = StringIO.new
92+
original_stdout = $stdout
93+
94+
begin
95+
$stdout = output
96+
@transport.send(:handle_request, JSON.generate(request))
97+
response = JSON.parse(output.string, symbolize_names: true)
98+
assert_equal("2.0", response[:jsonrpc])
99+
assert_nil(response[:id])
100+
assert_nil(response[:result])
101+
ensure
102+
$stdout = original_stdout
103+
end
104+
end
105+
106+
test "handles invalid JSON requests" do
107+
invalid_json = "invalid json"
108+
output = StringIO.new
109+
original_stdout = $stdout
110+
111+
begin
112+
$stdout = output
113+
@transport.send(:handle_request, invalid_json)
114+
response = JSON.parse(output.string, symbolize_names: true)
115+
assert_equal("2.0", response[:jsonrpc])
116+
assert_nil(response[:id])
117+
assert_equal(-32600, response[:error][:code])
118+
assert_equal("Invalid Request", response[:error][:message])
119+
assert_equal("Request must be an array or a hash", response[:error][:data])
120+
ensure
121+
$stdout = original_stdout
122+
end
123+
end
124+
end
125+
end
126+
end
127+
end

0 commit comments

Comments
 (0)