ruby_llm-mcp 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,16 +13,17 @@ module RubyLLM
13
13
  end
14
14
  end
15
15
 
16
- attr_reader :name, :description, :arguments, :mcp_client
16
+ attr_reader :name, :description, :arguments, :coordinator
17
17
 
18
- def initialize(mcp_client, name:, description:, arguments:)
19
- @mcp_client = mcp_client
20
- @name = name
21
- @description = description
18
+ def initialize(coordinator, prompt)
19
+ @coordinator = coordinator
20
+ @name = prompt["name"]
21
+ @description = prompt["description"]
22
+ @arguments = parse_arguments(prompt["arguments"])
23
+ end
22
24
 
23
- @arguments = arguments.map do |arg|
24
- Argument.new(name: arg["name"], description: arg["description"], required: arg["required"])
25
- end
25
+ def fetch(arguments = {})
26
+ fetch_prompt_messages(arguments)
26
27
  end
27
28
 
28
29
  def include(chat, arguments: {})
@@ -41,21 +42,21 @@ module RubyLLM
41
42
 
42
43
  alias say ask
43
44
 
44
- def arguments_search(argument, value)
45
- if @mcp_client.capabilities.completion?
46
- response = @mcp_client.completion(type: :prompt, name: @name, argument: argument, value: value)
45
+ def complete(argument, value)
46
+ if @coordinator.capabilities.completion?
47
+ response = @coordinator.completion_prompt(name: @name, argument: argument, value: value)
47
48
  response = response.dig("result", "completion")
48
49
 
49
50
  Completion.new(values: response["values"], total: response["total"], has_more: response["hasMore"])
50
51
  else
51
- raise Errors::CompletionNotAvailable, "Completion is not available for this MCP server"
52
+ raise Errors::CompletionNotAvailable.new(message: "Completion is not available for this MCP server")
52
53
  end
53
54
  end
54
55
 
55
56
  private
56
57
 
57
58
  def fetch_prompt_messages(arguments)
58
- response = @mcp_client.execute_prompt(
59
+ response = @coordinator.execute_prompt(
59
60
  name: @name,
60
61
  arguments: arguments
61
62
  )
@@ -86,10 +87,20 @@ module RubyLLM
86
87
  attachment = MCP::Attachment.new(content["content"], content["mime_type"])
87
88
  MCP::Content.new(text: nil, attachments: [attachment])
88
89
  when "resource"
89
- resource = Resource.new(mcp_client, content["resource"])
90
+ resource = Resource.new(coordinator, content["resource"])
90
91
  resource.to_content
91
92
  end
92
93
  end
94
+
95
+ def parse_arguments(arguments)
96
+ if arguments.nil?
97
+ []
98
+ else
99
+ arguments.map do |arg|
100
+ Argument.new(name: arg["name"], description: arg["description"], required: arg["required"])
101
+ end
102
+ end
103
+ end
93
104
  end
94
105
  end
95
106
  end
@@ -20,21 +20,33 @@ module RubyLLM
20
20
  def build_properties(param)
21
21
  case param.type
22
22
  when :array
23
- {
24
- type: param.type,
25
- items: { type: param.item_type }
26
- }
23
+ if param.item_type == :object
24
+ {
25
+ type: param.type,
26
+ items: { type: param.item_type, properties: clean_parameters(param.properties) }
27
+ }
28
+ else
29
+ {
30
+ type: param.type,
31
+ default: param.default,
32
+ items: { type: param.item_type, enum: param.enum }.compact
33
+ }.compact
34
+ end
27
35
  when :object
28
36
  {
29
37
  type: param.type,
30
38
  properties: clean_parameters(param.properties),
31
39
  required: required_parameters(param.properties)
32
40
  }
41
+ when :union
42
+ {
43
+ param.union_type => param.properties.map { |property| build_properties(property) }
44
+ }
33
45
  else
34
46
  {
35
47
  type: param.type,
36
48
  description: param.description
37
- }
49
+ }.compact
38
50
  end
39
51
  end
40
52
  end
@@ -43,4 +55,12 @@ module RubyLLM
43
55
  end
44
56
  end
45
57
 
