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.
This commit is contained in:
parent
4968d9cffc
commit
aa8bbad491
7 changed files with 106 additions and 100 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue