ruby_llm_swarm-mcp 0.8.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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +277 -0
  4. data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
  5. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
  6. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  19. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  20. data/lib/ruby_llm/chat.rb +34 -0
  21. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
  25. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
  26. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
  27. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
  28. data/lib/ruby_llm/mcp/attachment.rb +18 -0
  29. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  30. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  31. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  32. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  33. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  34. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
  35. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  36. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  37. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  38. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  39. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  40. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  41. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  42. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  43. data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
  44. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
  45. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  46. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  47. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  48. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  49. data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
  50. data/lib/ruby_llm/mcp/auth.rb +359 -0
  51. data/lib/ruby_llm/mcp/client.rb +401 -0
  52. data/lib/ruby_llm/mcp/completion.rb +16 -0
  53. data/lib/ruby_llm/mcp/configuration.rb +310 -0
  54. data/lib/ruby_llm/mcp/content.rb +28 -0
  55. data/lib/ruby_llm/mcp/elicitation.rb +48 -0
  56. data/lib/ruby_llm/mcp/error.rb +34 -0
  57. data/lib/ruby_llm/mcp/errors.rb +91 -0
  58. data/lib/ruby_llm/mcp/logging.rb +16 -0
  59. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
  60. data/lib/ruby_llm/mcp/native/client.rb +387 -0
  61. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  62. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  63. data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
  64. data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
  65. data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
  66. data/lib/ruby_llm/mcp/native/messages.rb +36 -0
  67. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  68. data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
  69. data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
  70. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  71. data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
  72. data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
  73. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
  74. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  75. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
  76. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  77. data/lib/ruby_llm/mcp/native.rb +12 -0
  78. data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
  79. data/lib/ruby_llm/mcp/progress.rb +35 -0
  80. data/lib/ruby_llm/mcp/prompt.rb +132 -0
  81. data/lib/ruby_llm/mcp/railtie.rb +14 -0
  82. data/lib/ruby_llm/mcp/resource.rb +112 -0
  83. data/lib/ruby_llm/mcp/resource_template.rb +85 -0
  84. data/lib/ruby_llm/mcp/result.rb +108 -0
  85. data/lib/ruby_llm/mcp/roots.rb +45 -0
  86. data/lib/ruby_llm/mcp/sample.rb +152 -0
  87. data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
  88. data/lib/ruby_llm/mcp/tool.rb +228 -0
  89. data/lib/ruby_llm/mcp/version.rb +7 -0
  90. data/lib/ruby_llm/mcp.rb +125 -0
  91. data/lib/tasks/release.rake +23 -0
  92. metadata +184 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Content < RubyLLM::Content
6
+ attr_reader :text, :attachments, :content
7
+
8
+ def initialize(text: nil, attachments: nil) # rubocop:disable Lint/MissingSuper
9
+ @text = text
10
+ @attachments = []
11
+
12
+ # Handle MCP::Attachment objects directly without processing
13
+ if attachments.is_a?(Array) && attachments.all? { |a| a.is_a?(MCP::Attachment) }
14
+ @attachments = attachments
15
+ elsif attachments
16
+ # Let parent class process other types of attachments
17
+ process_attachments(attachments)
18
+ end
19
+ end
20
+
21
+ # This is a workaround to allow the content object to be passed as the tool call
22
+ # to return audio or image attachments.
23
+ def to_s
24
+ text.to_s
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Elicitation
6
+ ACCEPT_ACTION = "accept"
7
+ CANCEL_ACTION = "cancel"
8
+ REJECT_ACTION = "reject"
9
+
10
+ attr_writer :structured_response
11
+
12
+ def initialize(coordinator, result)
13
+ @coordinator = coordinator
14
+ @result = result
15
+ @id = result.id
16
+
17
+ @message = @result.params["message"]
18
+ @requested_schema = @result.params["requestedSchema"]
19
+ end
20
+
21
+ def execute
22
+ success = @coordinator.elicitation_callback&.call(self)
23
+
24
+ if success
25
+ valid = validate_response
26
+ if valid
27
+ @coordinator.elicitation_response(id: @id,
28
+ elicitation: {
29
+ action: ACCEPT_ACTION, content: @structured_response
30
+ })
31
+ else
32
+ @coordinator.elicitation_response(id: @id, elicitation: { action: CANCEL_ACTION, content: nil })
33
+ end
34
+ else
35
+ @coordinator.elicitation_response(id: @id, elicitation: { action: REJECT_ACTION, content: nil })
36
+ end
37
+ end
38
+
39
+ def message
40
+ @result.params["message"]
41
+ end
42
+
43
+ def validate_response
44
+ JSON::Validator.validate(@requested_schema, @structured_response)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Error
6
+ def initialize(error_data)
7
+ @code = error_data["code"]
8
+ @message = error_data["message"]
9
+ @data = error_data["data"]
10
+ end
11
+
12
+ def type
13
+ case @code
14
+ when -32_700
15
+ :parse_error
16
+ when -32_600
17
+ :invalid_request
18
+ when -32_601
19
+ :method_not_found
20
+ when -32_602
21
+ :invalid_params
22
+ when -32_603
23
+ :internal_error
24
+ else
25
+ :custom_error
26
+ end
27
+ end
28
+
29
+ def to_s
30
+ "Error: code: #{@code} (#{type}), message: #{@message}, data: #{@data}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Errors
6
+ class BaseError < StandardError
7
+ attr_reader :message
8
+
9
+ def initialize(message:)
10
+ @message = message
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ module Capabilities
16
+ class CompletionNotAvailable < BaseError; end
17
+ class ResourceSubscribeNotAvailable < BaseError; end
18
+ end
19
+
20
+ class InvalidFormatError < BaseError; end
21
+
22
+ class InvalidProtocolVersionError < BaseError; end
23
+
24
+ class InvalidTransportType < BaseError; end
25
+
26
+ class ProgressHandlerNotAvailable < BaseError; end
27
+
28
+ class PromptArgumentError < BaseError; end
29
+
30
+ class ResponseError < BaseError
31
+ attr_reader :error
32
+
33
+ def initialize(message:, error:)
34
+ @error = error
35
+ super(message: message)
36
+ end
37
+ end
38
+
39
+ class AuthenticationRequiredError < BaseError
40
+ attr_reader :code
41
+
42
+ def initialize(message: "Authentication required", code: 401)
43
+ @code = code
44
+ super(message: message)
45
+ end
46
+ end
47
+
48
+ class ConfigurationError < BaseError; end
49
+
50
+ class SessionExpiredError < BaseError; end
51
+
52
+ class TimeoutError < BaseError
53
+ attr_reader :request_id
54
+
55
+ def initialize(message:, request_id: nil)
56
+ @request_id = request_id
57
+ super(message: message)
58
+ end
59
+ end
60
+
61
+ class TransportError < BaseError
62
+ attr_reader :code, :error
63
+
64
+ def initialize(message:, code: nil, error: nil)
65
+ @code = code
66
+ @error = error
67
+ super(message: message)
68
+ end
69
+ end
70
+
71
+ class UnknownRequest < BaseError; end
72
+
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
+ end
90
+ end
91
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Logging
6
+ DEBUG = "debug"
7
+ INFO = "info"
8
+ NOTICE = "notice"
9
+ WARNING = "warning"
10
+ ERROR = "error"
11
+ CRITICAL = "critical"
12
+ ALERT = "alert"
13
+ EMERGENCY = "emergency"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
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
@@ -0,0 +1,387 @@
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