46
- RubyLLM::Providers::Anthropic.extend(RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport)
58
+ module RubyLLM::Providers::Anthropic::Tools
59
+ def self.clean_parameters(parameters)
60
+ RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.clean_parameters(parameters)
61
+ end
62
+
63
+ def self.required_parameters(parameters)
64
+ RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport.required_parameters(parameters)
65
+ end
66
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Providers
6
+ module Gemini
7
+ module ComplexParameterSupport
8
+ module_function
9
+
10
+ # Format tool parameters for Gemini API
11
+ def format_parameters(parameters)
12
+ {
13
+ type: "OBJECT",
14
+ properties: parameters.transform_values { |param| build_properties(param) },
15
+ required: parameters.select { |_, p| p.required }.keys.map(&:to_s)
16
+ }
17
+ end
18
+
19
+ def build_properties(param)
20
+ properties = case param.type
21
+ when :array
22
+ if param.item_type == :object
23
+ {
24
+ type: param_type_for_gemini(param.type),
25
+ items: {
26
+ type: param_type_for_gemini(param.item_type),
27
+ properties: param.properties.transform_values { |value| build_properties(value) }
28
+ }
29
+ }
30
+ else
31
+ {
32
+ type: param_type_for_gemini(param.type),
33
+ default: param.default,
34
+ items: { type: param_type_for_gemini(param.item_type), enum: param.enum }.compact
35
+ }.compact
36
+ end
37
+ when :object
38
+ {
39
+ type: param_type_for_gemini(param.type),
40
+ properties: param.properties.transform_values { |value| build_properties(value) },
41
+ required: param.properties.select { |_, p| p.required }.keys
42
+ }
43
+ when :union
44
+ {
45
+ param.union_type => param.properties.map { |properties| build_properties(properties) }
46
+ }
47
+ else
48
+ {
49
+ type: param_type_for_gemini(param.type),
50
+ description: param.description
51
+ }
52
+ end
53
+
54
+ properties.compact
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ RubyLLM::Providers::Gemini.extend(RubyLLM::MCP::Providers::Gemini::ComplexParameterSupport)
@@ -10,21 +10,36 @@ module RubyLLM
10
10
  def param_schema(param)
11
11
  properties = case param.type
12
12
  when :array
13
- {
14
- type: param.type,
15
- items: { type: param.item_type }
16
- }
13
+ if param.item_type == :object
14
+ {
15
+ type: param.type,
16
+ items: {
17
+ type: param.item_type,
18
+ properties: param.properties.transform_values { |value| param_schema(value) }
19
+ }
20
+ }
21
+ else
22
+ {
23
+ type: param.type,
24
+ default: param.default,
25
+ items: { type: param.item_type, enum: param.enum }.compact
26
+ }.compact
27
+ end
17
28
  when :object
18
29
  {
19
30
  type: param.type,
20
31
  properties: param.properties.transform_values { |value| param_schema(value) },
21
32
  required: param.properties.select { |_, p| p.required }.keys
22
33
  }
34
+ when :union
35
+ {
36
+ param.union_type => param.properties.map { |property| param_schema(property) }
37
+ }
23
38
  else
24
39
  {
25
40
  type: param.type,
26
41
  description: param.description
27
- }
42
+ }.compact
28
43
  end
29
44
 
30
45
  properties.compact
@@ -3,10 +3,9 @@
3
3
  module RubyLLM
4
4
  module MCP
5
5
  module Requests
6
- class Completion
7
- def initialize(client, type:, name:, argument:, value:)
6
+ class CompletionPrompt
7
+ def initialize(client, name:, argument:, value:)
8
8
  @client = client
9
- @type = type
10
9
  @name = name
11
10
  @argument = argument
12
11
  @value = value
@@ -25,7 +24,7 @@ module RubyLLM
25
24
  method: "completion/complete",
26
25
  params: {
27
26
  ref: {
28
- type: ref_type,
27
+ type: "ref/prompt",
29
28
  name: @name
30
29
  },
31
30
  argument: {
@@ -35,15 +34,6 @@ module RubyLLM
35
34
  }
36
35
  }
37
36
  end
