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"