diff --git a/README.md b/README.md index 6c4c5279..2acf2fd5 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/examples/stdio_server.rb b/examples/stdio_server.rb index 177b3721..cddf912e 100755 --- a/examples/stdio_server.rb +++ b/examples/stdio_server.rb @@ -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 @@ 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 diff --git a/lib/mcp.rb b/lib/mcp.rb index 7eb8870a..cb2c2f92 100644 --- a/lib/mcp.rb +++ b/lib/mcp.rb @@ -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 diff --git a/lib/mcp/server/transports/stdio.rb b/lib/mcp/server/transports/stdio.rb new file mode 100644 index 00000000..a90ea67d --- /dev/null +++ b/lib/mcp/server/transports/stdio.rb @@ -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 diff --git a/lib/mcp/transports/stdio.rb b/lib/mcp/transports/stdio.rb deleted file mode 100644 index b76b9619..00000000 --- a/lib/mcp/transports/stdio.rb +++ /dev/null @@ -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 diff --git a/test/model_context_protocol/configuration_test.rb b/test/mcp/configuration_test.rb similarity index 100% rename from test/model_context_protocol/configuration_test.rb rename to test/mcp/configuration_test.rb diff --git a/test/model_context_protocol/instrumentation_test.rb b/test/mcp/instrumentation_test.rb similarity index 100% rename from test/model_context_protocol/instrumentation_test.rb rename to test/mcp/instrumentation_test.rb diff --git a/test/model_context_protocol/methods_test.rb b/test/mcp/methods_test.rb similarity index 100% rename from test/model_context_protocol/methods_test.rb rename to test/mcp/methods_test.rb diff --git a/test/model_context_protocol/prompt_test.rb b/test/mcp/prompt_test.rb similarity index 100% rename from test/model_context_protocol/prompt_test.rb rename to test/mcp/prompt_test.rb diff --git a/test/mcp/server/transports/stdio_test.rb b/test/mcp/server/transports/stdio_test.rb new file mode 100644 index 00000000..57a16909 --- /dev/null +++ b/test/mcp/server/transports/stdio_test.rb @@ -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 diff --git a/test/model_context_protocol/server_test.rb b/test/mcp/server_test.rb similarity index 100% rename from test/model_context_protocol/server_test.rb rename to test/mcp/server_test.rb diff --git a/test/model_context_protocol/string_utils_test.rb b/test/mcp/string_utils_test.rb similarity index 100% rename from test/model_context_protocol/string_utils_test.rb rename to test/mcp/string_utils_test.rb diff --git a/test/model_context_protocol/tool/input_schema_test.rb b/test/mcp/tool/input_schema_test.rb similarity index 100% rename from test/model_context_protocol/tool/input_schema_test.rb rename to test/mcp/tool/input_schema_test.rb diff --git a/test/model_context_protocol/tool_test.rb b/test/mcp/tool_test.rb similarity index 100% rename from test/model_context_protocol/tool_test.rb rename to test/mcp/tool_test.rb diff --git a/test/model_context_protocol/transports/stdio_transport_test.rb b/test/model_context_protocol/transports/stdio_transport_test.rb deleted file mode 100644 index 2fe5df2a..00000000 --- a/test/model_context_protocol/transports/stdio_transport_test.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "mcp/transports/stdio" -require "json" - -module MCP - module Transports - class StdioTransportTest < 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