Merge pull request #27 from jcat4/refactor-for-client-support

Refactor in preparation for client support
This commit is contained in:
Topher Bullock 2025-06-03 17:27:54 -04:00 • committed by GitHub
commit dcc31ca6fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 183 additions and 51 deletions

View file

@ -86,7 +86,7 @@ If you want to build a local command-line application, you can use the stdio tra
```ruby
#!/usr/bin/env ruby
require "mcp"
require "mcp/transports/stdio"
require "mcp/server/transports/stdio"
# Create a simple tool
class ExampleTool < MCP::Tool
@ -115,7 +115,7 @@ server = MCP::Server.new(
)
# Create and start the transport
transport = MCP::Transports::StdioTransport.new(server)
transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open
```

View file

@ -3,7 +3,7 @@
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "mcp"
require "mcp/transports/stdio"
require "mcp/server/transports/stdio"
# Create a simple tool
class ExampleTool < MCP::Tool
@ -91,5 +91,5 @@ server.resources_read_handler do |params|
end
# Create and start the transport
transport = MCP::Transports::StdioTransport.new(server)
transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open

View file

@ -1,23 +1,26 @@
# frozen_string_literal: true
require_relative "mcp/server"
require_relative "mcp/string_utils"
require_relative "mcp/tool"
require_relative "mcp/tool/input_schema"
require_relative "mcp/tool/annotations"
require_relative "mcp/tool/response"
require_relative "mcp/configuration"
require_relative "mcp/content"
require_relative "mcp/resource"
require_relative "mcp/resource/contents"
require_relative "mcp/resource/embedded"
require_relative "mcp/resource_template"
require_relative "mcp/instrumentation"
require_relative "mcp/methods"
require_relative "mcp/prompt"
require_relative "mcp/prompt/argument"
require_relative "mcp/prompt/message"
require_relative "mcp/prompt/result"
require_relative "mcp/resource"
require_relative "mcp/resource/contents"
require_relative "mcp/resource/embedded"
require_relative "mcp/resource_template"
require_relative "mcp/server"
require_relative "mcp/server/transports/stdio"
require_relative "mcp/string_utils"
require_relative "mcp/tool"
require_relative "mcp/tool/input_schema"
require_relative "mcp/tool/response"
require_relative "mcp/tool/annotations"
require_relative "mcp/transport"
require_relative "mcp/version"
require_relative "mcp/configuration"
require_relative "mcp/methods"
module MCP
class << self

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
require_relative "../../transport"
require "json"
module MCP
class Server
module Transports
class StdioTransport < Transport
def initialize(server)
@server = server
@open = false
$stdin.set_encoding("UTF-8")
$stdout.set_encoding("UTF-8")
super
end
def open
@open = true
while @open && (line = $stdin.gets)
handle_json_request(line.strip)
end
end
def close
@open = false
end
def send_response(message)
json_message = message.is_a?(String) ? message : JSON.generate(message)
$stdout.puts(json_message)
$stdout.flush
end
end
end
end
end

View file

@ -1,35 +0,0 @@
# frozen_string_literal: true
require_relative "../transport"
require "json"
module MCP
module Transports
class StdioTransport < Transport
def initialize(server)
@server = server
@open = false
$stdin.set_encoding("UTF-8")
$stdout.set_encoding("UTF-8")
super
end
def open
@open = true
while @open && (line = $stdin.gets)
handle_json_request(line.strip)
end
end
def close
@open = false
end
def send_response(message)
json_message = message.is_a?(String) ? message : JSON.generate(message)
$stdout.puts(json_message)
$stdout.flush
end
end
end
end

View file

@ -0,0 +1,127 @@
# frozen_string_literal: true
require "test_helper"
require "mcp/server/transports/stdio"
require "json"
module MCP
class Server
module Transports
class StdioTest < ActiveSupport::TestCase
include InstrumentationTestHelper
setup do
configuration = MCP::Configuration.new
configuration.instrumentation_callback = instrumentation_helper.callback
@server = Server.new(name: "test_server", configuration: configuration)
@transport = StdioTransport.new(@server)
end
test "initializes with server and closed state" do
server = @transport.instance_variable_get(:@server)
assert_equal @server.object_id, server.object_id
refute @transport.instance_variable_get(:@open)
end
test "processes JSON-RPC requests from stdin and sends responses to stdout" do
request = {
jsonrpc: "2.0",
method: "ping",
id: "123",
}
input = StringIO.new(JSON.generate(request) + "\n")
output = StringIO.new
original_stdin = $stdin
original_stdout = $stdout
begin
$stdin = input
$stdout = output
thread = Thread.new { @transport.open }
sleep(0.1)
@transport.close
thread.join
response = JSON.parse(output.string, symbolize_names: true)
assert_equal("2.0", response[:jsonrpc])
assert_equal("123", response[:id])
assert_empty(response[:result])
refute(@transport.instance_variable_get(:@open))
ensure
$stdin = original_stdin
$stdout = original_stdout
end
end
test "sends string responses to stdout" do
output = StringIO.new
original_stdout = $stdout
begin
$stdout = output
@transport.send_response("test response")
assert_equal("test response\n", output.string)
ensure
$stdout = original_stdout
end
end
test "sends JSON responses to stdout" do
output = StringIO.new
original_stdout = $stdout
begin
$stdout = output
response = { key: "value" }
@transport.send_response(response)
assert_equal(JSON.generate(response) + "\n", output.string)
ensure
$stdout = original_stdout
end
end
test "handles valid JSON-RPC requests" do
request = {
jsonrpc: "2.0",
method: "ping",
id: "123",
}
output = StringIO.new
original_stdout = $stdout
begin
$stdout = output
@transport.send(:handle_request, JSON.generate(request))
response = JSON.parse(output.string, symbolize_names: true)
assert_equal("2.0", response[:jsonrpc])
assert_nil(response[:id])
assert_nil(response[:result])
ensure
$stdout = original_stdout
end
end
test "handles invalid JSON requests" do
invalid_json = "invalid json"
output = StringIO.new
original_stdout = $stdout
begin
$stdout = output
@transport.send(:handle_request, invalid_json)
response = JSON.parse(output.string, symbolize_names: true)
assert_equal("2.0", response[:jsonrpc])
assert_nil(response[:id])
assert_equal(-32600, response[:error][:code])
assert_equal("Invalid Request", response[:error][:message])
assert_equal("Request must be an array or a hash", response[:error][:data])
ensure
$stdout = original_stdout
end
end
end
end
end
end