38
-
39
- def ref_type
40
- case @type
41
- when :prompt
42
- "ref/prompt"
43
- when :resource
44
- "ref/resource"
45
- end
46
- end
47
37
  end
48
38
  end
49
39
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Requests
6
+ class CompletionResource
7
+ def initialize(client, uri:, argument:, value:)
8
+ @client = client
9
+ @uri = uri
10
+ @argument = argument
11
+ @value = value
12
+ end
13
+
14
+ def call
15
+ @client.request(request_body)
16
+ end
17
+
18
+ private
19
+
20
+ def request_body
21
+ {
22
+ jsonrpc: "2.0",
23
+ id: 1,
24
+ method: "completion/complete",
25
+ params: {
26
+ ref: {
27
+ type: "ref/resource",
28
+ uri: @uri
29
+ },
30
+ argument: {
31
+ name: @argument,
32
+ value: @value
33
+ }
34
+ }
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RubyLLM::MCP::Requests::Notification < RubyLLM::MCP::Requests::Base
3
+ class RubyLLM::MCP::Requests::InitializeNotification < RubyLLM::MCP::Requests::Base
4
4
  def call
5
5
  client.request(notification_body, add_id: false, wait_for_response: false)
6
6
  end
@@ -3,37 +3,26 @@
3
3
  module RubyLLM
4
4
  module MCP
5
5
  class Resource
6
- attr_reader :uri, :name, :description, :mime_type, :mcp_client, :template
6
+ attr_reader :uri, :name, :description, :mime_type, :coordinator
7
7
 
8
- def initialize(mcp_client, resource, template: false)
9
- @mcp_client = mcp_client
8
+ def initialize(coordinator, resource)
9
+ @coordinator = coordinator
10
10
  @uri = resource["uri"]
11
11
  @name = resource["name"]
12
12
  @description = resource["description"]
13
13
  @mime_type = resource["mimeType"]
14
- @content = resource["content"] || nil
15
- @template = template
14
+ if resource.key?("content_response")
15
+ @content_response = resource["content_response"]
16
+ @content = @content_response["text"] || @content_response["blob"]
17
+ end
16
18
  end
17
19
 
18
- def content(arguments: {})
19
- return @content if @content && !template?
20
-
21
- response = if template?
22
- templated_uri = apply_template(@uri, arguments)
23
-
24
- read_response(uri: templated_uri)
25
- else
26
- read_response
27
- end
28
-
29
- content = response.dig("result", "contents", 0)
30
- @type = if content.key?("type")
31
- content["type"]
32
- else
33
- "text"
34
- end
20
+ def content
21
+ return @content unless @content.nil?
35
22
 
36
- @content = content["text"] || content["blob"]
23
+ response = read_response
24
+ @content_response = response.dig("result", "contents", 0)
25
+ @content = @content_response["text"] || @content_response["blob"]
37
26
  end
38
27
 
39
28
  def include(chat, **args)
@@ -45,42 +34,37 @@ module RubyLLM
45
34
  chat.add_message(message)
46
35
  end
47
36
 
48
- def to_content(arguments: {})
49
- content = content(arguments: arguments)
37
+ def to_content
38
+ content = self.content
50
39
 
51
- case @type
40
+ case content_type
52
41
  when "text"
53
- MCP::Content.new(content)
42
+ MCP::Content.new(text: "#{name}: #{description}\n\n#{content}")
54
43
  when "blob"
55
44
  attachment = MCP::Attachment.new(content, mime_type)
56
45
  MCP::Content.new(text: "#{name}: #{description}", attachments: [attachment])
57
46
  end
58
47
  end
59
48
 
60
- def arguments_search(argument, value)
61
- if template? && @mcp_client.capabilities.completion?
62
- response = @mcp_client.completion(type: :resource, name: @name, argument: argument, value: value)
63
- response = response.dig("result", "completion")
49
+ private
50
+
51
+ def content_type
52
+ return "text" if @content_response.nil?
64
53
 
65
- Completion.new(values: response["values"], total: response["total"], has_more: response["hasMore"])
54
+ if @content_response.key?("blob")
55
+ "blob"
66
56
  else
67
- raise Errors::CompletionNotAvailable, "Completion is not available for this MCP server"
57
+ "text"
68
58
  end
69
59
  end
70
60
 
71
- def template?
72
- @template
73
- end
74
-
75
- private
76
-
77
61
  def read_response(uri: @uri)
78
62
  parsed = URI.parse(uri)
79
63
  case parsed.scheme
80
64
  when "http", "https"
81
65
  fetch_uri_content(uri)
82
66
  else # file:// or git://
83
- @read_response ||= @mcp_client.resource_read_request(uri: uri)
67
+ @read_response ||= @coordinator.resource_read(uri: uri)
84
68
  end
85
69
  end
86
70
 
@@ -88,12 +72,6 @@ module RubyLLM
88
72
  response = Faraday.get(uri)
89
73
  { "result" => { "contents" => [{ "text" => response.body }] } }
90
74
  end
91
-
92
- def apply_template(uri, arguments)
93
- uri.gsub(/\{(\w+)\}/) do
94
- arguments[::Regexp.last_match(1).to_sym] || "{#{::Regexp.last_match(1)}}"
95
- end
96
- end
97
75
  end
98
76
  end
99
77
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class ResourceTemplate
6
+ attr_reader :uri, :name, :description, :mime_type, :coordinator, :template
7
+
8
+ def initialize(coordinator, resource)
9
+ @coordinator = coordinator
10
+ @uri = resource["uriTemplate"]
11
+ @name = resource["name"]
12
+ @description = resource["description"]
13
+ @mime_type = resource["mimeType"]
14
+ end
15
+
16
+ def fetch_resource(arguments: {})
17
+ uri = apply_template(@uri, arguments)
18
+ response = read_response(uri)
19
+ content_response = response.dig("result", "contents", 0)
20
+
21
+ Resource.new(coordinator, {
22
+ "uri" => uri,
23
+ "name" => "#{@name} (#{uri})",
24
+ "description" => @description,
25
+ "mimeType" => @mime_type,
26
+ "content_response" => content_response
27
+ })
28
+ end
29
+
30
+ def to_content(arguments: {})
31
+ fetch_resource(arguments: arguments).to_content
32
+ end
33
+
34
+ def complete(argument, value)
35
+ if @coordinator.capabilities.completion?
36
+ response = @coordinator.completion_resource(uri: @uri, argument: argument, value: value)
37
+ response = response.dig("result", "completion")
38
+
39
+ Completion.new(values: response["values"], total: response["total"], has_more: response["hasMore"])
40
+ else
41
+ raise Errors::CompletionNotAvailable.new(message: "Completion is not available for this MCP server")
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def content_type
48
+ if @content.key?("type")
49
+ @content["type"]
50
+ else
51
+ "text"
52
+ end
53
+ end
54
+
55
+ def read_response(uri)
56
+ parsed = URI.parse(uri)
57
+ case parsed.scheme
58
+ when "http", "https"
59
+ fetch_uri_content(uri)
60
+ else # file:// or git://
61
+ @coordinator.resource_read(uri: uri)
62
+ end
63
+ end
64
+
65
+ def fetch_uri_content(uri)
66
+ response = Faraday.get(uri)
67
+ { "result" => { "contents" => [{ "text" => response.body }] } }
68
+ end
69
+
70
+ def apply_template(uri, arguments)
71
+ uri.gsub(/\{(\w+)\}/) do
72
+ arguments[::Regexp.last_match(1).to_s] ||
73
+ arguments[::Regexp.last_match(1).to_sym] ||
74
+ "{#{::Regexp.last_match(1)}}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -3,28 +3,29 @@
3
3
  module RubyLLM
4
4
  module MCP
5
5
  class Tool < RubyLLM::Tool
6
- attr_reader :name, :description, :parameters, :mcp_client, :tool_response
6
+ attr_reader :name, :description, :parameters, :coordinator, :tool_response
7
7
 
8
- def initialize(mcp_client, tool_response)
8
+ def initialize(coordinator, tool_response)
9
9
  super()
10
- @mcp_client = mcp_client
10
+ @coordinator = coordinator
11
11
 
12
12
  @name = tool_response["name"]
13
- @description = tool_response["description"]
13
+ @description = tool_response["description"].to_s
14
14
  @parameters = create_parameters(tool_response["inputSchema"])
15
15
  end
16
16
 
17
17
  def execute(**params)
18
- response = @mcp_client.execute_tool(
18
+ response = @coordinator.execute_tool(
19
19
  name: @name,
20
20
  parameters: params
21
21
  )
22
22
 
23
23
  text_values = response.dig("result", "content").map { |content| content["text"] }.compact.join("\n")
24
+
24
25
  if text_values.empty?
25
26
  create_content_for_message(response.dig("result", "content", 0))
26
27
  else
27
- create_content_for_message({ type: "text", text: text_values })
28
+ create_content_for_message({ "type" => "text", "text" => text_values })
28
29
  end
29
30
  end
30
31
 
@@ -32,20 +33,16 @@ module RubyLLM
32
33
 
33
34
  def create_parameters(input_schema)
34
35
  params = {}
36
+ return params if input_schema["properties"].nil?
37
+
35
38
  input_schema["properties"].each_key do |key|
36
- param = RubyLLM::MCP::Parameter.new(
37
- key,
38
- type: input_schema["properties"][key]["type"],
39
- desc: input_schema["properties"][key]["description"],
40
- required: input_schema["properties"][key]["required"]
41
- )
42
-
43
- if param.type == "array"
44
- param.items = input_schema["properties"][key]["items"]
45
- elsif param.type == "object"
46
- properties = create_parameters(input_schema["properties"][key]["properties"])
47
- param.properties = properties
48
- end
39
+ param_data = input_schema.dig("properties", key)
40
+
41
+ param = if param_data.key?("oneOf") || param_data.key?("anyOf") || param_data.key?("allOf")
42
+ process_union_parameter(key, param_data)
43
+ else
44
+ process_parameter(key, param_data)
45
+ end
49
46
 
50
47
  params[key] = param
51
48
  end
@@ -53,15 +50,64 @@ module RubyLLM
53
50
  params
54
51
  end
55
52
 
53
+ def process_union_parameter(key, param_data)
54
+ union_type = param_data.keys.first
55
+ param = RubyLLM::MCP::Parameter.new(
56
+ key,
57
+ type: :union,
58
+ union_type: union_type
59
+ )
60
+
61
+ param.properties = param_data[union_type].map do |value|
62
+ process_parameter(key, value, lifted_type: param_data["type"])
63
+ end.compact
64
+
65
+ param
66
+ end
67
+
68
+ def process_parameter(key, param_data, lifted_type: nil)
69
+ param = RubyLLM::MCP::Parameter.new(
70
+ key,
71
+ type: param_data["type"] || lifted_type,
72
+ desc: param_data["description"],
73
+ required: param_data["required"],
74
+ default: param_data["default"]
75
+ )
76
+
77
+ if param.type == :array
78
+ items = param_data["items"]
79
+ param.items = items
80
+ if items.key?("properties")
81
+ param.properties = create_parameters(items)
82
+ end
83
+ if items.key?("enum")
84
+ param.enum = items["enum"]
85
+ end
86
+ elsif param.type == :object
87
+ if param_data.key?("properties")
88
+ param.properties = create_parameters(param_data)
89
+ end
90
+ end
91
+
92
+ param
93
+ end
94
+
56
95
  def create_content_for_message(content)
57
96
  case content["type"]
58
97
  when "text"
59
98
  MCP::Content.new(text: content["text"])
60
99
  when "image", "audio"
61
- attachment = MCP::Attachment.new(content["content"], content["mime_type"])
100
+ attachment = MCP::Attachment.new(content["data"], content["mimeType"])
62
101
  MCP::Content.new(text: nil, attachments: [attachment])
63
102
  when "resource"
64
- resource = Resource.new(mcp_client, content["resource"])
103
+ resource_data = {
104
+ "name" => name,
105
+ "description" => description,
106
+ "uri" => content.dig("resource", "uri"),
107
+ "content" => content["resource"]
108
+ }
109
+
110
+ resource = Resource.new(coordinator, resource_data)
65
111
  resource.to_content
66
112
  end
67
113
  end