Skip to content

Commit cb53555

Browse files
authored
Merge pull request #82 from patvice/original-ruby-parameters-bug
Support both base and MCP Parameters in ComplexParameterSupport via Delegation
2 parents 0233d66 + 03b2054 commit cb53555

File tree

143 files changed

+5603
-3089
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+5603
-3089
lines changed

lib/ruby_llm/mcp/parameter.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ module MCP
77
class Parameter < RubyLLM::Parameter
88
attr_accessor :items, :properties, :enum, :union_type, :default, :title
99

10+
class << self
11+
def all_mcp_parameters?(parameters)
12+
parameters.is_a?(Hash) &&
13+
parameters.any? &&
14+
parameters.values.all? { |p| p.is_a?(RubyLLM::MCP::Parameter) }
15+
end
16+
end
17+
1018
def initialize(name, type: "string", title: nil, desc: nil, required: true, default: nil, union_type: nil) # rubocop:disable Metrics/ParameterLists
1119
super(name, type: type.to_sym, desc: desc, required: required)
1220
@title = title

lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ module ComplexParameterSupport
99

1010
def clean_parameters(parameters)
1111
parameters.transform_values do |param|
12-
build_properties(param).compact
12+
mcp_build_properties(param).compact
1313
end
1414
end
1515

1616
def required_parameters(parameters)
1717
parameters.select { |_, param| param.required }.keys
1818
end
1919

