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 317f5a80..f4496bef 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 fb1a5cb2..2120de45 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 48dc6ea9..6b269d72 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 547ccd0d..fa2d2b89 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 309044e9..536406de 100644 --- a/lib/json_rpc_handler.rb +++ b/lib/json_rpc_handler.rb @@ -22,14 +22,14 @@ class ErrorCode 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 @@ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &metho 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 @@ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &metho 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 @@ def process_request(request, id_validation_pattern:, &method_finder) '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 @@ def process_request(request, id_validation_pattern:, &method_finder) 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 @@ def process_request(request, id_validation_pattern:, &method_finder) 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 @@ def process_request(request, id_validation_pattern:, &method_finder) 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 @@ def valid_params?(params) 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 7b065a89..084178f3 100644 --- a/lib/mcp/client/http.rb +++ b/lib/mcp/client/http.rb @@ -18,42 +18,42 @@ def send_request(request:) 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 a0f038e7..03105bdb 100644 --- a/lib/mcp/configuration.rb +++ b/lib/mcp/configuration.rb @@ -85,10 +85,10 @@ def merge(other) 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 7f4f9214..7ab984d6 100644 --- a/lib/mcp/content.rb +++ b/lib/mcp/content.rb @@ -11,7 +11,7 @@ def initialize(text, annotations: nil) end def to_h - { text:, annotations:, type: "text" }.compact + { text: text, annotations: annotations, type: "text" }.compact end end @@ -25,7 +25,7 @@ def initialize(data, mime_type, annotations: nil) 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 5632fdda..40975a91 100644 --- a/lib/mcp/instrumentation.rb +++ b/lib/mcp/instrumentation.rb @@ -6,7 +6,7 @@ def instrument_call(method, &block) 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 317df7e0..9445cf74 100644 --- a/lib/mcp/prompt.rb +++ b/lib/mcp/prompt.rb @@ -96,7 +96,7 @@ def define(name: nil, title: nil, description: nil, icons: [], arguments: [], me 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 aa203019..906ebd19 100644 --- a/lib/mcp/prompt/message.rb +++ b/lib/mcp/prompt/message.rb @@ -11,7 +11,7 @@ def initialize(role:, content:) 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 53a5e21c..b55b2424 100644 --- a/lib/mcp/prompt/result.rb +++ b/lib/mcp/prompt/result.rb @@ -11,7 +11,7 @@ def initialize(description: nil, messages: []) 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 d87ded84..4c3bf116 100644 --- a/lib/mcp/resource/contents.rb +++ b/lib/mcp/resource/contents.rb @@ -11,7 +11,7 @@ def initialize(uri:, mime_type: nil) 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 343f86ea..9df04c65 100644 --- a/lib/mcp/resource/embedded.rb +++ b/lib/mcp/resource/embedded.rb @@ -10,7 +10,7 @@ def initialize(resource:, annotations: nil) 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 688a52d3..74c014dd 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -114,7 +114,7 @@ def handle_json(request) 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 @@ def define_tool(name: nil, title: nil, description: nil, input_schema: nil, anno 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 @@ def default_capabilities 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 @@ def call_tool(request) 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 @@ def get_prompt(request) 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 91f5ae3c..4c88085d 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -42,7 +42,7 @@ def send_notification(method, params = nil, session_id: nil) 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 328a24cd..381cda7c 100644 --- a/lib/mcp/tool/annotations.rb +++ b/lib/mcp/tool/annotations.rb @@ -19,7 +19,7 @@ def to_h 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 af3ec8ff..0d9dccbb 100644 --- a/lib/mcp/tool/response.rb +++ b/lib/mcp/tool/response.rb @@ -23,7 +23,7 @@ def error? 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 eb87aeb5..34677655 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 38004e69..751d1404 100644 --- a/test/json_rpc_handler_test.rb +++ b/test/json_rpc_handler_test.rb @@ -94,7 +94,7 @@ 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 @@ 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 @@ 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 @@ 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 b56f011d..168ed944 100644 --- a/test/mcp/client_test.rb +++ b/test/mcp/client_test.rb @@ -18,7 +18,7 @@ def test_tools_sends_request_to_transport_and_returns_tools_array # 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 @@ def test_tools_returns_empty_array_when_no_tools # 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 @@ def test_call_tool_sends_request_to_transport_and_returns_content # 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 @@ def test_resources_sends_request_to_transport_and_returns_resources_array # 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 @@ def test_read_resource_sends_request_to_transport_and_returns_contents # 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 @@ def test_prompts_sends_request_to_transport_and_returns_prompts_array # 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 @@ def test_get_prompt_sends_request_to_transport_and_returns_contents # 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 64630b8f..16bb5847 100644 --- a/test/mcp/prompt_test.rb +++ b/test/mcp/prompt_test.rb @@ -132,7 +132,7 @@ def template(args, server_context:) 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 acae7b6f..743268e0 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 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase @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 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase 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 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase 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 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase 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 588faab9..b9ef6e51 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -80,7 +80,7 @@ class ServerTest < ActiveSupport::TestCase prompts: [@prompt], resources: [@resource], resource_templates: [@resource_template], - configuration:, + configuration: configuration, ) end @@ -244,7 +244,13 @@ class ServerTest < ActiveSupport::TestCase 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 @@ class ServerTest < ActiveSupport::TestCase 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 @@ class ServerTest < ActiveSupport::TestCase 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 @@ class ServerTest < ActiveSupport::TestCase 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 @@ class Example < Tool 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 ee71827e..3d8369c8 100644 --- a/test/mcp/tool_test.rb +++ b/test/mcp/tool_test.rb @@ -243,6 +243,8 @@ class UpdatableMetaTool < Tool 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 9a712f5b..19e9974b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,6 +9,6 @@ require "active_support" require "active_support/test_case" -require "sorbet-runtime" +require "sorbet-runtime" if RUBY_VERSION >= "3.0" require_relative "instrumentation_test_helper"