From aa8bbad491ced3fdc406f17a64549c5afff3fd67 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sun, 21 Dec 2025 23:21:19 +0900 Subject: [PATCH] Support Ruby 2.7 - Ruby 3.1 ## Summary Unlike applications, libraries provide value by remaining usable for a broad range of users, even when that means supporting older Ruby versions beyond Ruby's own EOL timeline. I maintain RuboCop, a Ruby linter. Because linters need to work across the ecosystem, they often need to keep compatibility with older Ruby versions to some extent. The trade-off is that the MCP Ruby SDK must remain compatible with Ruby 2.7. However, given the ecosystem impact, supporting Ruby 2.7 is still worthwhile. For example, bundled gems such as `csv` (Ruby >= 2.5), `bigdecimal` (Ruby >= 2.5), `json` (Ruby >= 2.7), and `language_server-protocol` (Ruby >= 2.5) tend to keep compatibility with older Ruby versions. Unlike dropping support, which imposes restrictions on users, this change broadens compatibility and does not introduce breaking changes. ## Development Note `rubocop-shopify` (2.18) supports Ruby 3.1+, and this PR does not change that. Since RuboCop can configure the runtime Ruby version and the target Ruby version for analysis independently, CI continues to run RuboCop on Ruby 4.0, while analyzing code with `TargetRubyVersion: 2.7`. When setting `TargetRubyVersion: 2.7`, several RuboCop offenses were resolved. Additionally, Sorbet-related libraries support Ruby 3.0+. Since these are not dependencies of the MCP Ruby SDK itself, they are versioned and tested in the development Gemfile. --- .github/workflows/ci.yml | 7 ++- .rubocop.yml | 3 ++ AGENTS.md | 2 +- Gemfile | 9 ++-- lib/json_rpc_handler.rb | 26 +++++------ lib/mcp/client/http.rb | 12 ++--- lib/mcp/configuration.rb | 8 ++-- lib/mcp/content.rb | 4 +- lib/mcp/instrumentation.rb | 2 +- lib/mcp/prompt.rb | 2 +- lib/mcp/prompt/message.rb | 2 +- lib/mcp/prompt/result.rb | 2 +- lib/mcp/resource/contents.rb | 2 +- lib/mcp/resource/embedded.rb | 2 +- lib/mcp/server.rb | 20 ++++---- .../transports/streamable_http_transport.rb | 2 +- lib/mcp/tool/annotations.rb | 2 +- lib/mcp/tool/response.rb | 2 +- mcp.gemspec | 3 +- test/json_rpc_handler_test.rb | 8 ++-- test/mcp/client_test.rb | 46 ++++++------------- test/mcp/prompt_test.rb | 2 +- .../streamable_http_transport_test.rb | 8 ++-- test/mcp/server_test.rb | 26 ++++++++--- test/mcp/tool_test.rb | 2 + test/test_helper.rb | 2 +- 26 files changed, 106 insertions(+), 100 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 317f5a8..f4496be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ jobs: strategy: matrix: entry: + - { ruby: '2.7', allowed-failure: false } + - { ruby: '3.0', allowed-failure: false } + - { ruby: '3.1', allowed-failure: false } - { ruby: '3.2', allowed-failure: false } - { ruby: '3.3', allowed-failure: false } - { ruby: '3.4', allowed-failure: false } @@ -29,7 +32,7 @@ jobs: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: - ruby-version: 3.2 # Specify the oldest supported Ruby version. + ruby-version: 4.0 # Specify the latest supported Ruby version. bundler-cache: true - run: bundle exec rake rubocop @@ -40,6 +43,6 @@ jobs: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: - ruby-version: 3.2 # Specify the oldest supported Ruby version. + ruby-version: 4.0 # Specify the latest supported Ruby version. bundler-cache: true - run: bundle exec yard --no-output diff --git a/.rubocop.yml b/.rubocop.yml index fb1a5cb..2120de4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,6 +5,9 @@ plugins: - rubocop-minitest - rubocop-rake +AllCops: + TargetRubyVersion: 2.7 + Gemspec/DevelopmentDependencies: Enabled: true diff --git a/AGENTS.md b/AGENTS.md index 48dc6ea..6b269d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing ## Dev environment setup -- Ruby 3.2.0+ required +- Ruby 3.2.0+ required to run the full test suite, including all Sorbet-related features - Run `bundle install` to install dependencies - Dependencies: `json-schema` >= 4.1 - Schema validation diff --git a/Gemfile b/Gemfile index 547ccd0..fa2d2b8 100644 --- a/Gemfile +++ b/Gemfile @@ -8,17 +8,18 @@ gemspec # Specify development dependencies below gem "rubocop-minitest", require: false gem "rubocop-rake", require: false -gem "rubocop-shopify", require: false +gem "rubocop-shopify", ">= 2.18", require: false if RUBY_VERSION >= "3.1" gem "puma", ">= 5.0.0" gem "rackup", ">= 2.1.0" gem "activesupport" -gem "debug" +# Fix io-console install error when Ruby 3.0. +gem "debug" if RUBY_VERSION >= "3.1" gem "rake", "~> 13.0" -gem "sorbet-static-and-runtime" +gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0" gem "yard", "~> 0.9" -gem "yard-sorbet", "~> 0.9" +gem "yard-sorbet", "~> 0.9" if RUBY_VERSION >= "3.1" group :test do gem "faraday", ">= 2.0" diff --git a/lib/json_rpc_handler.rb b/lib/json_rpc_handler.rb index 309044e..536406d 100644 --- a/lib/json_rpc_handler.rb +++ b/lib/json_rpc_handler.rb @@ -22,14 +22,14 @@ module JsonRpcHandler def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder) if request.is_a?(Array) - return error_response(id: :unknown_id, id_validation_pattern:, error: { + return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::INVALID_REQUEST, message: "Invalid Request", data: "Request is an empty array", }) if request.empty? # Handle batch requests - responses = request.map { |req| process_request(req, id_validation_pattern:, &method_finder) }.compact + responses = request.map { |req| process_request(req, id_validation_pattern: id_validation_pattern, &method_finder) }.compact # A single item is hoisted out of the array return responses.first if responses.one? @@ -38,9 +38,9 @@ module JsonRpcHandler responses if responses.any? elsif request.is_a?(Hash) # Handle single request - process_request(request, id_validation_pattern:, &method_finder) + process_request(request, id_validation_pattern: id_validation_pattern, &method_finder) else - error_response(id: :unknown_id, id_validation_pattern:, error: { + error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::INVALID_REQUEST, message: "Invalid Request", data: "Request must be an array or a hash", @@ -51,9 +51,9 @@ module JsonRpcHandler def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder) begin request = JSON.parse(request_json, symbolize_names: true) - response = handle(request, id_validation_pattern:, &method_finder) + response = handle(request, id_validation_pattern: id_validation_pattern, &method_finder) rescue JSON::ParserError - response = error_response(id: :unknown_id, id_validation_pattern:, error: { + response = error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::PARSE_ERROR, message: "Parse error", data: "Invalid JSON", @@ -74,7 +74,7 @@ module JsonRpcHandler 'Method name must be a string and not start with "rpc."' end - return error_response(id: :unknown_id, id_validation_pattern:, error: { + return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::INVALID_REQUEST, message: "Invalid Request", data: error, @@ -84,7 +84,7 @@ module JsonRpcHandler params = request[:params] unless valid_params?(params) - return error_response(id:, id_validation_pattern:, error: { + return error_response(id: id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::INVALID_PARAMS, message: "Invalid params", data: "Method parameters must be an array or an object or null", @@ -95,7 +95,7 @@ module JsonRpcHandler method = method_finder.call(method_name) if method.nil? - return error_response(id:, id_validation_pattern:, error: { + return error_response(id: id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::METHOD_NOT_FOUND, message: "Method not found", data: method_name, @@ -104,9 +104,9 @@ module JsonRpcHandler result = method.call(params) - success_response(id:, result:) + success_response(id: id, result: result) rescue StandardError => e - error_response(id:, id_validation_pattern:, error: { + error_response(id: id, id_validation_pattern: id_validation_pattern, error: { code: ErrorCode::INTERNAL_ERROR, message: "Internal error", data: e.message, @@ -136,8 +136,8 @@ module JsonRpcHandler def success_response(id:, result:) { jsonrpc: Version::V2_0, - id:, - result:, + id: id, + result: result, } unless id.nil? end diff --git a/lib/mcp/client/http.rb b/lib/mcp/client/http.rb index 7b065a8..084178f 100644 --- a/lib/mcp/client/http.rb +++ b/lib/mcp/client/http.rb @@ -18,42 +18,42 @@ module MCP rescue Faraday::BadRequestError => e raise RequestHandlerError.new( "The #{method} request is invalid", - { method:, params: }, + { method: method, params: params }, error_type: :bad_request, original_error: e, ) rescue Faraday::UnauthorizedError => e raise RequestHandlerError.new( "You are unauthorized to make #{method} requests", - { method:, params: }, + { method: method, params: params }, error_type: :unauthorized, original_error: e, ) rescue Faraday::ForbiddenError => e raise RequestHandlerError.new( "You are forbidden to make #{method} requests", - { method:, params: }, + { method: method, params: params }, error_type: :forbidden, original_error: e, ) rescue Faraday::ResourceNotFound => e raise RequestHandlerError.new( "The #{method} request is not found", - { method:, params: }, + { method: method, params: params }, error_type: :not_found, original_error: e, ) rescue Faraday::UnprocessableEntityError => e raise RequestHandlerError.new( "The #{method} request is unprocessable", - { method:, params: }, + { method: method, params: params }, error_type: :unprocessable_entity, original_error: e, ) rescue Faraday::Error => e # Catch-all raise RequestHandlerError.new( "Internal error handling #{method} request", - { method:, params: }, + { method: method, params: params }, error_type: :internal_error, original_error: e, ) diff --git a/lib/mcp/configuration.rb b/lib/mcp/configuration.rb index a0f038e..03105bd 100644 --- a/lib/mcp/configuration.rb +++ b/lib/mcp/configuration.rb @@ -85,10 +85,10 @@ module MCP validate_tool_call_arguments = other.validate_tool_call_arguments Configuration.new( - exception_reporter:, - instrumentation_callback:, - protocol_version:, - validate_tool_call_arguments:, + exception_reporter: exception_reporter, + instrumentation_callback: instrumentation_callback, + protocol_version: protocol_version, + validate_tool_call_arguments: validate_tool_call_arguments, ) end diff --git a/lib/mcp/content.rb b/lib/mcp/content.rb index 7f4f921..7ab984d 100644 --- a/lib/mcp/content.rb +++ b/lib/mcp/content.rb @@ -11,7 +11,7 @@ module MCP end def to_h - { text:, annotations:, type: "text" }.compact + { text: text, annotations: annotations, type: "text" }.compact end end @@ -25,7 +25,7 @@ module MCP end def to_h - { data:, mime_type:, annotations:, type: "image" }.compact + { data: data, mime_type: mime_type, annotations: annotations, type: "image" }.compact end end end diff --git a/lib/mcp/instrumentation.rb b/lib/mcp/instrumentation.rb index 5632fdd..40975a9 100644 --- a/lib/mcp/instrumentation.rb +++ b/lib/mcp/instrumentation.rb @@ -6,7 +6,7 @@ module MCP start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin @instrumentation_data = {} - add_instrumentation_data(method:) + add_instrumentation_data(method: method) result = yield block diff --git a/lib/mcp/prompt.rb b/lib/mcp/prompt.rb index 317df7e..9445cf7 100644 --- a/lib/mcp/prompt.rb +++ b/lib/mcp/prompt.rb @@ -96,7 +96,7 @@ module MCP icons icons arguments arguments define_singleton_method(:template) do |args, server_context: nil| - instance_exec(args, server_context:, &block) + instance_exec(args, server_context: server_context, &block) end meta meta end diff --git a/lib/mcp/prompt/message.rb b/lib/mcp/prompt/message.rb index aa20301..906ebd1 100644 --- a/lib/mcp/prompt/message.rb +++ b/lib/mcp/prompt/message.rb @@ -11,7 +11,7 @@ module MCP end def to_h - { role:, content: content.to_h }.compact + { role: role, content: content.to_h }.compact end end end diff --git a/lib/mcp/prompt/result.rb b/lib/mcp/prompt/result.rb index 53a5e21..b55b242 100644 --- a/lib/mcp/prompt/result.rb +++ b/lib/mcp/prompt/result.rb @@ -11,7 +11,7 @@ module MCP end def to_h - { description:, messages: messages.map(&:to_h) }.compact + { description: description, messages: messages.map(&:to_h) }.compact end end end diff --git a/lib/mcp/resource/contents.rb b/lib/mcp/resource/contents.rb index d87ded8..4c3bf11 100644 --- a/lib/mcp/resource/contents.rb +++ b/lib/mcp/resource/contents.rb @@ -11,7 +11,7 @@ module MCP end def to_h - { uri:, mime_type: }.compact + { uri: uri, mime_type: mime_type }.compact end end diff --git a/lib/mcp/resource/embedded.rb b/lib/mcp/resource/embedded.rb index 343f86e..9df04c6 100644 --- a/lib/mcp/resource/embedded.rb +++ b/lib/mcp/resource/embedded.rb @@ -10,7 +10,7 @@ module MCP end def to_h - { resource: resource.to_h, annotations: }.compact + { resource: resource.to_h, annotations: annotations }.compact end end end diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 688a52d..74c014d 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -114,7 +114,7 @@ module MCP end def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block) - tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &block) + tool = Tool.define(name: name, title: title, description: description, input_schema: input_schema, annotations: annotations, meta: meta, &block) tool_name = tool.name_value @tool_names << tool_name @@ -124,7 +124,7 @@ module MCP end def define_prompt(name: nil, title: nil, description: nil, arguments: [], &block) - prompt = Prompt.define(name:, title:, description:, arguments:, &block) + prompt = Prompt.define(name: name, title: title, description: description, arguments: arguments, &block) @prompts[prompt.name_value] = prompt validate! @@ -289,11 +289,11 @@ module MCP def server_info @server_info ||= { - description:, - icons:, - name:, - title:, - version:, + description: description, + icons: icons, + name: name, + title: title, + version: version, websiteUrl: website_url, }.compact end @@ -316,13 +316,13 @@ module MCP tool = tools[tool_name] unless tool - add_instrumentation_data(tool_name:, error: :tool_not_found) + add_instrumentation_data(tool_name: tool_name, error: :tool_not_found) return error_tool_response("Tool not found: #{tool_name}") end arguments = request[:arguments] || {} - add_instrumentation_data(tool_name:) + add_instrumentation_data(tool_name: tool_name) if tool.input_schema&.missing_required_arguments?(arguments) add_instrumentation_data(error: :missing_required_arguments) @@ -360,7 +360,7 @@ module MCP raise RequestHandlerError.new("Prompt not found #{prompt_name}", request, error_type: :prompt_not_found) end - add_instrumentation_data(prompt_name:) + add_instrumentation_data(prompt_name: prompt_name) prompt_args = request[:arguments] prompt.validate_arguments!(prompt_args) diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index 91f5ae3..4c88085 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -42,7 +42,7 @@ module MCP notification = { jsonrpc: "2.0", - method:, + method: method, } notification[:params] = params if params diff --git a/lib/mcp/tool/annotations.rb b/lib/mcp/tool/annotations.rb index 328a24c..381cda7 100644 --- a/lib/mcp/tool/annotations.rb +++ b/lib/mcp/tool/annotations.rb @@ -19,7 +19,7 @@ module MCP idempotentHint: idempotent_hint, openWorldHint: open_world_hint, readOnlyHint: read_only_hint, - title:, + title: title, }.compact end end diff --git a/lib/mcp/tool/response.rb b/lib/mcp/tool/response.rb index af3ec8f..0d9dccb 100644 --- a/lib/mcp/tool/response.rb +++ b/lib/mcp/tool/response.rb @@ -23,7 +23,7 @@ module MCP end def to_h - { content:, isError: error?, structuredContent: @structured_content }.compact + { content: content, isError: error?, structuredContent: @structured_content }.compact end end end diff --git a/mcp.gemspec b/mcp.gemspec index eb87aeb..3467765 100644 --- a/mcp.gemspec +++ b/mcp.gemspec @@ -13,7 +13,8 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/modelcontextprotocol/ruby-sdk" spec.license = "MIT" - spec.required_ruby_version = ">= 3.2.0" + # Since this library is used by a broad range of users, it does not align its support policy with Ruby's EOL. + spec.required_ruby_version = ">= 2.7.0" spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["changelog_uri"] = "https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v#{spec.version}" diff --git a/test/json_rpc_handler_test.rb b/test/json_rpc_handler_test.rb index 38004e6..751d140 100644 --- a/test/json_rpc_handler_test.rb +++ b/test/json_rpc_handler_test.rb @@ -94,7 +94,7 @@ describe JsonRpcHandler do register("add") { |params| params[:a] + params[:b] } id = "request-123_abc" - handle jsonrpc: "2.0", id:, method: "add", params: { a: 1, b: 2 } + handle jsonrpc: "2.0", id: id, method: "add", params: { a: 1, b: 2 } assert_rpc_success expected_result: 3 assert_equal id, @response[:id] @@ -104,7 +104,7 @@ describe JsonRpcHandler do register("add") { |params| params[:a] + params[:b] } id = 42 - handle jsonrpc: "2.0", id:, method: "add", params: { a: 1, b: 2 } + handle jsonrpc: "2.0", id: id, method: "add", params: { a: 1, b: 2 } assert_rpc_success expected_result: 3 assert_equal id, @response[:id] @@ -124,7 +124,7 @@ describe JsonRpcHandler do register("add") { |params| params[:a] + params[:b] } id = "request-123_ABC" - handle jsonrpc: "2.0", id:, method: "add", params: { a: 1, b: 2 } + handle jsonrpc: "2.0", id: id, method: "add", params: { a: 1, b: 2 } assert_rpc_success expected_result: 3 assert_equal id, @response[:id] @@ -134,7 +134,7 @@ describe JsonRpcHandler do register("add") { |params| params[:a] + params[:b] } id = "550e8400-e29b-41d4-a716-446655440000" - handle jsonrpc: "2.0", id:, method: "add", params: { a: 1, b: 2 } + handle jsonrpc: "2.0", id: id, method: "add", params: { a: 1, b: 2 } assert_rpc_success expected_result: 3 assert_equal id, @response[:id] diff --git a/test/mcp/client_test.rb b/test/mcp/client_test.rb index b56f011..168ed94 100644 --- a/test/mcp/client_test.rb +++ b/test/mcp/client_test.rb @@ -18,7 +18,7 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { request: { method: "tools/list", jsonrpc: "2.0" } } + args.dig(:request, :method) == "tools/list" && args.dig(:request, :jsonrpc) == "2.0" end.returns(mock_response).once client = Client.new(transport: transport) @@ -35,7 +35,7 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { request: { method: "tools/list", jsonrpc: "2.0" } } + args.dig(:request, :method) == "tools/list" && args.dig(:request, :jsonrpc) == "2.0" end.returns(mock_response).once client = Client.new(transport: transport) @@ -54,16 +54,10 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { - request: { - method: "tools/call", - jsonrpc: "2.0", - params: { - name: "tool1", - arguments: arguments, - }, - }, - } + args.dig(:request, :method) == "tools/call" && + args.dig(:request, :jsonrpc) == "2.0" && + args.dig(:request, :params, :name) == "tool1" && + args.dig(:request, :params, :arguments) == arguments end.returns(mock_response).once client = Client.new(transport: transport) @@ -86,7 +80,7 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { request: { method: "resources/list", jsonrpc: "2.0" } } + args.dig(:request, :method) == "resources/list" && args.dig(:request, :jsonrpc) == "2.0" end.returns(mock_response).once client = Client.new(transport: transport) @@ -124,15 +118,9 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { - request: { - method: "resources/read", - jsonrpc: "2.0", - params: { - uri: uri, - }, - }, - } + args.dig(:request, :method) == "resources/read" && + args.dig(:request, :jsonrpc) == "2.0" && + args.dig(:request, :params, :uri) == uri end.returns(mock_response).once client = Client.new(transport: transport) @@ -190,7 +178,7 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { request: { method: "prompts/list", jsonrpc: "2.0" } } + args.dig(:request, :method) == "prompts/list" && args.dig(:request, :jsonrpc) == "2.0" end.returns(mock_response).once client = Client.new(transport: transport) @@ -242,15 +230,9 @@ module MCP # Only checking for the essential parts of the request transport.expects(:send_request).with do |args| - args in { - request: { - method: "prompts/get", - jsonrpc: "2.0", - params: { - name: name, - }, - }, - } + args.dig(:request, :method) == "prompts/get" && + args.dig(:request, :jsonrpc) == "2.0" && + args.dig(:request, :params, :name) == name end.returns(mock_response).once client = Client.new(transport: transport) diff --git a/test/mcp/prompt_test.rb b/test/mcp/prompt_test.rb index 64630b8..16bb584 100644 --- a/test/mcp/prompt_test.rb +++ b/test/mcp/prompt_test.rb @@ -132,7 +132,7 @@ module MCP description: "Hello, world!", messages: [ Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")), - Prompt::Message.new(role: "assistant", content:), + Prompt::Message.new(role: "assistant", content: content), ], ) end diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index acae7b6..743268e 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -388,7 +388,7 @@ module MCP @server.define_singleton_method(:handle_json) do |request| result = original_handle_json.call(request) # Send notification while still in request context - broadcast to all sessions - transport.send_notification("test_notification", { session: "current" }) + transport.send_notification("test_notification", { session: "current" }, **{}) result end @@ -500,7 +500,7 @@ module MCP sleep(0.1) # Broadcast notification to all sessions - sent_count = @transport.send_notification("broadcast", { message: "Hello everyone" }) + sent_count = @transport.send_notification("broadcast", { message: "Hello everyone" }, **{}) assert_equal 2, sent_count @@ -517,7 +517,7 @@ module MCP end test "send_notification returns false for non-existent session" do - result = @transport.send_notification({ message: "test" }, session_id: "non_existent") + result = @transport.send_notification("test", { message: "test" }, session_id: "non_existent") refute result end @@ -549,7 +549,7 @@ module MCP io.close # Try to send notification - result = @transport.send_notification({ message: "test" }, session_id: session_id) + result = @transport.send_notification("test", { message: "test" }, session_id: session_id) # Should return false and clean up the session refute result diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 588faab..b9ef6e5 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -80,7 +80,7 @@ module MCP prompts: [@prompt], resources: [@resource], resource_templates: [@resource_template], - configuration:, + configuration: configuration, ) end @@ -244,7 +244,13 @@ module MCP tool_args = { arg: "value" } tool_response = Tool::Response.new([{ result: "success" }]) - @tool.expects(:call).with(arg: "value", server_context: nil).returns(tool_response) + if RUBY_VERSION >= "3.1" + # Ruby 3.1+: Mocha stub preserves `method.parameters` info. + @tool.expects(:call).with(arg: "value", server_context: nil).returns(tool_response) + else + # Ruby 3.0: Mocha stub changes `method.parameters`, so `accepts_server_context?` returns false. + @tool.expects(:call).with(arg: "value").returns(tool_response) + end request = { jsonrpc: "2.0", @@ -258,7 +264,7 @@ module MCP response = @server.handle(request) assert_equal tool_response.to_h, response[:result] - assert_instrumentation_data({ method: "tools/call", tool_name: }) + assert_instrumentation_data({ method: "tools/call", tool_name: tool_name }) end test "#handle tools/call returns error response with isError true if required tool arguments are missing" do @@ -296,7 +302,13 @@ module MCP tool_args = { arg: "value" } tool_response = Tool::Response.new([{ result: "success" }]) - @tool.expects(:call).with(arg: "value", server_context: nil).returns(tool_response) + if RUBY_VERSION >= "3.1" + # Ruby 3.1+: Mocha stub preserves `method.parameters` info. + @tool.expects(:call).with(arg: "value", server_context: nil).returns(tool_response) + else + # Ruby 3.0: Mocha stub changes `method.parameters`, so `accepts_server_context?` returns false. + @tool.expects(:call).with(arg: "value").returns(tool_response) + end request = JSON.generate({ jsonrpc: "2.0", @@ -308,10 +320,12 @@ module MCP raw_response = @server.handle_json(request) response = JSON.parse(raw_response, symbolize_names: true) if raw_response assert_equal tool_response.to_h, response[:result] if response - assert_instrumentation_data({ method: "tools/call", tool_name: }) + assert_instrumentation_data({ method: "tools/call", tool_name: tool_name }) end test "#handle_json tools/call executes tool and returns result, when the tool is typed with Sorbet" do + skip "Sorbet is not available" unless defined?(T::Sig) + class TypedTestTool < Tool tool_name "test_tool" description "a test tool for testing" @@ -836,7 +850,7 @@ module MCP configuration.instrumentation_callback = local_callback configuration.exception_reporter = local_exception_reporter - server = Server.new(name: "test_server", configuration:) + server = Server.new(name: "test_server", configuration: configuration) assert_equal local_callback, server.configuration.instrumentation_callback assert_equal local_exception_reporter, server.configuration.exception_reporter diff --git a/test/mcp/tool_test.rb b/test/mcp/tool_test.rb index ee71827..3d8369c 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -243,6 +243,8 @@ module MCP end test "#call with Sorbet typed tools invokes the tool block and returns the response" do + skip "sorbet-static-and-runtime requires Ruby 3.0+." if RUBY_VERSION < "3.0" + class TypedTestTool < Tool tool_name "test_tool" description "a test tool for testing" diff --git a/test/test_helper.rb b/test/test_helper.rb index 9a712f5..19e9974 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,6 +9,6 @@ require "mocha/minitest" require "active_support" require "active_support/test_case" -require "sorbet-runtime" +require "sorbet-runtime" if RUBY_VERSION >= "3.0" require_relative "instrumentation_test_helper"