Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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

Expand All @@ -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
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ plugins:
- rubocop-minitest
- rubocop-rake

AllCops:
TargetRubyVersion: 2.7

Gemspec/DevelopmentDependencies:
Enabled: true

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 5 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 13 additions & 13 deletions lib/json_rpc_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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

Expand Down
12 changes: 6 additions & 6 deletions lib/mcp/client/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
8 changes: 4 additions & 4 deletions lib/mcp/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions lib/mcp/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/resource/contents.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/resource/embedded.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/server/transports/streamable_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool/annotations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def to_h
idempotentHint: idempotent_hint,
openWorldHint: open_world_hint,
readOnlyHint: read_only_hint,
title:,
title: title,
}.compact
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion mcp.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Loading