Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down
4 changes: 2 additions & 2 deletions examples/stdio_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,5 +91,5 @@ def template(args, server_context:)
end

# Create and start the transport
transport = MCP::Transports::StdioTransport.new(server)
transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open
25 changes: 16 additions & 9 deletions lib/mcp.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
# 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/version"
require_relative "mcp/configuration"
require_relative "mcp/instrumentation"
require_relative "mcp/methods"
require_relative "mcp/transport"
require_relative "mcp/content"
require_relative "mcp/string_utils"

require_relative "mcp/resource"
require_relative "mcp/resource/contents"
require_relative "mcp/resource/embedded"
require_relative "mcp/resource_template"

require_relative "mcp/tool"
require_relative "mcp/tool/input_schema"
require_relative "mcp/tool/response"
require_relative "mcp/tool/annotations"

require_relative "mcp/prompt"
require_relative "mcp/prompt/argument"
require_relative "mcp/prompt/message"
require_relative "mcp/prompt/result"
require_relative "mcp/version"
require_relative "mcp/configuration"
require_relative "mcp/methods"

require_relative "mcp/server"
require_relative "mcp/server/transports/stdio"

module MCP
class << self
Expand Down
37 changes: 37 additions & 0 deletions lib/mcp/server/transports/stdio.rb
Original file line number Diff line number Diff line change
@@ -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
35 changes: 0 additions & 35 deletions lib/mcp/transports/stdio.rb

This file was deleted.

File renamed without changes.
File renamed without changes.
127 changes: 127 additions & 0 deletions test/mcp/server/transports/stdio_test.rb
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
Loading