Skip to content

Commit aa8bbad

Browse files
committed
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.
1 parent 4968d9c commit aa8bbad

26 files changed

+106
-100
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ jobs:
77
strategy:
88
matrix:
99
entry:
10+
- { ruby: '2.7', allowed-failure: false }
11+
- { ruby: '3.0', allowed-failure: false }
12+
- { ruby: '3.1', allowed-failure: false }
1013
- { ruby: '3.2', allowed-failure: false }
1114
- { ruby: '3.3', allowed-failure: false }
1215
- { ruby: '3.4', allowed-failure: false }
@@ -29,7 +32,7 @@ jobs:
2932
- uses: actions/checkout@v6
3033
- uses: ruby/setup-ruby@v1
3134
with:
32-
ruby-version: 3.2 # Specify the oldest supported Ruby version.
35+
ruby-version: 4.0 # Specify the latest supported Ruby version.
3336
bundler-cache: true
3437
- run: bundle exec rake rubocop
3538

@@ -40,6 +43,6 @@ jobs:
4043
- uses: actions/checkout@v6
4144
- uses: ruby/setup-ruby@v1
4245
with:
43-
ruby-version: 3.2 # Specify the oldest supported Ruby version.
46+
ruby-version: 4.0 # Specify the latest supported Ruby version.
4447
bundler-cache: true
4548
- run: bundle exec yard --no-output

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ plugins:
55
- rubocop-minitest
66
- rubocop-rake
77

8+
AllCops:
9+
TargetRubyVersion: 2.7
10+
811
Gemspec/DevelopmentDependencies:
912
Enabled: true
1013

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
66

77
## Dev environment setup
88

9-
- Ruby 3.2.0+ required
9+
- Ruby 3.2.0+ required to run the full test suite, including all Sorbet-related features
1010
- Run `bundle install` to install dependencies
1111
- Dependencies: `json-schema` >= 4.1 - Schema validation
1212

Gemfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ gemspec
88
# Specify development dependencies below
99
gem "rubocop-minitest", require: false
1010
gem "rubocop-rake", require: false
11-
gem "rubocop-shopify", require: false
11+
gem "rubocop-shopify", ">= 2.18", require: false if RUBY_VERSION >= "3.1"
1212

1313
gem "puma", ">= 5.0.0"
1414
gem "rackup", ">= 2.1.0"
1515

1616
gem "activesupport"
17-
gem "debug"
17+
# Fix io-console install error when Ruby 3.0.
18+
gem "debug" if RUBY_VERSION >= "3.1"
1819
gem "rake", "~> 13.0"
19-
gem "sorbet-static-and-runtime"
20+
gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0"
2021
gem "yard", "~> 0.9"
21-
gem "yard-sorbet", "~> 0.9"
22+
gem "yard-sorbet", "~> 0.9" if RUBY_VERSION >= "3.1"
2223

2324
group :test do
2425
gem "faraday", ">= 2.0"

lib/json_rpc_handler.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ class ErrorCode
2222

2323
def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
2424
if request.is_a?(Array)
25-
return error_response(id: :unknown_id, id_validation_pattern:, error: {
25+
return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
2626
code: ErrorCode::INVALID_REQUEST,
2727
message: "Invalid Request",
2828
data: "Request is an empty array",
2929
}) if request.empty?
3030

3131
# Handle batch requests
32-
responses = request.map { |req| process_request(req, id_validation_pattern:, &method_finder) }.compact
32+
responses = request.map { |req| process_request(req, id_validation_pattern: id_validation_pattern, &method_finder) }.compact
3333

