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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -44
  3. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +4 -21
  4. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +0 -20
  5. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +3 -0
  6. data/lib/ruby_llm/mcp/auth/browser/opener.rb +2 -0
  7. data/lib/ruby_llm/mcp/auth/browser/pages.rb +32 -100
  8. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +6 -32
  9. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +2 -0
  10. data/lib/ruby_llm/mcp/auth/memory_storage.rb +0 -18
  11. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +3 -82
  12. data/lib/ruby_llm/mcp/auth/session_manager.rb +2 -0
  13. data/lib/ruby_llm/mcp/auth/url_builder.rb +2 -0
  14. data/lib/ruby_llm/mcp/client.rb +32 -119
  15. data/lib/ruby_llm/mcp/configuration.rb +6 -74
  16. data/lib/ruby_llm/mcp/coordinator.rb +304 -0
  17. data/lib/ruby_llm/mcp/elicitation.rb +6 -8
  18. data/lib/ruby_llm/mcp/errors.rb +0 -15
  19. data/lib/ruby_llm/mcp/notification_handler.rb +5 -21
  20. data/lib/ruby_llm/mcp/notifications/cancelled.rb +32 -0
  21. data/lib/ruby_llm/mcp/notifications/initialize.rb +24 -0
  22. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
  23. data/lib/ruby_llm/mcp/prompt.rb +7 -7
  24. data/lib/ruby_llm/mcp/protocol.rb +34 -0
  25. data/lib/ruby_llm/mcp/railtie.rb +6 -8
  26. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +50 -0
  27. data/lib/ruby_llm/mcp/requests/completion_resource.rb +50 -0
  28. data/lib/ruby_llm/mcp/requests/initialization.rb +34 -0
  29. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
  30. data/lib/ruby_llm/mcp/requests/ping.rb +24 -0
  31. data/lib/ruby_llm/mcp/requests/prompt_call.rb +32 -0
  32. data/lib/ruby_llm/mcp/requests/prompt_list.rb +31 -0
  33. data/lib/ruby_llm/mcp/requests/resource_list.rb +31 -0
  34. data/lib/ruby_llm/mcp/requests/resource_read.rb +30 -0
  35. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +31 -0
  36. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
  37. data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
  38. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
  39. data/lib/ruby_llm/mcp/requests/tool_call.rb +35 -0
  40. data/lib/ruby_llm/mcp/requests/tool_list.rb +31 -0
  41. data/lib/ruby_llm/mcp/resource.rb +8 -6
  42. data/lib/ruby_llm/mcp/resource_template.rb +7 -7
  43. data/lib/ruby_llm/mcp/response_handler.rb +67 -0
  44. data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
  45. data/lib/ruby_llm/mcp/responses/error.rb +33 -0
  46. data/lib/ruby_llm/mcp/responses/ping.rb +28 -0
  47. data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
  48. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
  49. data/lib/ruby_llm/mcp/result.rb +4 -8
  50. data/lib/ruby_llm/mcp/roots.rb +4 -4
  51. data/lib/ruby_llm/mcp/sample.rb +2 -6
  52. data/lib/ruby_llm/mcp/tool.rb +9 -9
  53. data/lib/ruby_llm/mcp/transport.rb +151 -0
  54. data/lib/ruby_llm/mcp/transports/sse.rb +435 -0
  55. data/lib/ruby_llm/mcp/transports/stdio.rb +231 -0
  56. data/lib/ruby_llm/mcp/transports/streamable_http.rb +725 -0
  57. data/lib/ruby_llm/mcp/transports/support/http_client.rb +28 -0
  58. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +47 -0
  59. data/lib/ruby_llm/mcp/transports/support/timeout.rb +34 -0
  60. data/lib/ruby_llm/mcp/version.rb +1 -1
  61. data/lib/ruby_llm/mcp.rb +7 -30
  62. metadata +38 -33
  63. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +0 -179
  64. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +0 -292
  65. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +0 -33
  66. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +0 -52
  67. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +0 -52
  68. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +0 -86
  69. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +0 -92
  70. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +0 -107
  71. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +0 -57
  72. data/lib/ruby_llm/mcp/native/client.rb +0 -387
  73. data/lib/ruby_llm/mcp/native/json_rpc.rb +0 -170
  74. data/lib/ruby_llm/mcp/native/messages/helpers.rb +0 -39
  75. data/lib/ruby_llm/mcp/native/messages/notifications.rb +0 -42
  76. data/lib/ruby_llm/mcp/native/messages/requests.rb +0 -206
  77. data/lib/ruby_llm/mcp/native/messages/responses.rb +0 -106
  78. data/lib/ruby_llm/mcp/native/messages.rb +0 -36
  79. data/lib/ruby_llm/mcp/native/notification.rb +0 -16
  80. data/lib/ruby_llm/mcp/native/protocol.rb +0 -36
  81. data/lib/ruby_llm/mcp/native/response_handler.rb +0 -110
  82. data/lib/ruby_llm/mcp/native/transport.rb +0 -88
  83. data/lib/ruby_llm/mcp/native/transports/sse.rb +0 -607
  84. data/lib/ruby_llm/mcp/native/transports/stdio.rb +0 -356
  85. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +0 -926
  86. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +0 -28
  87. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +0 -49
  88. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +0 -36
  89. data/lib/ruby_llm/mcp/native.rb +0 -12
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module MCP
5
- module Auth
6
- # Helper module for preparing OAuth providers for transports
7
- # This keeps OAuth logic out of the Native module while making it reusable
8
- module TransportOauthHelper
9
- module_function
10
-
11
- # Check if OAuth configuration is present
12
- # @param config [Hash] transport configuration hash
13
- # @return [Boolean] true if OAuth config is present
14
- def oauth_config_present?(config)
15
- oauth_config = config[:oauth] || config["oauth"]
16
- return false if oauth_config.nil?
17
-
18
- # If it's an OAuth provider instance, it's present
19
- return true if oauth_config.respond_to?(:access_token)
20
-
21
- # If it's a hash, check if it's not empty
22
- !oauth_config.empty?
23
- end
24
-
25
- # Create OAuth provider from configuration
26
- # Accepts either a provider instance or a configuration hash
27
- # @param config [Hash] transport configuration hash (will be modified)
28
- # @return [OAuthProvider, BrowserOAuthProvider, nil] OAuth provider or nil
29
- def create_oauth_provider(config)
30
- oauth_config = config.delete(:oauth) || config.delete("oauth")
31
- return nil unless oauth_config
32
-
33
- # If provider key exists with an instance, use it
34
- if oauth_config.is_a?(Hash) && (oauth_config[:provider] || oauth_config["provider"])
35
- return oauth_config[:provider] || oauth_config["provider"]
36
- end
37
-
38
- # If oauth_config itself is a provider instance, use it directly
39
- if oauth_config.respond_to?(:access_token) && oauth_config.respond_to?(:start_authorization_flow)
40
- return oauth_config
41
- end
42
-
43
- # Otherwise create new provider from config hash
44
- server_url = determine_server_url(config)
45
- return nil unless server_url
46
-
47
- redirect_uri = oauth_config[:redirect_uri] || oauth_config["redirect_uri"] || "http://localhost:8080/callback"
48
- scope = oauth_config[:scope] || oauth_config["scope"]
49
- storage = oauth_config[:storage] || oauth_config["storage"]
50
- grant_type = oauth_config[:grant_type] || oauth_config["grant_type"] || :authorization_code
51
-
52
- RubyLLM::MCP::Auth::OAuthProvider.new(
53
- server_url: server_url,
54
- redirect_uri: redirect_uri,
55
- scope: scope,
56
- logger: MCP.logger,
57
- storage: storage,
58
- grant_type: grant_type
59
- )
60
- end
61
-
62
- # Determine server URL from transport config
63
- # @param config [Hash] transport configuration hash
64
- # @return [String, nil] server URL or nil
65
- def determine_server_url(config)
66
- config[:url] || config["url"]
67
- end
68
-
69
- # Prepare HTTP transport configuration with OAuth provider
70
- # @param config [Hash] transport configuration hash (will be modified)
71
- # @param oauth_provider [OAuthProvider, nil] OAuth provider instance
72
- # @return [Hash] prepared configuration
73
- def prepare_http_transport_config(config, oauth_provider)
74
- options = {
75
- version: config.delete(:version) || config.delete("version"),
76
- headers: config.delete(:headers) || config.delete("headers"),
77
- oauth_provider: oauth_provider,
78
- reconnection: config.delete(:reconnection) || config.delete("reconnection"),
79
- reconnection_options: config.delete(:reconnection_options) || config.delete("reconnection_options"),
80
- rate_limit: config.delete(:rate_limit) || config.delete("rate_limit"),
81
- session_id: config.delete(:session_id) || config.delete("session_id")
82
- }.compact
83
-
84
- config[:options] = options
85
- config
86
- end
87
-
88
- # Prepare stdio transport configuration
89
- # @param config [Hash] transport configuration hash (will be modified)
90
- # @return [Hash] prepared configuration
91
- def prepare_stdio_transport_config(config)
92
- # Remove OAuth config from stdio transport (not supported)
93
- config.delete(:oauth)
94
- config.delete("oauth")
95
-
96
- options = {
97
- args: config.delete(:args) || config.delete("args"),
98
- env: config.delete(:env) || config.delete("env")
99
- }.compact
100
-
101
- config[:options] = options unless options.empty?
102
- config
103
- end
104
- end
105
- end
106
- end
107
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module MCP
5
- module Native
6
- # Wraps server-initiated requests to support cancellation
7
- # Executes the request in a separate thread that can be terminated on cancellation
8
- class CancellableOperation
9
- attr_reader :request_id, :thread
10
-
11
- def initialize(request_id)
12
- @request_id = request_id
13
- @cancelled = false
14
- @mutex = Mutex.new
15
- @thread = nil
16
- @result = nil
17
- @error = nil
18
- end
19
-
20
- def cancelled?
21
- @mutex.synchronize { @cancelled }
22
- end
23
-
24
- def cancel
25
- @mutex.synchronize { @cancelled = true }
26
- if @thread&.alive?
27
- @thread.raise(Errors::RequestCancelled.new(
28
- message: "Request #{@request_id} was cancelled",
29
- request_id: @request_id
30
- ))
31
- end
32
- end
33
-
34
- # Execute a block in a separate thread
35
- # This allows the thread to be terminated if cancellation is requested
36
- # Returns the result of the block or re-raises any error that occurred
37
- def execute(&)
38
- @thread = Thread.new do
39
- Thread.current.abort_on_exception = false
40
- begin
41
- @result = yield
42
- rescue Errors::RequestCancelled, StandardError => e
43
- @error = e
44
- end
45
- end
46
-
47
- @thread.join
48
- raise @error if @error && !@error.is_a?(Errors::RequestCancelled)
49
-
50
- @result
51
- ensure
52
- @thread = nil
53
- end
54
- end
55
- end
56
- end
57
- end
@@ -1,387 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module MCP
5
- module Native
6
- # Native MCP protocol client implementation
7
- # This is the core protocol implementation that handles all MCP operations
8
- # It's self-contained and could potentially be extracted as a separate gem
9
- class Client
10
- attr_reader :name, :transport_type, :config, :capabilities, :protocol_version, :elicitation_callback,
11
- :sampling_callback
12
-
13
- def initialize( # rubocop:disable Metrics/ParameterLists
14
- name:,
15
- transport_type:,
16
- transport_config: {},
17
- human_in_the_loop_callback: nil,
18
- roots_callback: nil,
19
- logging_enabled: false,
20
- logging_level: nil,
21
- elicitation_enabled: false,
22
- elicitation_callback: nil,
23
- progress_tracking_enabled: false,
24
- sampling_callback: nil,
25
- notification_callback: nil,
26
- protocol_version: nil,
27
- request_timeout: nil
28
- )
29
- @name = name
30
- @transport_type = transport_type
31
- @config = transport_config.merge(request_timeout: request_timeout || MCP.config.request_timeout)
32
- @protocol_version = protocol_version || MCP.config.protocol_version || Native::Protocol.default_negotiated_version
33
-
34
- # Callbacks
35
- @human_in_the_loop_callback = human_in_the_loop_callback
36
- @roots_callback = roots_callback
37
- @logging_enabled = logging_enabled
38
- @logging_level = logging_level
39
- @elicitation_enabled = elicitation_enabled
40
- @elicitation_callback = elicitation_callback
41
- @progress_tracking_enabled = progress_tracking_enabled
42
- @sampling_callback = sampling_callback
43
- @notification_callback = notification_callback
44
-
45
- @transport = nil
46
- @capabilities = nil
47
-
48
- # Track in-flight server-initiated requests for cancellation
49
- @in_flight_requests = {}
50
- @in_flight_mutex = Mutex.new
51
- end
52
-
53
- def request(body, **options)
54
- transport.request(body, **options)
55
- rescue RubyLLM::MCP::Errors::TimeoutError => e
56
- if transport&.alive? && !e.request_id.nil?
57
- cancelled_notification(reason: "Request timed out", request_id: e.request_id)
58
- end
59
- raise e
60
- end
61
-
62
- def process_result(result)
63
- if result.notification?
64
- process_notification(result)
65
- return nil
66
- end
67
-
68
- if result.request?
69
- process_request(result) if alive?
70
- return nil
71
- end
72
-
73
- if result.response?
74
- return result
75
- end
76
-
77
- nil
78
- end
79
-
80
- def start
81
- return unless capabilities.nil?
82
-
83
- transport.start
84
-
85
- initialize_response = initialize_request
86
- initialize_response.raise_error! if initialize_response.error?
87
-
88
- # Extract and store the negotiated protocol version
89
- negotiated_version = initialize_response.value["protocolVersion"]
90
-
91
- if negotiated_version && !Native::Protocol.supported_version?(negotiated_version)
92
- raise Errors::UnsupportedProtocolVersion.new(
93
- message: <<~MESSAGE
94
- Unsupported protocol version, and could not negotiate a supported version: #{negotiated_version}.
95
- Supported versions: #{Native::Protocol.supported_versions.join(', ')}
96
- MESSAGE
97
- )
98
- end
99
-
100
- @protocol_version = negotiated_version if negotiated_version
101
-
102
- # Set the protocol version on the transport for subsequent requests
103
- if @transport.respond_to?(:set_protocol_version)
104
- @transport.set_protocol_version(@protocol_version)
105
- end
106
-
107
- @capabilities = RubyLLM::MCP::ServerCapabilities.new(initialize_response.value["capabilities"])
108
- initialize_notification
109
-
110
- if @logging_enabled && @logging_level
111
- set_logging(level: @logging_level)
112
- end
113
- end
114
-
115
- def stop
116
- @transport&.close
117
- @capabilities = nil
118
- @transport = nil
119
- @protocol_version = Native::Protocol.default_negotiated_version
120
- end
121
-
122
- def restart!
123
- @initialize_response = nil
124
- stop
125
- start
126
- end
127
-
128
- def alive?
129
- !!@transport&.alive?
130
- end
131
-
132
- def ping
133
- body = Native::Messages::Requests.ping(tracking_progress: tracking_progress?)
134
- if alive?
135
- result = request(body)
136
- else
137
- transport.start
138
-
139
- result = request(body)
140
- @transport = nil
141
- end
142
-
143
- result.value == {}
144
- rescue RubyLLM::MCP::Errors::TimeoutError, RubyLLM::MCP::Errors::TransportError
145
- false
146
- end
147
-
148
- def process_notification(result)
149
- notification = result.notification
150
- @notification_callback&.call(notification)
151
- end
152
-
153
- def process_request(result)
154
- Native::ResponseHandler.new(self).execute(result)
155
- end
156
-
157
- def initialize_request
158
- body = Native::Messages::Requests.initialize(
159
- protocol_version: protocol_version,
160
- capabilities: client_capabilities
161
- )
162
- request(body)
163
- end
164
-
165
- def tool_list(cursor: nil)
166
- body = Native::Messages::Requests.tool_list(cursor: cursor, tracking_progress: tracking_progress?)
167
- result = request(body)
168
- result.raise_error! if result.error?
169
-
170
- if result.next_cursor?
171
- result.value["tools"] + tool_list(cursor: result.next_cursor)
172
- else
173
- result.value["tools"]
174
- end
175
- end
176
-
177
- def execute_tool(name:, parameters:)
178
- if @human_in_the_loop_callback && !@human_in_the_loop_callback.call(name, parameters)
179
- result = Result.new(
180
- {
181
- "result" => {
182
- "isError" => true,
183
- "content" => [{ "type" => "text", "text" => "Tool call was cancelled by the client" }]
184
- }
185
- }
186
- )
187
- return result
188
- end
189
-
190
- body = Native::Messages::Requests.tool_call(name: name, parameters: parameters,
191
- tracking_progress: tracking_progress?)
192
- request(body)
193
- end
194
-
195
- def resource_list(cursor: nil)
196
- body = Native::Messages::Requests.resource_list(cursor: cursor, tracking_progress: tracking_progress?)
197
- result = request(body)
198
- result.raise_error! if result.error?
199
-
200
- if result.next_cursor?
201
- result.value["resources"] + resource_list(cursor: result.next_cursor)
202
- else
203
- result.value["resources"]
204
- end
205
- end
206
-
207
- def resource_read(uri:)
208
- body = Native::Messages::Requests.resource_read(uri: uri, tracking_progress: tracking_progress?)
209
- request(body)
210
- end
211
-
212
- def resource_template_list(cursor: nil)
213
- body = Native::Messages::Requests.resource_template_list(cursor: cursor,
214
- tracking_progress: tracking_progress?)
215
- result = request(body)
216
- result.raise_error! if result.error?
217
-
218
- if result.next_cursor?
219
- result.value["resourceTemplates"] + resource_template_list(cursor: result.next_cursor)
220
- else
221
- result.value["resourceTemplates"]
222
- end
223
- end
224
-
225
- def resources_subscribe(uri:)
226
- body = Native::Messages::Requests.resources_subscribe(uri: uri, tracking_progress: tracking_progress?)
227
- request(body, wait_for_response: false)
228
- end
229
-
230
- def prompt_list(cursor: nil)
231
- body = Native::Messages::Requests.prompt_list(cursor: cursor, tracking_progress: tracking_progress?)
232
- result = request(body)
233
- result.raise_error! if result.error?
234
-
235
- if result.next_cursor?
236
- result.value["prompts"] + prompt_list(cursor: result.next_cursor)
237
- else
238
- result.value["prompts"]
239
- end
240
- end
241
-
242
- def execute_prompt(name:, arguments:)
243
- body = Native::Messages::Requests.prompt_call(name: name, arguments: arguments,
244
- tracking_progress: tracking_progress?)
245
- request(body)
246
- end
247
-
248
- def completion_resource(uri:, argument:, value:, context: nil)
249
- body = Native::Messages::Requests.completion_resource(uri: uri, argument: argument, value: value,
250
- context: context, tracking_progress: tracking_progress?)
251
- request(body)
252
- end
253
-
254
- def completion_prompt(name:, argument:, value:, context: nil)
255
- body = Native::Messages::Requests.completion_prompt(name: name, argument: argument, value: value,
256
- context: context, tracking_progress: tracking_progress?)
257
- request(body)
258
- end
259
-
260
- def set_logging(level:)
261
- body = Native::Messages::Requests.logging_set_level(level: level, tracking_progress: tracking_progress?)
262
- request(body)
263
- end
264
-
265
- def set_progress_tracking(enabled:)
266
- @progress_tracking_enabled = enabled
267
- end
268
-
269
- ## Notifications
270
- #
271
- def initialize_notification
272
- body = Native::Messages::Notifications.initialized
273
- request(body, wait_for_response: false)
274
- end
275
-
276
- def cancelled_notification(reason:, request_id:)
277
- body = Native::Messages::Notifications.cancelled(request_id: request_id, reason: reason)
278
- request(body, wait_for_response: false)
279
- end
280
-
281
- def roots_list_change_notification
282
- body = Native::Messages::Notifications.roots_list_changed
283
- request(body, wait_for_response: false)
284
- end
285
-
286
- ## Responses
287
- #
288
- def ping_response(id:)
289
- body = Native::Messages::Responses.ping(id: id)
290
- request(body, wait_for_response: false)
291
- end
292
-
293
- def roots_list_response(id:)
294
- body = Native::Messages::Responses.roots_list(id: id, roots_paths: roots_paths)
295
- request(body, wait_for_response: false)
296
- end
297
-
298
- def sampling_create_message_response(id:, model:, message:, **_options)
299
- body = Native::Messages::Responses.sampling_create_message(id: id, model: model, message: message)
300
- request(body, wait_for_response: false)
301
- end
302
-
303
- def error_response(id:, message:, code: Native::JsonRpc::ErrorCodes::SERVER_ERROR, data: nil)
304
- body = Native::Messages::Responses.error(id: id, message: message, code: code, data: data)
305
- request(body, wait_for_response: false)
306
- end
307
-
308
- def elicitation_response(id:, elicitation:)
309
- body = Native::Messages::Responses.elicitation(id: id, action: elicitation[:action],
310
- content: elicitation[:content])
311
- request(body, wait_for_response: false)
312
- end
313
-
314
- def client_capabilities
315
- capabilities_hash = {}
316
-
317
- if @roots_callback&.call&.any?
318
- capabilities_hash[:roots] = {
319
- listChanged: true
320
- }
321
- end
322
-
323
- if MCP.config.sampling.enabled?
324
- capabilities_hash[:sampling] = {}
325
- end
326
-
327
- if @elicitation_enabled
328
- capabilities_hash[:elicitation] = {}
329
- end
330
-
331
- capabilities_hash
332
- end
333
-
334
- def roots_paths
335
- @roots_callback&.call || []
336
- end
337
-
338
- def tracking_progress?
339
- @progress_tracking_enabled
340
- end
341
-
342
- def sampling_callback_enabled?
343
- !@sampling_callback.nil?
344
- end
345
-
346
- def transport
347
- @transport ||= Native::Transport.new(@transport_type, self, config: @config)
348
- end
349
-
350
- # Register a server-initiated request that can be cancelled
351
- # @param request_id [String] The ID of the request
352
- # @param cancellable_operation [CancellableOperation, nil] The operation that can be cancelled
353
- def register_in_flight_request(request_id, cancellable_operation = nil)
354
- @in_flight_mutex.synchronize do
355
- @in_flight_requests[request_id.to_s] = cancellable_operation
356
- end
357
- end
358
-
359
- # Unregister a completed or cancelled request
360
- # @param request_id [String] The ID of the request
361
- def unregister_in_flight_request(request_id)
362
- @in_flight_mutex.synchronize do
363
- @in_flight_requests.delete(request_id.to_s)
364
- end
365
- end
366
-
367
- # Cancel an in-flight server-initiated request
368
- # @param request_id [String] The ID of the request to cancel
369
- # @return [Boolean] true if the request was found and cancelled, false otherwise
370
- def cancel_in_flight_request(request_id) # rubocop:disable Naming/PredicateMethod
371
- operation = nil
372
- @in_flight_mutex.synchronize do
373
- operation = @in_flight_requests[request_id.to_s]
374
- end
375
-
376
- if operation.respond_to?(:cancel)
377
- operation.cancel
378
- true
379
- else
380
- RubyLLM::MCP.logger.warn("Request #{request_id} cannot be cancelled or was already completed")
381
- false
382
- end
383
- end
384
- end
385
- end
386
- end
387
- end