ruby_llm_swarm-mcp 0.8.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +3 -44
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +4 -21
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +0 -20
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +3 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +2 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +32 -100
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +6 -32
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +2 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +0 -18
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +3 -82
- data/lib/ruby_llm/mcp/auth/session_manager.rb +2 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +2 -0
- data/lib/ruby_llm/mcp/client.rb +32 -119
- data/lib/ruby_llm/mcp/configuration.rb +6 -74
- data/lib/ruby_llm/mcp/coordinator.rb +304 -0
- data/lib/ruby_llm/mcp/elicitation.rb +6 -8
- data/lib/ruby_llm/mcp/errors.rb +0 -15
- data/lib/ruby_llm/mcp/notification_handler.rb +5 -21
- data/lib/ruby_llm/mcp/notifications/cancelled.rb +32 -0
- data/lib/ruby_llm/mcp/notifications/initialize.rb +24 -0
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
- data/lib/ruby_llm/mcp/prompt.rb +7 -7
- data/lib/ruby_llm/mcp/protocol.rb +34 -0
- data/lib/ruby_llm/mcp/railtie.rb +6 -8
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +50 -0
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +50 -0
- data/lib/ruby_llm/mcp/requests/initialization.rb +34 -0
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
- data/lib/ruby_llm/mcp/requests/ping.rb +24 -0
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +32 -0
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resource_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resource_read.rb +30 -0
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
- data/lib/ruby_llm/mcp/requests/tool_call.rb +35 -0
- data/lib/ruby_llm/mcp/requests/tool_list.rb +31 -0
- data/lib/ruby_llm/mcp/resource.rb +8 -6
- data/lib/ruby_llm/mcp/resource_template.rb +7 -7
- data/lib/ruby_llm/mcp/response_handler.rb +67 -0
- data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
- data/lib/ruby_llm/mcp/responses/error.rb +33 -0
- data/lib/ruby_llm/mcp/responses/ping.rb +28 -0
- data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
- data/lib/ruby_llm/mcp/result.rb +4 -8
- data/lib/ruby_llm/mcp/roots.rb +4 -4
- data/lib/ruby_llm/mcp/sample.rb +2 -6
- data/lib/ruby_llm/mcp/tool.rb +9 -9
- data/lib/ruby_llm/mcp/transport.rb +151 -0
- data/lib/ruby_llm/mcp/transports/sse.rb +435 -0
- data/lib/ruby_llm/mcp/transports/stdio.rb +231 -0
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +725 -0
- 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 +7 -30
- metadata +38 -33
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +0 -179
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +0 -292
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +0 -33
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +0 -52
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +0 -52
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +0 -86
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +0 -92
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +0 -107
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +0 -57
- data/lib/ruby_llm/mcp/native/client.rb +0 -387
- data/lib/ruby_llm/mcp/native/json_rpc.rb +0 -170
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +0 -39
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +0 -42
- data/lib/ruby_llm/mcp/native/messages/requests.rb +0 -206
- data/lib/ruby_llm/mcp/native/messages/responses.rb +0 -106
- data/lib/ruby_llm/mcp/native/messages.rb +0 -36
- data/lib/ruby_llm/mcp/native/notification.rb +0 -16
- data/lib/ruby_llm/mcp/native/protocol.rb +0 -36
- data/lib/ruby_llm/mcp/native/response_handler.rb +0 -110
- data/lib/ruby_llm/mcp/native/transport.rb +0 -88
- data/lib/ruby_llm/mcp/native/transports/sse.rb +0 -607
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +0 -356
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +0 -926
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +0 -28
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +0 -49
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +0 -36
- data/lib/ruby_llm/mcp/native.rb +0 -12
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module MCP
|
|
7
|
+
class Coordinator
|
|
8
|
+
attr_reader :client, :transport_type, :config, :capabilities, :protocol_version
|
|
9
|
+
|
|
10
|
+
def initialize(client, transport_type:, config: {})
|
|
11
|
+
@client = client
|
|
12
|
+
@transport_type = transport_type
|
|
13
|
+
@config = config
|
|
14
|
+
|
|
15
|
+
@protocol_version = MCP.config.protocol_version || MCP::Protocol.default_negotiated_version
|
|
16
|
+
|
|
17
|
+
@transport = nil
|
|
18
|
+
@capabilities = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def name
|
|
22
|
+
client.name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def request(body, **options)
|
|
26
|
+
transport.request(body, **options)
|
|
27
|
+
rescue RubyLLM::MCP::Errors::TimeoutError => e
|
|
28
|
+
if transport&.alive? && !e.request_id.nil?
|
|
29
|
+
cancelled_notification(reason: "Request timed out", request_id: e.request_id)
|
|
30
|
+
end
|
|
31
|
+
raise e
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def process_result(result)
|
|
35
|
+
if result.notification?
|
|
36
|
+
process_notification(result)
|
|
37
|
+
return nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if result.request?
|
|
41
|
+
process_request(result) if alive?
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if result.response?
|
|
46
|
+
return result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def start_transport
|
|
53
|
+
return unless capabilities.nil?
|
|
54
|
+
|
|
55
|
+
transport.start
|
|
56
|
+
|
|
57
|
+
initialize_response = initialize_request
|
|
58
|
+
initialize_response.raise_error! if initialize_response.error?
|
|
59
|
+
|
|
60
|
+
# Extract and store the negotiated protocol version
|
|
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
|
+
|
|
72
|
+
@protocol_version = negotiated_version if negotiated_version
|
|
73
|
+
|
|
74
|
+
# Set the protocol version on the transport for subsequent requests
|
|
75
|
+
if @transport.respond_to?(:set_protocol_version)
|
|
76
|
+
@transport.set_protocol_version(@protocol_version)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@capabilities = RubyLLM::MCP::ServerCapabilities.new(initialize_response.value["capabilities"])
|
|
80
|
+
initialize_notification
|
|
81
|
+
|
|
82
|
+
if client.logging_handler_enabled?
|
|
83
|
+
set_logging(level: client.on_logging_level)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stop_transport
|
|
88
|
+
@transport&.close
|
|
89
|
+
@capabilities = nil
|
|
90
|
+
@transport = nil
|
|
91
|
+
@protocol_version = MCP::Protocol.default_negotiated_version
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def restart_transport
|
|
95
|
+
@initialize_response = nil
|
|
96
|
+
stop_transport
|
|
97
|
+
start_transport
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def alive?
|
|
101
|
+
!!@transport&.alive?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ping
|
|
105
|
+
ping_request = RubyLLM::MCP::Requests::Ping.new(self)
|
|
106
|
+
if alive?
|
|
107
|
+
result = ping_request.call
|
|
108
|
+
else
|
|
109
|
+
transport.start
|
|
110
|
+
|
|
111
|
+
result = ping_request.call
|
|
112
|
+
@transport = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
result.value == {}
|
|
116
|
+
rescue RubyLLM::MCP::Errors::TimeoutError, RubyLLM::MCP::Errors::TransportError
|
|
117
|
+
false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_notification(result)
|
|
121
|
+
notification = result.notification
|
|
122
|
+
NotificationHandler.new(self).execute(notification)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def process_request(result)
|
|
126
|
+
ResponseHandler.new(self).execute(result)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def initialize_request
|
|
130
|
+
RubyLLM::MCP::Requests::Initialization.new(self).call
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def tool_list(cursor: nil)
|
|
134
|
+
result = RubyLLM::MCP::Requests::ToolList.new(self, cursor: cursor).call
|
|
135
|
+
result.raise_error! if result.error?
|
|
136
|
+
|
|
137
|
+
if result.next_cursor?
|
|
138
|
+
result.value["tools"] + tool_list(cursor: result.next_cursor)
|
|
139
|
+
else
|
|
140
|
+
result.value["tools"]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def execute_tool(**args)
|
|
145
|
+
if client.human_in_the_loop?
|
|
146
|
+
name = args[:name]
|
|
147
|
+
params = args[:parameters]
|
|
148
|
+
unless client.on[:human_in_the_loop].call(name, params)
|
|
149
|
+
result = Result.new(
|
|
150
|
+
{
|
|
151
|
+
"result" => {
|
|
152
|
+
"isError" => true,
|
|
153
|
+
"content" => [{ "type" => "text", "text" => "Tool call was cancelled by the client" }]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
return result
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def register_resource(resource)
|
|
165
|
+
@client.linked_resources << resource
|
|
166
|
+
@client.resources[resource.name] = resource
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def resource_list(cursor: nil)
|
|
170
|
+
result = RubyLLM::MCP::Requests::ResourceList.new(self, cursor: cursor).call
|
|
171
|
+
result.raise_error! if result.error?
|
|
172
|
+
|
|
173
|
+
if result.next_cursor?
|
|
174
|
+
result.value["resources"] + resource_list(cursor: result.next_cursor)
|
|
175
|
+
else
|
|
176
|
+
result.value["resources"]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def resource_read(**args)
|
|
181
|
+
RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def resource_template_list(cursor: nil)
|
|
185
|
+
result = RubyLLM::MCP::Requests::ResourceTemplateList.new(self, cursor: cursor).call
|
|
186
|
+
result.raise_error! if result.error?
|
|
187
|
+
|
|
188
|
+
if result.next_cursor?
|
|
189
|
+
result.value["resourceTemplates"] + resource_template_list(cursor: result.next_cursor)
|
|
190
|
+
else
|
|
191
|
+
result.value["resourceTemplates"]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def resources_subscribe(**args)
|
|
196
|
+
RubyLLM::MCP::Requests::ResourcesSubscribe.new(self, **args).call
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def prompt_list(cursor: nil)
|
|
200
|
+
result = RubyLLM::MCP::Requests::PromptList.new(self, cursor: cursor).call
|
|
201
|
+
result.raise_error! if result.error?
|
|
202
|
+
|
|
203
|
+
if result.next_cursor?
|
|
204
|
+
result.value["prompts"] + prompt_list(cursor: result.next_cursor)
|
|
205
|
+
else
|
|
206
|
+
result.value["prompts"]
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def execute_prompt(**args)
|
|
211
|
+
RubyLLM::MCP::Requests::PromptCall.new(self, **args).call
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def completion_resource(**args)
|
|
215
|
+
RubyLLM::MCP::Requests::CompletionResource.new(self, **args).call
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def completion_prompt(**args)
|
|
219
|
+
RubyLLM::MCP::Requests::CompletionPrompt.new(self, **args).call
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def set_logging(**args)
|
|
223
|
+
RubyLLM::MCP::Requests::LoggingSetLevel.new(self, **args).call
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
## Notifications
|
|
227
|
+
#
|
|
228
|
+
def initialize_notification
|
|
229
|
+
RubyLLM::MCP::Notifications::Initialize.new(self).call
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def cancelled_notification(**args)
|
|
233
|
+
RubyLLM::MCP::Notifications::Cancelled.new(self, **args).call
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def roots_list_change_notification
|
|
237
|
+
RubyLLM::MCP::Notifications::RootsListChange.new(self).call
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
## Responses
|
|
241
|
+
#
|
|
242
|
+
def ping_response(**args)
|
|
243
|
+
RubyLLM::MCP::Responses::Ping.new(self, **args).call
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def roots_list_response(**args)
|
|
247
|
+
RubyLLM::MCP::Responses::RootsList.new(self, **args).call
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def sampling_create_message_response(**args)
|
|
251
|
+
RubyLLM::MCP::Responses::SamplingCreateMessage.new(self, **args).call
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def error_response(**args)
|
|
255
|
+
RubyLLM::MCP::Responses::Error.new(self, **args).call
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def elicitation_response(**args)
|
|
259
|
+
RubyLLM::MCP::Responses::Elicitation.new(self, **args).call
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def client_capabilities
|
|
263
|
+
capabilities = {}
|
|
264
|
+
|
|
265
|
+
if client.roots.active?
|
|
266
|
+
capabilities[:roots] = {
|
|
267
|
+
listChanged: true
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if sampling_enabled?
|
|
272
|
+
capabilities[:sampling] = {}
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if client.elicitation_enabled?
|
|
276
|
+
capabilities[:elicitation] = {}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
capabilities
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def transport
|
|
283
|
+
@transport ||= RubyLLM::MCP::Transport.new(@transport_type, self, config: @config)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Get OAuth provider from transport if available
|
|
287
|
+
# @return [OAuthProvider, BrowserOAuthProvider, nil] OAuth provider or nil
|
|
288
|
+
def transport_oauth_provider
|
|
289
|
+
return nil unless @transport
|
|
290
|
+
|
|
291
|
+
transport_protocol = @transport.transport_protocol
|
|
292
|
+
return nil unless transport_protocol.respond_to?(:oauth_provider)
|
|
293
|
+
|
|
294
|
+
transport_protocol.oauth_provider
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def sampling_enabled?
|
|
300
|
+
MCP.config.sampling.enabled?
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json-schema"
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
6
|
module MCP
|
|
5
7
|
class Elicitation
|
|
@@ -19,20 +21,16 @@ module RubyLLM
|
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def execute
|
|
22
|
-
success = @coordinator.
|
|
23
|
-
|
|
24
|
+
success = @coordinator.client.on[:elicitation].call(self)
|
|
24
25
|
if success
|
|
25
26
|
valid = validate_response
|
|
26
27
|
if valid
|
|
27
|
-
@coordinator.elicitation_response(id: @id,
|
|
28
|
-
elicitation: {
|
|
29
|
-
action: ACCEPT_ACTION, content: @structured_response
|
|
30
|
-
})
|
|
28
|
+
@coordinator.elicitation_response(id: @id, action: ACCEPT_ACTION, content: @structured_response)
|
|
31
29
|
else
|
|
32
|
-
@coordinator.elicitation_response(id: @id,
|
|
30
|
+
@coordinator.elicitation_response(id: @id, action: CANCEL_ACTION, content: nil)
|
|
33
31
|
end
|
|
34
32
|
else
|
|
35
|
-
@coordinator.elicitation_response(id: @id,
|
|
33
|
+
@coordinator.elicitation_response(id: @id, action: REJECT_ACTION, content: nil)
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
data/lib/ruby_llm/mcp/errors.rb
CHANGED
|
@@ -71,21 +71,6 @@ module RubyLLM
|
|
|
71
71
|
class UnknownRequest < BaseError; end
|
|
72
72
|
|
|
73
73
|
class UnsupportedProtocolVersion < BaseError; end
|
|
74
|
-
|
|
75
|
-
class UnsupportedFeature < BaseError; end
|
|
76
|
-
|
|
77
|
-
class UnsupportedTransport < BaseError; end
|
|
78
|
-
|
|
79
|
-
class AdapterConfigurationError < BaseError; end
|
|
80
|
-
|
|
81
|
-
class RequestCancelled < BaseError
|
|
82
|
-
attr_reader :request_id
|
|
83
|
-
|
|
84
|
-
def initialize(message:, request_id:)
|
|
85
|
-
@request_id = request_id
|
|
86
|
-
super(message: message)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
74
|
end
|
|
90
75
|
end
|
|
91
76
|
end
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module MCP
|
|
5
5
|
class NotificationHandler
|
|
6
|
-
attr_reader :client
|
|
6
|
+
attr_reader :coordinator, :client
|
|
7
7
|
|
|
8
|
-
def initialize(
|
|
9
|
-
@
|
|
8
|
+
def initialize(coordinator)
|
|
9
|
+
@coordinator = coordinator
|
|
10
|
+
@client = coordinator.client
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def execute(notification)
|
|
@@ -24,7 +25,7 @@ module RubyLLM
|
|
|
24
25
|
when "notifications/progress"
|
|
25
26
|
process_progress_message(notification)
|
|
26
27
|
when "notifications/cancelled"
|
|
27
|
-
|
|
28
|
+
# TODO: - do nothing at the moment until we support client operations
|
|
28
29
|
else
|
|
29
30
|
process_unknown_notification(notification)
|
|
30
31
|
end
|
|
@@ -74,23 +75,6 @@ module RubyLLM
|
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
77
|
-
def process_cancelled_notification(notification)
|
|
78
|
-
request_id = notification.params["requestId"]
|
|
79
|
-
reason = notification.params["reason"] || "No reason provided"
|
|
80
|
-
|
|
81
|
-
RubyLLM::MCP.logger.info(
|
|
82
|
-
"Received cancellation for request #{request_id}: #{reason}"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
success = client.cancel_in_flight_request(request_id)
|
|
86
|
-
|
|
87
|
-
unless success
|
|
88
|
-
RubyLLM::MCP.logger.debug(
|
|
89
|
-
"Request #{request_id} was not found or already completed"
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
78
|
def process_unknown_notification(notification)
|
|
95
79
|
message = "Unknown notification type: #{notification.type} params: #{notification.params.to_h}"
|
|
96
80
|
RubyLLM::MCP.logger.error(message)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Notifications
|
|
6
|
+
class Cancelled
|
|
7
|
+
def initialize(coordinator, request_id:, reason:)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
@request_id = request_id
|
|
10
|
+
@reason = reason
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
@coordinator.request(cancelled_notification_body, add_id: false, wait_for_response: false)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def cancelled_notification_body
|
|
20
|
+
{
|
|
21
|
+
jsonrpc: "2.0",
|
|
22
|
+
method: "notifications/cancelled",
|
|
23
|
+
params: {
|
|
24
|
+
requestId: @request_id,
|
|
25
|
+
reason: @reason
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Notifications
|
|
6
|
+
class Initialize
|
|
7
|
+
def initialize(coordinator)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
@coordinator.request(notification_body, add_id: true, wait_for_response: false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def notification_body
|
|
16
|
+
{
|
|
17
|
+
jsonrpc: "2.0",
|
|
18
|
+
method: "notifications/initialized"
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Notifications
|
|
6
|
+
class RootsListChange
|
|
7
|
+
def initialize(coordinator)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
@coordinator.request(roots_list_change_notification_body, add_id: false, wait_for_response: false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def roots_list_change_notification_body
|
|
18
|
+
{
|
|
19
|
+
jsonrpc: "2.0",
|
|
20
|
+
method: "notifications/roots/list_changed"
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/ruby_llm/mcp/prompt.rb
CHANGED
|
@@ -21,10 +21,10 @@ module RubyLLM
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
attr_reader :name, :description, :arguments, :
|
|
24
|
+
attr_reader :name, :description, :arguments, :coordinator
|
|
25
25
|
|
|
26
|
-
def initialize(
|
|
27
|
-
@
|
|
26
|
+
def initialize(coordinator, prompt)
|
|
27
|
+
@coordinator = coordinator
|
|
28
28
|
@name = prompt["name"]
|
|
29
29
|
@description = prompt["description"]
|
|
30
30
|
@arguments = parse_arguments(prompt["arguments"])
|
|
@@ -51,8 +51,8 @@ module RubyLLM
|
|
|
51
51
|
alias say ask
|
|
52
52
|
|
|
53
53
|
def complete(argument, value, context: nil)
|
|
54
|
-
if @
|
|
55
|
-
result = @
|
|
54
|
+
if @coordinator.capabilities.completion?
|
|
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
|
|
@@ -80,7 +80,7 @@ module RubyLLM
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
82
|
def fetch_prompt_messages(arguments)
|
|
83
|
-
result = @
|
|
83
|
+
result = @coordinator.execute_prompt(
|
|
84
84
|
name: @name,
|
|
85
85
|
arguments: arguments
|
|
86
86
|
)
|
|
@@ -113,7 +113,7 @@ module RubyLLM
|
|
|
113
113
|
attachment = MCP::Attachment.new(content["content"], content["mime_type"])
|
|
114
114
|
MCP::Content.new(text: nil, attachments: [attachment])
|
|
115
115
|
when "resource"
|
|
116
|
-
resource = Resource.new(
|
|
116
|
+
resource = Resource.new(coordinator, content["resource"])
|
|
117
117
|
resource.to_content
|
|
118
118
|
end
|
|
119
119
|
end
|
|
@@ -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
|
data/lib/ruby_llm/mcp/railtie.rb
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
generators
|
|
8
|
-
|
|
9
|
-
require_relative "../../generators/ruby_llm/mcp/oauth/install_generator"
|
|
10
|
-
end
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
generators do
|
|
7
|
+
require_relative "../../generators/ruby_llm/mcp/install/install_generator"
|
|
8
|
+
require_relative "../../generators/ruby_llm/mcp/oauth/install_generator"
|
|
11
9
|
end
|
|
12
10
|
end
|
|
13
11
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Requests
|
|
6
|
+
class CompletionPrompt
|
|
7
|
+
def initialize(coordinator, name:, argument:, value:, context: nil)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
@name = name
|
|
10
|
+
@argument = argument
|
|
11
|
+
@value = value
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@coordinator.request(request_body)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def request_body
|
|
22
|
+
{
|
|
23
|
+
jsonrpc: "2.0",
|
|
24
|
+
id: 1,
|
|
25
|
+
method: "completion/complete",
|
|
26
|
+
params: {
|
|
27
|
+
ref: {
|
|
28
|
+
type: "ref/prompt",
|
|
29
|
+
name: @name
|
|
30
|
+
},
|
|
31
|
+
argument: {
|
|
32
|
+
name: @argument,
|
|
33
|
+
value: @value
|
|
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
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Requests
|
|
6
|
+
class CompletionResource
|
|
7
|
+
def initialize(coordinator, uri:, argument:, value:, context: nil)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
@uri = uri
|
|
10
|
+
@argument = argument
|
|
11
|
+
@value = value
|
|
12
|
+
@context = context
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@coordinator.request(request_body)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def request_body
|
|
22
|
+
{
|
|
23
|
+
jsonrpc: "2.0",
|
|
24
|
+
id: 1,
|
|
25
|
+
method: "completion/complete",
|
|
26
|
+
params: {
|
|
27
|
+
ref: {
|
|
28
|
+
type: "ref/resource",
|
|
29
|
+
uri: @uri
|
|
30
|
+
},
|
|
31
|
+
argument: {
|
|
32
|
+
name: @argument,
|
|
33
|
+
value: @value
|
|
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
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|