3434
# A single item is hoisted out of the array
3535
return responses.first if responses.one?
@@ -38,9 +38,9 @@ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &metho
3838
responses if responses.any?
3939
elsif request.is_a?(Hash)
4040
# Handle single request
41-
process_request(request, id_validation_pattern:, &method_finder)
41+
process_request(request, id_validation_pattern: id_validation_pattern, &method_finder)
4242
else
43-
error_response(id: :unknown_id, id_validation_pattern:, error: {
43+
error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
4444
code: ErrorCode::INVALID_REQUEST,
4545
message: "Invalid Request",
4646
data: "Request must be an array or a hash",
@@ -51,9 +51,9 @@ def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &metho
5151
def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
5252
begin
5353
request = JSON.parse(request_json, symbolize_names: true)
54-
response = handle(request, id_validation_pattern:, &method_finder)
54+
response = handle(request, id_validation_pattern: id_validation_pattern, &method_finder)
5555
rescue JSON::ParserError
56-
response = error_response(id: :unknown_id, id_validation_pattern:, error: {
56+
response = error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
5757
code: ErrorCode::PARSE_ERROR,
5858
message: "Parse error",
5959
data: "Invalid JSON",
@@ -74,7 +74,7 @@ def process_request(request, id_validation_pattern:, &method_finder)
7474
'Method name must be a string and not start with "rpc."'
7575
end
7676

77-
return error_response(id: :unknown_id, id_validation_pattern:, error: {
77+
return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
7878
code: ErrorCode::INVALID_REQUEST,
7979
message: "Invalid Request",
8080
data: error,
@@ -84,7 +84,7 @@ def process_request(request, id_validation_pattern:, &method_finder)
8484
params = request[:params]
8585

8686
unless valid_params?(params)
87-
return error_response(id:, id_validation_pattern:, error: {
87+
return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
8888
code: ErrorCode::INVALID_PARAMS,
8989
message: "Invalid params",
9090
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)
9595
method = method_finder.call(method_name)
9696

9797
if method.nil?
98-
return error_response(id:, id_validation_pattern:, error: {
98+
return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
9999
code: ErrorCode::METHOD_NOT_FOUND,
100100
message: "Method not found",
101101
data: method_name,
@@ -104,9 +104,9 @@ def process_request(request, id_validation_pattern:, &method_finder)
104104

105105
result = method.call(params)
106106

107-
success_response(id:, result:)
107+
success_response(id: id, result: result)
108108
rescue StandardError => e
109-
error_response(id:, id_validation_pattern:, error: {
109+
error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
110110
code: ErrorCode::INTERNAL_ERROR,
111111
message: "Internal error",
112112
data: e.message,
@@ -136,8 +136,8 @@ def valid_params?(params)
136136
def success_response(id:, result:)
137137
{
138138
jsonrpc: Version::V2_0,
139-
id:,
140-
result:,
139+
id: id,
140+
result: result,
141141
} unless id.nil?
142142
end
143143

lib/mcp/client/http.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,42 @@ def send_request(request:)
1818
rescue Faraday::BadRequestError => e
1919
raise RequestHandlerError.new(
2020
"The #{method} request is invalid",
21-
{ method:, params: },
21+
{ method: method, params: params },
2222
error_type: :bad_request,
2323
original_error: e,
2424
)
2525
rescue Faraday::UnauthorizedError => e
2626
raise RequestHandlerError.new(
2727
"You are unauthorized to make #{method} requests",
28-
{ method:, params: },
28+
{ method: method, params: params },
2929
error_type: :unauthorized,
3030
original_error: e,
3131
)
3232
rescue Faraday::ForbiddenError => e
3333
raise RequestHandlerError.new(
3434
"You are forbidden to make #{method} requests",
35-
{ method:, params: },
35+
{ method: method, params: params },
3636
error_type: :forbidden,
3737
original_error: e,
3838
)
3939
rescue Faraday::ResourceNotFound => e
4040
raise RequestHandlerError.new(
4141
"The #{method} request is not found",
42-
{ method:, params: },
42+
{ method: method, params: params },
4343
error_type: :not_found,
4444
original_error: e,
4545
)
4646
rescue Faraday::UnprocessableEntityError => e
4747
raise RequestHandlerError.new(
4848
"The #{method} request is unprocessable",
49-
{ method:, params: },
49+
{ method: method, params: params },
5050
error_type: :unprocessable_entity,
5151
original_error: e,
5252
)
5353
rescue Faraday::Error => e # Catch-all
5454
raise RequestHandlerError.new(
5555
"Internal error handling #{method} request",
56-
{ method:, params: },
56+
{ method: method, params: params },
5757
error_type: :internal_error,
5858
original_error: e,
5959
)

lib/mcp/configuration.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ def merge(other)
8585
validate_tool_call_arguments = other.validate_tool_call_arguments
8686

8787
Configuration.new(
88-
exception_reporter:,
89-
instrumentation_callback:,
90-
protocol_version:,
91-
validate_tool_call_arguments:,
88+
exception_reporter: exception_reporter,
89+
instrumentation_callback: instrumentation_callback,
90+
protocol_version: protocol_version,
91+
validate_tool_call_arguments: validate_tool_call_arguments,
9292
)
9393
end
9494

lib/mcp/content.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def initialize(text, annotations: nil)
1111
end
1212

1313
def to_h
14-
{ text:, annotations:, type: "text" }.compact
14+
{ text: text, annotations: annotations, type: "text" }.compact
1515
end
1616
end
1717

@@ -25,7 +25,7 @@ def initialize(data, mime_type, annotations: nil)
2525
end
2626

2727
def to_h
28-
{ data:, mime_type:, annotations:, type: "image" }.compact
28+
{ data: data, mime_type: mime_type, annotations: annotations, type: "image" }.compact
2929
end
3030
end
3131
end

lib/mcp/instrumentation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def instrument_call(method, &block)
66
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
77
begin
88
@instrumentation_data = {}
9-
add_instrumentation_data(method:)
9+
add_instrumentation_data(method: method)
1010

1111
result = yield block
1212

lib/mcp/prompt.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def define(name: nil, title: nil, description: nil, icons: [], arguments: [], me
9696
icons icons
9797
arguments arguments
9898
define_singleton_method(:template) do |args, server_context: nil|
99-
instance_exec(args, server_context:, &block)
99+
instance_exec(args, server_context: server_context, &block)
100100
end
101101
meta meta
102102
end

0 commit comments

Comments
 (0)