ruby_llm-mcp 0.5.0 → 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 -620
- data/lib/generators/ruby_llm/mcp/install_generator.rb +27 -0
- data/lib/generators/ruby_llm/mcp/templates/README.txt +32 -0
- data/lib/generators/ruby_llm/mcp/templates/initializer.rb +42 -0
- data/lib/generators/ruby_llm/mcp/templates/mcps.yml +9 -0
- 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 +21 -9
- data/lib/tasks/release.rake +23 -0
- metadata +28 -8
- data/lib/ruby_llm/mcp/transports/http_client.rb +0 -26
- data/lib/ruby_llm/mcp/transports/timeout.rb +0 -32
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module RubyLlm
|
6
|
+
module Mcp
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
desc "Install RubyLLM MCP configuration files"
|
12
|
+
|
13
|
+
def create_initializer
|
14
|
+
template "initializer.rb", "config/initializers/ruby_llm_mcp.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_config_file
|
18
|
+
template "mcps.yml", "config/mcps.yml"
|
19
|
+
end
|
20
|
+
|
21
|
+
def display_readme
|
22
|
+
readme "README.txt" if behavior == :invoke
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
RubyLLM MCP has been successfully installed!
|
2
|
+
|
3
|
+
The following files have been created:
|
4
|
+
|
5
|
+
config/initializers/ruby_llm_mcp.rb - Main configuration file
|
6
|
+
config/mcps.json - MCP servers configuration
|
7
|
+
|
8
|
+
Next steps:
|
9
|
+
|
10
|
+
1. Edit config/initializers/ruby_llm_mcp.rb to configure your MCP settings
|
11
|
+
2. Edit config/mcps.json to define your MCP servers
|
12
|
+
3. Install any MCP servers you want to use (e.g., npm install @modelcontextprotocol/server-filesystem) or use remote MCPs
|
13
|
+
4. Update environment variables for any MCP servers that require authentication
|
14
|
+
|
15
|
+
Example usage in your Rails application:
|
16
|
+
|
17
|
+
# With Ruby::MCP installed in a controller or service
|
18
|
+
clients = RubyLLM::MCP.clients
|
19
|
+
|
20
|
+
# Get all tools use the configured client
|
21
|
+
tools = RubyLLM::MCP.tools
|
22
|
+
|
23
|
+
# Or use the configured client
|
24
|
+
client = RubyLLM::MCP.clients["file-system"]
|
25
|
+
|
26
|
+
# Or use the configured client
|
27
|
+
tools = client.tools
|
28
|
+
|
29
|
+
|
30
|
+
For more information, visit: https://github.com/patvice/ruby_llm-mcp
|
31
|
+
|
32
|
+
===============================================================================
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configure RubyLLM MCP
|
4
|
+
RubyLLM::MCP.configure do |config|
|
5
|
+
# Request timeout in milliseconds
|
6
|
+
config.request_timeout = 8000
|
7
|
+
|
8
|
+
# Maximum connections in the pool
|
9
|
+
config.max_connections = Float::INFINITY
|
10
|
+
|
11
|
+
# Pool timeout in seconds
|
12
|
+
config.pool_timeout = 5
|
13
|
+
|
14
|
+
# Enable complex parameter support for various providers
|
15
|
+
config.support_complex_parameters!
|
16
|
+
|
17
|
+
# Path to MCPs configuration file
|
18
|
+
config.config_path = Rails.root.join("config", "mcps.yml")
|
19
|
+
|
20
|
+
# Launch MCPs (:automatic, :manual)
|
21
|
+
config.launch_control = :automatic
|
22
|
+
|
23
|
+
# Configure roots for file system access
|
24
|
+
# config.roots = [
|
25
|
+
# Rails.root.to_s
|
26
|
+
# ]
|
27
|
+
|
28
|
+
# Configure sampling (optional)
|
29
|
+
config.sampling.enabled = false
|
30
|
+
|
31
|
+
# Set preferred model for sampling
|
32
|
+
# config.sampling.preferred_model do
|
33
|
+
# # Return the preferred model name
|
34
|
+
# "claude-3-5-sonnet-20240620"
|
35
|
+
# end
|
36
|
+
|
37
|
+
# Set a guard for sampling
|
38
|
+
# config.sampling.guard do
|
39
|
+
# # Return true to enable sampling, false to disable
|
40
|
+
# Rails.env.development?
|
41
|
+
# end
|
42
|
+
end
|
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