20-
def build_properties(param) # rubocop:disable Metrics/MethodLength
20+
def mcp_build_properties(param) # rubocop:disable Metrics/MethodLength
2121
case param.type
2222
when :array
2323
if param.item_type == :object
@@ -46,7 +46,7 @@ def build_properties(param) # rubocop:disable Metrics/MethodLength
4646
}.compact
4747
when :union
4848
{
49-
param.union_type => param.properties.map { |property| build_properties(property) }
49+
param.union_type => param.properties.map { |property| mcp_build_properties(property) }
5050
}
5151
else
5252
{
@@ -63,11 +63,25 @@ def build_properties(param) # rubocop:disable Metrics/MethodLength
6363
end
6464

6565
module RubyLLM::Providers::Anthropic::Tools
66-
def self.clean_parameters(parameters)
67-
RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.clean_parameters(parameters)
66+
alias original_clean_parameters clean_parameters
67+
alias original_required_parameters required_parameters
68+
module_function :original_clean_parameters, :original_required_parameters
69+
70+
def clean_parameters(parameters)
71+
if RubyLLM::MCP::Parameter.all_mcp_parameters?(parameters)
72+
return RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.clean_parameters(parameters)
73+
end
74+
75+
original_clean_parameters(parameters)
6876
end
77+
module_function :clean_parameters
78+
79+
def required_parameters(parameters)
80+
if RubyLLM::MCP::Parameter.all_mcp_parameters?(parameters)
81+
return RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.required_parameters(parameters)
82+
end
6983

70-
def self.required_parameters(parameters)
71-
RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.required_parameters(parameters)
84+
original_required_parameters(parameters)
7285
end
86+
module_function :required_parameters
7387
end

lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ module ComplexParameterSupport
1111
def format_parameters(parameters)
1212
{
1313
type: "OBJECT",
14-
properties: parameters.transform_values { |param| build_properties(param) },
14+
properties: parameters.transform_values { |param| mcp_build_properties(param) },
1515
required: parameters.select { |_, p| p.required }.keys.map(&:to_s)
1616
}
1717
end
1818

19-
def build_properties(param) # rubocop:disable Metrics/MethodLength
19+
def mcp_build_properties(param) # rubocop:disable Metrics/MethodLength
2020
properties = case param.type
2121
when :array
2222
if param.item_type == :object
@@ -26,7 +26,7 @@ def build_properties(param) # rubocop:disable Metrics/MethodLength
2626
description: param.description,
2727
items: {
2828
type: param_type_for_gemini(param.item_type),
29-
properties: param.properties.transform_values { |value| build_properties(value) }
29+
properties: param.properties.transform_values { |value| mcp_build_properties(value) }
3030
}
3131
}.compact
3232
else
@@ -43,12 +43,12 @@ def build_properties(param) # rubocop:disable Metrics/MethodLength
4343
type: param_type_for_gemini(param.type),
4444
title: param.title,
4545
description: param.description,
46-
properties: param.properties.transform_values { |value| build_properties(value) },
46+
properties: param.properties.transform_values { |value| mcp_build_properties(value) },
4747
required: param.properties.select { |_, p| p.required }.keys
4848
}.compact
4949
when :union
5050
{
51-
param.union_type => param.properties.map { |properties| build_properties(properties) }
51+
param.union_type => param.properties.map { |properties| mcp_build_properties(properties) }
5252
}
5353
else
5454
{
@@ -60,10 +60,27 @@ def build_properties(param) # rubocop:disable Metrics/MethodLength
6060

6161
properties.compact
6262
end
63+
64+
def param_type_for_gemini(type)
65+
RubyLLM::Providers::Gemini::Tools.param_type_for_gemini(type)
66+
end
6367
end
6468
end
6569
end
6670
end
6771
end
6872

69-
RubyLLM::Providers::Gemini.extend(RubyLLM::MCP::Providers::Gemini::ComplexParameterSupport)
73+
module RubyLLM::Providers::Gemini::Tools
74+
alias original_format_parameters format_parameters
75+
module_function :original_format_parameters
76+
module_function :param_type_for_gemini
77+
78+
def format_parameters(parameters)
79+
if RubyLLM::MCP::Parameter.all_mcp_parameters?(parameters)
80+
return RubyLLM::MCP::Providers::Gemini::ComplexParameterSupport.format_parameters(parameters)
81+
end
82+
83+
original_format_parameters(parameters)
84+
end
85+
module_function :format_parameters
86+
end

lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,16 @@ def param_schema(param) # rubocop:disable Metrics/MethodLength
5757
end
5858
end
5959

60-
RubyLLM::Providers::OpenAI.extend(RubyLLM::MCP::Providers::OpenAI::ComplexParameterSupport)
60+
module RubyLLM::Providers::OpenAI::Tools
61+
alias original_param_schema param_schema
62+
module_function :original_param_schema
63+
64+
def param_schema(param)
65+
if param.is_a?(RubyLLM::MCP::Parameter)
66+
return RubyLLM::MCP::Providers::OpenAI::ComplexParameterSupport.param_schema(param)
67+
end
68+
69+
original_param_schema(param)
70+
end
71+
module_function :param_schema
72+
end

lib/ruby_llm/mcp/transports/streamable_http.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,11 @@ def start_sse(options) # rubocop:disable Metrics/MethodLength
496496
# Server doesn't support SSE - this is acceptable
497497
RubyLLM::MCP.logger.info "Server does not support SSE streaming"
498498
nil
499+
when 409
500+
# Conflict - SSE connection already exists for this session
501+
# This is expected when reusing sessions and is acceptable
502+
RubyLLM::MCP.logger.debug "SSE stream already exists for this session"
503+
nil
499504
else
500505
reason_phrase = response.respond_to?(:reason_phrase) ? response.reason_phrase : nil
501506
raise Errors::TransportError.new(

lib/ruby_llm/mcp/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module RubyLLM
44
module MCP
5-
VERSION = "0.6.3"
5+
VERSION = "0.6.4"
66
end
77
end

spec/fixtures/typescript-mcp/src/tools/utilities.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,33 +107,40 @@ export function setupUtilityTools(server: McpServer) {
107107
throw new Error("Only HTTP and HTTPS URLs are supported");
108108
}
109109

110-
// Fetch the website content with timeout
111-
const controller = new AbortController();
112-
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
113-
114-
const headers = {
115-
"User-Agent": "Mozilla/5.0 (compatible; MCP-Tool/1.0)",
116-
...customHeaders,
117-
};
118-
119-
const headersArray = Object.entries(headers).map(([key, value]) => [
120-
key,
121-
value,
122-
]);
123-
124-
const response = await fetch(url.href, {
125-
headers: Object.fromEntries(headersArray),
126-
signal: controller.signal,
127-
});
128-
129-
clearTimeout(timeoutId);
130-
131-
if (!response.ok) {
132-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
110+
let html: string;
111+
112+
// Return hardcoded response for example.com to avoid SSL certificate issues
113+
if (websiteUrl === "https://www.example.com/") {
114+
html = '<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>';
115+
} else {
116+
// Fetch the website content with timeout
117+
const controller = new AbortController();
118+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
119+
120+
const headers = {
121+
"User-Agent": "Mozilla/5.0 (compatible; MCP-Tool/1.0)",
122+
...customHeaders,
123+
};
124+
125+
const headersArray = Object.entries(headers).map(([key, value]) => [
126+
key,
127+
value,
128+
]);
129+
130+
const response = await fetch(url.href, {
131+
headers: Object.fromEntries(headersArray),
132+
signal: controller.signal,
133+
});
134+
135+
clearTimeout(timeoutId);
136+
137+
if (!response.ok) {
138+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
139+
}
140+
141+
html = await response.text();
133142
}
134143

135-
const html = await response.text();
136-
137144
// Basic HTML content extraction (remove script, style, and HTML tags)
138145
const cleanText = html
139146
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "") // Remove scripts

spec/fixtures/vcr_cassettes/sample_with_stdio_client_can_call_a_block_to_determine_the_preferred_model__accessing_the_model_preferences.yml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/fixtures/vcr_cassettes/sample_with_streamable_with_anthropic_claude-3-5-sonnet-20240620_executes_a_chat_message_and_provides_information_to_the_server__without_a_guard.yml renamed to spec/fixtures/vcr_cassettes/sample_with_stdio_with_anthropic_claude-sonnet-4_executes_a_chat_message_and_provides_information_to_the_server__without_a_guard.yml

Lines changed: 25 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)