ruby_llm-mcp 0.5.1 → 0.6.0
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.
- checksums.yaml +4 -4
- data/README.md +20 -637
- data/lib/ruby_llm/mcp/client.rb +56 -2
- data/lib/ruby_llm/mcp/completion.rb +3 -2
- data/lib/ruby_llm/mcp/configuration.rb +30 -1
- data/lib/ruby_llm/mcp/coordinator.rb +30 -6
- data/lib/ruby_llm/mcp/elicitation.rb +46 -0
- data/lib/ruby_llm/mcp/errors.rb +2 -0
- data/lib/ruby_llm/mcp/prompt.rb +4 -3
- data/lib/ruby_llm/mcp/protocol.rb +34 -0
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +13 -3
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +13 -3
- data/lib/ruby_llm/mcp/resource.rb +1 -2
- data/lib/ruby_llm/mcp/resource_template.rb +4 -3
- data/lib/ruby_llm/mcp/response_handler.rb +10 -1
- data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
- data/lib/ruby_llm/mcp/result.rb +2 -1
- data/lib/ruby_llm/mcp/tool.rb +33 -5
- data/lib/ruby_llm/mcp/transports/sse.rb +69 -25
- data/lib/ruby_llm/mcp/transports/stdio.rb +2 -2
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +87 -19
- data/lib/ruby_llm/mcp/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +47 -0
- data/lib/ruby_llm/mcp/transports/support/timeout.rb +34 -0
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +9 -7
- metadata +23 -8
- data/lib/ruby_llm/mcp/transports/http_client.rb +0 -26
- data/lib/ruby_llm/mcp/transports/timeout.rb +0 -32
data/lib/ruby_llm/mcp/client.rb
CHANGED
@@ -8,6 +8,7 @@ module RubyLLM
|
|
8
8
|
extend Forwardable
|
9
9
|
|
10
10
|
attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on, :roots
|
11
|
+
attr_accessor :linked_resources
|
11
12
|
|
12
13
|
def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
|
13
14
|
@name = name
|
@@ -26,6 +27,8 @@ module RubyLLM
|
|
26
27
|
|
27
28
|
@log_level = nil
|
28
29
|
|
30
|
+
@linked_resources = []
|
31
|
+
|
29
32
|
setup_roots
|
30
33
|
setup_sampling
|
31
34
|
|
@@ -72,7 +75,8 @@ module RubyLLM
|
|
72
75
|
|
73
76
|
fetch(:resources, refresh) do
|
74
77
|
resources = @coordinator.resource_list
|
75
|
-
build_map(resources, MCP::Resource)
|
78
|
+
resources = build_map(resources, MCP::Resource)
|
79
|
+
include_linked_resources(resources)
|
76
80
|
end
|
77
81
|
|
78
82
|
@resources.values
|
@@ -157,7 +161,10 @@ module RubyLLM
|
|
157
161
|
end
|
158
162
|
|
159
163
|
def on_logging(level: Logging::WARNING, &block)
|
160
|
-
@
|
164
|
+
@on_logging_level = level
|
165
|
+
if alive?
|
166
|
+
@coordinator.set_logging(level: level)
|
167
|
+
end
|
161
168
|
|
162
169
|
@on[:logging] = block
|
163
170
|
self
|
@@ -172,6 +179,37 @@ module RubyLLM
|
|
172
179
|
self
|
173
180
|
end
|
174
181
|
|
182
|
+
def elicitation_enabled?
|
183
|
+
@on.key?(:elicitation) && !@on[:elicitation].nil?
|
184
|
+
end
|
185
|
+
|
186
|
+
def on_elicitation(&block)
|
187
|
+
@on[:elicitation] = block
|
188
|
+
self
|
189
|
+
end
|
190
|
+
|
191
|
+
def to_h
|
192
|
+
{
|
193
|
+
name: @name,
|
194
|
+
transport_type: @transport_type,
|
195
|
+
request_timeout: @request_timeout,
|
196
|
+
start: @start,
|
197
|
+
config: @config,
|
198
|
+
on: @on,
|
199
|
+
tools: @tools,
|
200
|
+
resources: @resources,
|
201
|
+
resource_templates: @resource_templates,
|
202
|
+
prompts: @prompts,
|
203
|
+
log_level: @log_level
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
alias as_json to_h
|
208
|
+
|
209
|
+
def inspect
|
210
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} #{to_h.map { |k, v| "#{k}: #{v}" }.join(', ')}>"
|
211
|
+
end
|
212
|
+
|
175
213
|
private
|
176
214
|
|
177
215
|
def setup_coordinator
|
@@ -199,6 +237,14 @@ module RubyLLM
|
|
199
237
|
end
|
200
238
|
end
|
201
239
|
|
240
|
+
def include_linked_resources(resources)
|
241
|
+
@linked_resources.each do |resource|
|
242
|
+
resources[resource.name] = resource
|
243
|
+
end
|
244
|
+
|
245
|
+
resources
|
246
|
+
end
|
247
|
+
|
202
248
|
def setup_roots
|
203
249
|
@roots = Roots.new(paths: MCP.config.roots, coordinator: @coordinator)
|
204
250
|
end
|
@@ -206,6 +252,14 @@ module RubyLLM
|
|
206
252
|
def setup_sampling
|
207
253
|
@on[:sampling] = MCP.config.sampling.guard
|
208
254
|
end
|
255
|
+
|
256
|
+
def setup_event_handlers
|
257
|
+
@on[:progress] = MCP.config.on_progress
|
258
|
+
@on[:human_in_the_loop] = MCP.config.on_human_in_the_loop
|
259
|
+
@on[:logging] = MCP.config.on_logging
|
260
|
+
@on_logging_level = MCP.config.on_logging_level
|
261
|
+
@on[:elicitation] = MCP.config.on_elicitation
|
262
|
+
end
|
209
263
|
end
|
210
264
|
end
|
211
265
|
end
|
@@ -3,9 +3,10 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module MCP
|
5
5
|
class Completion
|
6
|
-
attr_reader :values, :total, :has_more
|
6
|
+
attr_reader :argument, :values, :total, :has_more
|
7
7
|
|
8
|
-
def initialize(values:, total:, has_more:)
|
8
|
+
def initialize(argument:, values:, total:, has_more:)
|
9
|
+
@argument = argument
|
9
10
|
@values = values
|
10
11
|
@total = total
|
11
12
|
@has_more = has_more
|
@@ -92,8 +92,10 @@ module RubyLLM
|
|
92
92
|
:sampling,
|
93
93
|
:max_connections,
|
94
94
|
:pool_timeout,
|
95
|
+
:protocol_version,
|
95
96
|
:config_path,
|
96
|
-
:launch_control
|
97
|
+
:launch_control,
|
98
|
+
:on_logging_level
|
97
99
|
|
98
100
|
attr_writer :logger, :mcp_configuration
|
99
101
|
|
@@ -127,6 +129,26 @@ module RubyLLM
|
|
127
129
|
@mcp_configuration + load_mcps_config
|
128
130
|
end
|
129
131
|
|
132
|
+
def on_progress(&block)
|
133
|
+
@on_progress = block if block_given?
|
134
|
+
@on_progress
|
135
|
+
end
|
136
|
+
|
137
|
+
def on_human_in_the_loop(&block)
|
138
|
+
@on_human_in_the_loop = block if block_given?
|
139
|
+
@on_human_in_the_loop
|
140
|
+
end
|
141
|
+
|
142
|
+
def on_logging(&block)
|
143
|
+
@on_logging = block if block_given?
|
144
|
+
@on_logging
|
145
|
+
end
|
146
|
+
|
147
|
+
def on_elicitation(&block)
|
148
|
+
@on_elicitation = block if block_given?
|
149
|
+
@on_elicitation
|
150
|
+
end
|
151
|
+
|
130
152
|
def inspect
|
131
153
|
redacted = lambda do |name, value|
|
132
154
|
if name.match?(/_id|_key|_secret|_token$/)
|
@@ -180,6 +202,13 @@ module RubyLLM
|
|
180
202
|
|
181
203
|
# Sampling configuration
|
182
204
|
@sampling.reset!
|
205
|
+
|
206
|
+
# Event handlers
|
207
|
+
@on_progress = nil
|
208
|
+
@on_human_in_the_loop = nil
|
209
|
+
@on_elicitation = nil
|
210
|
+
@on_logging_level = nil
|
211
|
+
@on_logging = nil
|
183
212
|
end
|
184
213
|
end
|
185
214
|
end
|
@@ -5,9 +5,6 @@ require "logger"
|
|
5
5
|
module RubyLLM
|
6
6
|
module MCP
|
7
7
|
class Coordinator
|
8
|
-
PROTOCOL_VERSION = "2025-03-26"
|
9
|
-
PV_2024_11_05 = "2024-11-05"
|
10
|
-
|
11
8
|
attr_reader :client, :transport_type, :config, :capabilities, :protocol_version
|
12
9
|
|
13
10
|
def initialize(client, transport_type:, config: {})
|
@@ -15,7 +12,7 @@ module RubyLLM
|
|
15
12
|
@transport_type = transport_type
|
16
13
|
@config = config
|
17
14
|
|
18
|
-
@protocol_version =
|
15
|
+
@protocol_version = MCP.config.protocol_version || MCP::Protocol.default_negotiated_version
|
19
16
|
|
20
17
|
@transport = nil
|
21
18
|
@capabilities = nil
|
@@ -28,7 +25,7 @@ module RubyLLM
|
|
28
25
|
def request(body, **options)
|
29
26
|
transport.request(body, **options)
|
30
27
|
rescue RubyLLM::MCP::Errors::TimeoutError => e
|
31
|
-
if transport&.alive?
|
28
|
+
if transport&.alive? && !e.request_id.nil?
|
32
29
|
cancelled_notification(reason: "Request timed out", request_id: e.request_id)
|
33
30
|
end
|
34
31
|
raise e
|
@@ -62,6 +59,16 @@ module RubyLLM
|
|
62
59
|
|
63
60
|
# Extract and store the negotiated protocol version
|
64
61
|
negotiated_version = initialize_response.value["protocolVersion"]
|
62
|
+
|
63
|
+
if negotiated_version && !MCP::Protocol.supported_version?(negotiated_version)
|
64
|
+
raise Errors::UnsupportedProtocolVersion.new(
|
65
|
+
message: <<~MESSAGE
|
66
|
+
Unsupported protocol version, and could not negotiate a supported version: #{negotiated_version}.
|
67
|
+
Supported versions: #{MCP::Protocol.supported_versions.join(', ')}
|
68
|
+
MESSAGE
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
65
72
|
@protocol_version = negotiated_version if negotiated_version
|
66
73
|
|
67
74
|
# Set the protocol version on the transport for subsequent requests
|
@@ -71,13 +78,17 @@ module RubyLLM
|
|
71
78
|
|
72
79
|
@capabilities = RubyLLM::MCP::ServerCapabilities.new(initialize_response.value["capabilities"])
|
73
80
|
initialize_notification
|
81
|
+
|
82
|
+
if client.logging_handler_enabled?
|
83
|
+
set_logging(level: client.on_logging_level)
|
84
|
+
end
|
74
85
|
end
|
75
86
|
|
76
87
|
def stop_transport
|
77
88
|
@transport&.close
|
78
89
|
@capabilities = nil
|
79
90
|
@transport = nil
|
80
|
-
@protocol_version =
|
91
|
+
@protocol_version = MCP::Protocol.default_negotiated_version
|
81
92
|
end
|
82
93
|
|
83
94
|
def restart_transport
|
@@ -150,6 +161,11 @@ module RubyLLM
|
|
150
161
|
RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
|
151
162
|
end
|
152
163
|
|
164
|
+
def register_resource(resource)
|
165
|
+
@client.linked_resources << resource
|
166
|
+
@client.resources[resource.name] = resource
|
167
|
+
end
|
168
|
+
|
153
169
|
def resource_list(cursor: nil)
|
154
170
|
result = RubyLLM::MCP::Requests::ResourceList.new(self, cursor: cursor).call
|
155
171
|
result.raise_error! if result.error?
|
@@ -239,6 +255,10 @@ module RubyLLM
|
|
239
255
|
RubyLLM::MCP::Responses::Error.new(self, **args).call
|
240
256
|
end
|
241
257
|
|
258
|
+
def elicitation_response(**args)
|
259
|
+
RubyLLM::MCP::Responses::Elicitation.new(self, **args).call
|
260
|
+
end
|
261
|
+
|
242
262
|
def client_capabilities
|
243
263
|
capabilities = {}
|
244
264
|
|
@@ -252,6 +272,10 @@ module RubyLLM
|
|
252
272
|
capabilities[:sampling] = {}
|
253
273
|
end
|
254
274
|
|
275
|
+
if client.elicitation_enabled?
|
276
|
+
capabilities[:elicitation] = {}
|
277
|
+
end
|
278
|
+
|
255
279
|
capabilities
|
256
280
|
end
|
257
281
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json-schema"
|
4
|
+
|
5
|
+
module RubyLLM
|
6
|
+
module MCP
|
7
|
+
class Elicitation
|
8
|
+
ACCEPT_ACTION = "accept"
|
9
|
+
CANCEL_ACTION = "cancel"
|
10
|
+
REJECT_ACTION = "reject"
|
11
|
+
|
12
|
+
attr_writer :structured_response
|
13
|
+
|
14
|
+
def initialize(coordinator, result)
|
15
|
+
@coordinator = coordinator
|
16
|
+
@result = result
|
17
|
+
@id = result.id
|
18
|
+
|
19
|
+
@message = @result.params["message"]
|
20
|
+
@requested_schema = @result.params["requestedSchema"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute
|
24
|
+
success = @coordinator.client.on[:elicitation].call(self)
|
25
|
+
if success
|
26
|
+
valid = validate_response
|
27
|
+
if valid
|
28
|
+
@coordinator.elicitation_response(id: @id, action: ACCEPT_ACTION, content: @structured_response)
|
29
|
+
else
|
30
|
+
@coordinator.elicitation_response(id: @id, action: CANCEL_ACTION, content: nil)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@coordinator.elicitation_response(id: @id, action: REJECT_ACTION, content: nil)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def message
|
38
|
+
@result.params["message"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_response
|
42
|
+
JSON::Validator.validate(@requested_schema, @structured_response)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/ruby_llm/mcp/errors.rb
CHANGED
data/lib/ruby_llm/mcp/prompt.rb
CHANGED
@@ -50,16 +50,17 @@ module RubyLLM
|
|
50
50
|
|
51
51
|
alias say ask
|
52
52
|
|
53
|
-
def complete(argument, value)
|
53
|
+
def complete(argument, value, context: nil)
|
54
54
|
if @coordinator.capabilities.completion?
|
55
|
-
result = @coordinator.completion_prompt(name: @name, argument: argument, value: value)
|
55
|
+
result = @coordinator.completion_prompt(name: @name, argument: argument, value: value, context: context)
|
56
56
|
if result.error?
|
57
57
|
return result.to_error
|
58
58
|
end
|
59
59
|
|
60
60
|
response = result.value["completion"]
|
61
61
|
|
62
|
-
Completion.new(values: response["values"], total: response["total"],
|
62
|
+
Completion.new(argument: argument, values: response["values"], total: response["total"],
|
63
|
+
has_more: response["hasMore"])
|
63
64
|
else
|
64
65
|
message = "Completion is not available for this MCP server"
|
65
66
|
raise Errors::Capabilities::CompletionNotAvailable.new(message: message)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
module Protocol
|
6
|
+
module_function
|
7
|
+
|
8
|
+
LATEST_PROTOCOL_VERSION = "2025-06-18"
|
9
|
+
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
|
10
|
+
SUPPORTED_PROTOCOL_VERSIONS = [
|
11
|
+
LATEST_PROTOCOL_VERSION,
|
12
|
+
"2025-03-26",
|
13
|
+
"2024-11-05",
|
14
|
+
"2024-10-07"
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def supported_version?(version)
|
18
|
+
SUPPORTED_PROTOCOL_VERSIONS.include?(version)
|
19
|
+
end
|
20
|
+
|
21
|
+
def supported_versions
|
22
|
+
SUPPORTED_PROTOCOL_VERSIONS
|
23
|
+
end
|
24
|
+
|
25
|
+
def latest_version
|
26
|
+
LATEST_PROTOCOL_VERSION
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_negotiated_version
|
30
|
+
DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -4,11 +4,12 @@ module RubyLLM
|
|
4
4
|
module MCP
|
5
5
|
module Requests
|
6
6
|
class CompletionPrompt
|
7
|
-
def initialize(coordinator, name:, argument:, value:)
|
7
|
+
def initialize(coordinator, name:, argument:, value:, context: nil)
|
8
8
|
@coordinator = coordinator
|
9
9
|
@name = name
|
10
10
|
@argument = argument
|
11
11
|
@value = value
|
12
|
+
@context = context
|
12
13
|
end
|
13
14
|
|
14
15
|
def call
|
@@ -30,8 +31,17 @@ module RubyLLM
|
|
30
31
|
argument: {
|
31
32
|
name: @argument,
|
32
33
|
value: @value
|
33
|
-
}
|
34
|
-
|
34
|
+
},
|
35
|
+
context: format_context
|
36
|
+
}.compact
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def format_context
|
41
|
+
return nil if @context.nil?
|
42
|
+
|
43
|
+
{
|
44
|
+
arguments: @context
|
35
45
|
}
|
36
46
|
end
|
37
47
|
end
|
@@ -4,11 +4,12 @@ module RubyLLM
|
|
4
4
|
module MCP
|
5
5
|
module Requests
|
6
6
|
class CompletionResource
|
7
|
-
def initialize(coordinator, uri:, argument:, value:)
|
7
|
+
def initialize(coordinator, uri:, argument:, value:, context: nil)
|
8
8
|
@coordinator = coordinator
|
9
9
|
@uri = uri
|
10
10
|
@argument = argument
|
11
11
|
@value = value
|
12
|
+
@context = context
|
12
13
|
end
|
13
14
|
|
14
15
|
def call
|
@@ -30,8 +31,17 @@ module RubyLLM
|
|
30
31
|
argument: {
|
31
32
|
name: @argument,
|
32
33
|
value: @value
|
33
|
-
}
|
34
|
-
|
34
|
+
},
|
35
|
+
context: format_context
|
36
|
+
}.compact
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def format_context
|
41
|
+
return nil if @context.nil?
|
42
|
+
|
43
|
+
{
|
44
|
+
arguments: @context
|
35
45
|
}
|
36
46
|
end
|
37
47
|
end
|
@@ -61,7 +61,6 @@ module RubyLLM
|
|
61
61
|
|
62
62
|
def to_content
|
63
63
|
content = self.content
|
64
|
-
|
65
64
|
case content_type
|
66
65
|
when "text"
|
67
66
|
MCP::Content.new(text: "#{name}: #{description}\n\n#{content}")
|
@@ -89,7 +88,7 @@ module RubyLLM
|
|
89
88
|
def content_type
|
90
89
|
return "text" if @content_response.nil?
|
91
90
|
|
92
|
-
if @content_response.key?("blob")
|
91
|
+
if @content_response.key?("blob") && !@content_response["blob"].nil?
|
93
92
|
"blob"
|
94
93
|
else
|
95
94
|
"text"
|
@@ -33,14 +33,15 @@ module RubyLLM
|
|
33
33
|
fetch_resource(arguments: arguments).to_content
|
34
34
|
end
|
35
35
|
|
36
|
-
def complete(argument, value)
|
36
|
+
def complete(argument, value, context: nil)
|
37
37
|
if @coordinator.capabilities.completion?
|
38
|
-
result = @coordinator.completion_resource(uri: @uri, argument: argument, value: value)
|
38
|
+
result = @coordinator.completion_resource(uri: @uri, argument: argument, value: value, context: context)
|
39
39
|
result.raise_error! if result.error?
|
40
40
|
|
41
41
|
response = result.value["completion"]
|
42
42
|
|
43
|
-
Completion.new(values: response["values"], total: response["total"],
|
43
|
+
Completion.new(argument: argument, values: response["values"], total: response["total"],
|
44
|
+
has_more: response["hasMore"])
|
44
45
|
else
|
45
46
|
message = "Completion is not available for this MCP server"
|
46
47
|
raise Errors::Capabilities::CompletionNotAvailable.new(message: message)
|
@@ -20,6 +20,9 @@ module RubyLLM
|
|
20
20
|
elsif result.sampling?
|
21
21
|
handle_sampling_response(result)
|
22
22
|
true
|
23
|
+
elsif result.elicitation?
|
24
|
+
handle_elicitation_response(result)
|
25
|
+
true
|
23
26
|
else
|
24
27
|
handle_unknown_request(result)
|
25
28
|
RubyLLM::MCP.logger.error("MCP client was sent unknown method type and could not respond: #{result.inspect}")
|
@@ -30,6 +33,7 @@ module RubyLLM
|
|
30
33
|
private
|
31
34
|
|
32
35
|
def handle_roots_response(result)
|
36
|
+
RubyLLM::MCP.logger.info("Roots request: #{result.inspect}")
|
33
37
|
if client.roots.active?
|
34
38
|
coordinator.roots_list_response(id: result.id, roots: client.roots)
|
35
39
|
else
|
@@ -44,10 +48,15 @@ module RubyLLM
|
|
44
48
|
return
|
45
49
|
end
|
46
50
|
|
47
|
-
RubyLLM::MCP.logger.info("Sampling
|
51
|
+
RubyLLM::MCP.logger.info("Sampling request: #{result.inspect}")
|
48
52
|
Sample.new(result, coordinator).execute
|
49
53
|
end
|
50
54
|
|
55
|
+
def handle_elicitation_response(result)
|
56
|
+
RubyLLM::MCP.logger.info("Elicitation request: #{result.inspect}")
|
57
|
+
Elicitation.new(coordinator, result).execute
|
58
|
+
end
|
59
|
+
|
51
60
|
def handle_unknown_request(result)
|
52
61
|
coordinator.error_response(id: result.id,
|
53
62
|
message: "Unknown method and could not respond: #{result.method}",
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
module Responses
|
6
|
+
class Elicitation
|
7
|
+
def initialize(coordinator, id:, action:, content:)
|
8
|
+
@coordinator = coordinator
|
9
|
+
@id = id
|
10
|
+
@action = action
|
11
|
+
@content = content
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
@coordinator.request(elicitation_response_body, add_id: false, wait_for_response: false)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def elicitation_response_body
|
21
|
+
{
|
22
|
+
jsonrpc: "2.0",
|
23
|
+
id: @id,
|
24
|
+
result: {
|
25
|
+
action: @action,
|
26
|
+
content: @content
|
27
|
+
}.compact
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/ruby_llm/mcp/result.rb
CHANGED
data/lib/ruby_llm/mcp/tool.rb
CHANGED
@@ -36,6 +36,10 @@ module RubyLLM
|
|
36
36
|
@mcp_name = tool_response["name"]
|
37
37
|
@description = tool_response["description"].to_s
|
38
38
|
@parameters = create_parameters(tool_response["inputSchema"])
|
39
|
+
|
40
|
+
@input_schema = tool_response["inputSchema"]
|
41
|
+
@output_schema = tool_response["outputSchema"]
|
42
|
+
|
39
43
|
@annotations = tool_response["annotations"] ? Annotation.new(tool_response["annotations"]) : nil
|
40
44
|
end
|
41
45
|
|
@@ -59,6 +63,15 @@ module RubyLLM
|
|
59
63
|
return { error: "Tool execution error: #{text_values}" }
|
60
64
|
end
|
61
65
|
|
66
|
+
if result.value.key?("structuredContent") && !@output_schema.nil?
|
67
|
+
is_valid = JSON::Validator.validate(@output_schema, result.value["structuredContent"])
|
68
|
+
unless is_valid
|
69
|
+
return { error: "Structued outputs was not invalid: #{result.value['structuredContent']}" }
|
70
|
+
end
|
71
|
+
|
72
|
+
return text_values
|
73
|
+
end
|
74
|
+
|
62
75
|
if text_values.empty?
|
63
76
|
create_content_for_message(result.value.dig("content", 0))
|
64
77
|
else
|
@@ -79,12 +92,12 @@ module RubyLLM
|
|
79
92
|
|
80
93
|
private
|
81
94
|
|
82
|
-
def create_parameters(
|
95
|
+
def create_parameters(schema)
|
83
96
|
params = {}
|
84
|
-
return params if
|
97
|
+
return params if schema["properties"].nil?
|
85
98
|
|
86
|
-
|
87
|
-
param_data =
|
99
|
+
schema["properties"].each_key do |key|
|
100
|
+
param_data = schema.dig("properties", key)
|
88
101
|
|
89
102
|
param = if param_data.key?("oneOf") || param_data.key?("anyOf") || param_data.key?("allOf")
|
90
103
|
process_union_parameter(key, param_data)
|
@@ -152,10 +165,25 @@ module RubyLLM
|
|
152
165
|
"name" => name,
|
153
166
|
"description" => description,
|
154
167
|
"uri" => content.dig("resource", "uri"),
|
155
|
-
"
|
168
|
+
"mimeType" => content.dig("resource", "mimeType"),
|
169
|
+
"content_response" => {
|
170
|
+
"text" => content.dig("resource", "text"),
|
171
|
+
"blob" => content.dig("resource", "blob")
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
resource = Resource.new(coordinator, resource_data)
|
176
|
+
resource.to_content
|
177
|
+
when "resource_link"
|
178
|
+
resource_data = {
|
179
|
+
"name" => content["name"],
|
180
|
+
"uri" => content["uri"],
|
181
|
+
"description" => content["description"],
|
182
|
+
"mimeType" => content["mimeType"]
|
156
183
|
}
|
157
184
|
|
158
185
|
resource = Resource.new(coordinator, resource_data)
|
186
|
+
@coordinator.register_resource(resource)
|
159
187
|
resource.to_content
|
160
188
|
end
|
161
189
|
end
|