ruby_llm-mcp 0.7.1 → 1.0.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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -162
  3. data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
  4. data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +21 -4
  5. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  6. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  19. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
  20. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
  21. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
  25. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
  26. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  27. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +36 -0
  28. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  29. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  30. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  31. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +427 -0
  32. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  33. data/lib/ruby_llm/mcp/auth/discoverer.rb +255 -0
  34. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +122 -0
  35. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +67 -0
  36. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  37. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  38. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  39. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  40. data/lib/ruby_llm/mcp/auth/memory_storage.rb +91 -0
  41. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +341 -0
  42. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  43. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  44. data/lib/ruby_llm/mcp/auth/token_manager.rb +307 -0
  45. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  46. data/lib/ruby_llm/mcp/auth/url_builder.rb +135 -0
  47. data/lib/ruby_llm/mcp/auth.rb +371 -0
  48. data/lib/ruby_llm/mcp/client.rb +312 -35
  49. data/lib/ruby_llm/mcp/configuration.rb +199 -24
  50. data/lib/ruby_llm/mcp/elicitation.rb +261 -14
  51. data/lib/ruby_llm/mcp/errors.rb +29 -0
  52. data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
  53. data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
  54. data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
  55. data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
  56. data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
  57. data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
  58. data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
  59. data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
  60. data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
  61. data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
  62. data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
  63. data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
  64. data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
  65. data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
  66. data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
  67. data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
  68. data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
  69. data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
  70. data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
  71. data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
  72. data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
  73. data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
  74. data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
  75. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
  76. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
  77. data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
  78. data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
  79. data/lib/ruby_llm/mcp/handlers.rb +14 -0
  80. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
  81. data/lib/ruby_llm/mcp/native/client.rb +551 -0
  82. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  83. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  84. data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
  85. data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
  86. data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
  87. data/lib/ruby_llm/mcp/native/messages.rb +43 -0
  88. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  89. data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
  90. data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
  91. data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
  92. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  93. data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
  94. data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
  95. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
  96. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  97. data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
  98. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  99. data/lib/ruby_llm/mcp/native.rb +12 -0
  100. data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
  101. data/lib/ruby_llm/mcp/prompt.rb +7 -7
  102. data/lib/ruby_llm/mcp/railtie.rb +7 -13
  103. data/lib/ruby_llm/mcp/resource.rb +17 -8
  104. data/lib/ruby_llm/mcp/resource_template.rb +8 -7
  105. data/lib/ruby_llm/mcp/result.rb +8 -4
  106. data/lib/ruby_llm/mcp/roots.rb +4 -4
  107. data/lib/ruby_llm/mcp/sample.rb +83 -13
  108. data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
  109. data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
  110. data/lib/ruby_llm/mcp/task.rb +65 -0
  111. data/lib/ruby_llm/mcp/tool.rb +33 -27
  112. data/lib/ruby_llm/mcp/version.rb +1 -1
  113. data/lib/ruby_llm/mcp.rb +37 -7
  114. data/lib/tasks/smoke.rake +66 -0
  115. metadata +115 -39
  116. data/lib/generators/ruby_llm/mcp/templates/mcps.yml +0 -9
  117. data/lib/ruby_llm/mcp/coordinator.rb +0 -293
  118. data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
  119. data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
  120. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
  121. data/lib/ruby_llm/mcp/protocol.rb +0 -34
  122. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
  123. data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
  124. data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
  125. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
  126. data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
  127. data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
  128. data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
  129. data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
  130. data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
  131. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
  132. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
  133. data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
  134. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
  135. data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
  136. data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
  137. data/lib/ruby_llm/mcp/response_handler.rb +0 -67
  138. data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
  139. data/lib/ruby_llm/mcp/responses/error.rb +0 -33
  140. data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
  141. data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
  142. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
  143. data/lib/ruby_llm/mcp/transport.rb +0 -58
  144. data/lib/ruby_llm/mcp/transports/sse.rb +0 -341
  145. data/lib/ruby_llm/mcp/transports/stdio.rb +0 -230
  146. data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -723
  147. data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
  148. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
  149. data/lib/ruby_llm/mcp/transports/support/timeout.rb +0 -34
@@ -7,17 +7,42 @@ module RubyLLM
7
7
  class Client
8
8
  extend Forwardable
9
9
 
10
- attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on, :roots
10
+ attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on, :roots, :adapter,
11
+ :on_logging_level
11
12
  attr_accessor :linked_resources
12
13
 
13
- def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
14
+ def initialize(name:, transport_type:, sdk: nil, adapter: nil, start: true, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
15
+ request_timeout: MCP.config.request_timeout, config: {})
14
16
  @name = name
15
- @with_prefix = config.delete(:with_prefix) || false
16
- @config = config.merge(request_timeout: request_timeout)
17
17
  @transport_type = transport_type.to_sym
18
+ @adapter_type = adapter || sdk || MCP.config.default_adapter
19
+
20
+ # Validate early
21
+ MCP.config.adapter_config.validate!(
22
+ adapter: @adapter_type,
23
+ transport: @transport_type
24
+ )
25
+
26
+ @with_prefix = config.delete(:with_prefix) || false
27
+ @resolved_protocol_version = config[:protocol_version] ||
28
+ config["protocol_version"] ||
29
+ MCP.config.protocol_version
30
+ @resolved_extensions = Extensions::Registry.merge(
31
+ MCP.config.extensions.to_h,
32
+ config[:extensions] || config["extensions"]
33
+ )
34
+
35
+ @config = config.merge(
36
+ request_timeout: request_timeout,
37
+ protocol_version: @resolved_protocol_version,
38
+ extensions: @resolved_extensions
39
+ )
18
40
  @request_timeout = request_timeout
19
41
 
20
- @coordinator = setup_coordinator
42
+ # Store OAuth config for later use
43
+ @oauth_config = config[:oauth] || config["oauth"]
44
+ @oauth_provider = nil
45
+ @oauth_storage = nil
21
46
 
22
47
  @on = {}
23
48
  @tools = {}
@@ -29,31 +54,73 @@ module RubyLLM
29
54
 
30
55
  @linked_resources = []
31
56
 
32
- setup_roots
33
- setup_sampling
57
+ # Build adapter based on configuration
58
+ @adapter = build_adapter
59
+
60
+ setup_roots if @adapter.supports?(:roots)
61
+ setup_sampling if @adapter.supports?(:sampling)
62
+ setup_event_handlers
63
+ sync_elicitation_handler_state
34
64
 
35
- @coordinator.start_transport if start
65
+ @adapter.start if start
36
66
  end
37
67
 
38
- def_delegators :@coordinator, :alive?, :capabilities, :ping, :client_capabilities
68
+ def_delegators :@adapter, :alive?, :capabilities, :ping, :client_capabilities,
69
+ :register_in_flight_request, :unregister_in_flight_request,
70
+ :cancel_in_flight_request
39
71
 
40
72
  def start
41
- @coordinator.start_transport
73
+ @adapter.start
42
74
  end
43
75
 
44
76
  def stop
45
- @coordinator.stop_transport
77
+ @adapter.stop
46
78
  end
47
79
 
48
80
  def restart!
49
- @coordinator.restart_transport
81
+ @adapter.restart!
82
+ end
83
+
84
+ # Get or create OAuth provider for this client
85
+ # @param type [Symbol] OAuth provider type (:standard or :browser, defaults to :standard)
86
+ # @param options [Hash] additional options passed to provider
87
+ # @return [OAuthProvider, BrowserOAuthProvider] OAuth provider instance
88
+ def oauth(type: :standard, **options)
89
+ # Return existing provider if already created
90
+ return @oauth_provider if @oauth_provider
91
+
92
+ # Get provider from transport if it already exists
93
+ transport_oauth = transport_oauth_provider
94
+ return transport_oauth if transport_oauth
95
+
96
+ # Create new provider lazily
97
+ server_url = @config[:url] || @config["url"]
98
+ unless server_url
99
+ raise Errors::ConfigurationError.new(
100
+ message: "Cannot create OAuth provider without server URL in config"
101
+ )
102
+ end
103
+
104
+ oauth_options = {
105
+ server_url: server_url,
106
+ scope: @oauth_config&.dig(:scope) || @oauth_config&.dig("scope"),
107
+ storage: oauth_storage,
108
+ **options
109
+ }
110
+
111
+ @oauth_provider = Auth.create_oauth(
112
+ server_url,
113
+ type: type,
114
+ **oauth_options
115
+ )
50
116
  end
51
117
 
52
118
  def tools(refresh: false)
119
+ require_feature!(:tools)
53
120
  return [] unless capabilities.tools_list?
54
121
 
55
122
  fetch(:tools, refresh) do
56
- tools = @coordinator.tool_list
123
+ tools = @adapter.tool_list
57
124
  build_map(tools, MCP::Tool, with_prefix: @with_prefix)
58
125
  end
59
126
 
@@ -71,10 +138,11 @@ module RubyLLM
71
138
  end
72
139
 
73
140
  def resources(refresh: false)
141
+ require_feature!(:resources)
74
142
  return [] unless capabilities.resources_list?
75
143
 
76
144
  fetch(:resources, refresh) do
77
- resources = @coordinator.resource_list
145
+ resources = @adapter.resource_list
78
146
  resources = build_map(resources, MCP::Resource)
79
147
  include_linked_resources(resources)
80
148
  end
@@ -88,15 +156,30 @@ module RubyLLM
88
156
  @resources[name]
89
157
  end
90
158
 
159
+ def unsubscribe_from_resource(resource_or_uri) # rubocop:disable Naming/PredicateMethod
160
+ require_feature!(:subscriptions)
161
+
162
+ uri = if resource_or_uri.respond_to?(:uri)
163
+ resource_or_uri.uri
164
+ else
165
+ resource_or_uri.to_s
166
+ end
167
+ @adapter.resources_unsubscribe(uri: uri)
168
+ resource = @resources.values.find { |existing| existing.uri == uri }
169
+ resource&.instance_variable_set(:@subscribed, false)
170
+ true
171
+ end
172
+
91
173
  def reset_resources!
92
174
  @resources = {}
93
175
  end
94
176
 
95
177
  def resource_templates(refresh: false)
178
+ require_feature!(:resource_templates)
96
179
  return [] unless capabilities.resources_list?
97
180
 
98
181
  fetch(:resource_templates, refresh) do
99
- resource_templates = @coordinator.resource_template_list
182
+ resource_templates = @adapter.resource_template_list
100
183
  build_map(resource_templates, MCP::ResourceTemplate)
101
184
  end
102
185
 
@@ -114,10 +197,11 @@ module RubyLLM
114
197
  end
115
198
 
116
199
  def prompts(refresh: false)
200
+ require_feature!(:prompts)
117
201
  return [] unless capabilities.prompt_list?
118
202
 
119
203
  fetch(:prompts, refresh) do
120
- prompts = @coordinator.prompt_list
204
+ prompts = @adapter.prompt_list
121
205
  build_map(prompts, MCP::Prompt)
122
206
  end
123
207
 
@@ -134,11 +218,46 @@ module RubyLLM
134
218
  @prompts = {}
135
219
  end
136
220
 
221
+ def tasks_list
222
+ require_feature!(:tasks)
223
+ return [] unless capabilities.tasks? && capabilities.tasks_list?
224
+
225
+ @adapter.tasks_list.map { |task| MCP::Task.new(@adapter, task) }
226
+ end
227
+
228
+ def task_get(task_id)
229
+ require_feature!(:tasks)
230
+ result = @adapter.task_get(task_id: task_id)
231
+ MCP::Task.new(@adapter, result.value)
232
+ end
233
+
234
+ def task_result(task_id)
235
+ require_feature!(:tasks)
236
+ result = @adapter.task_result(task_id: task_id)
237
+ result.value
238
+ end
239
+
240
+ def task_cancel(task_id)
241
+ require_feature!(:tasks)
242
+ unless capabilities.tasks_cancel?
243
+ message = "Task cancellation is not available for this MCP server"
244
+ raise Errors::Capabilities::TaskCancelNotAvailable.new(message: message)
245
+ end
246
+
247
+ result = @adapter.task_cancel(task_id: task_id)
248
+ MCP::Task.new(@adapter, result.value)
249
+ end
250
+
137
251
  def tracking_progress?
138
252
  @on.key?(:progress) && !@on[:progress].nil?
139
253
  end
140
254
 
141
255
  def on_progress(&block)
256
+ require_feature!(:progress_tracking)
257
+ if alive?
258
+ @adapter.set_progress_tracking(enabled: true)
259
+ end
260
+
142
261
  @on[:progress] = block
143
262
  self
144
263
  end
@@ -147,8 +266,23 @@ module RubyLLM
147
266
  @on.key?(:human_in_the_loop) && !@on[:human_in_the_loop].nil?
148
267
  end
149
268
 
150
- def on_human_in_the_loop(&block)
151
- @on[:human_in_the_loop] = block
269
+ def on_human_in_the_loop(handler_class = nil, **options)
270
+ require_feature!(:human_in_the_loop)
271
+
272
+ if block_given?
273
+ raise ArgumentError, "Block-based human-in-the-loop callbacks are no longer supported. Use a handler class."
274
+ end
275
+
276
+ if handler_class
277
+ # Validate handler class
278
+ validate_handler_class!(handler_class, :execute)
279
+
280
+ @on[:human_in_the_loop] = { class: handler_class, options: options }
281
+ else
282
+ # Clear handler when called without arguments
283
+ @on[:human_in_the_loop] = nil
284
+ end
285
+
152
286
  self
153
287
  end
154
288
 
@@ -161,9 +295,10 @@ module RubyLLM
161
295
  end
162
296
 
163
297
  def on_logging(level: Logging::WARNING, &block)
298
+ require_feature!(:logging)
164
299
  @on_logging_level = level
165
300
  if alive?
166
- @coordinator.set_logging(level: level)
301
+ @adapter.set_logging(level: level)
167
302
  end
168
303
 
169
304
  @on[:logging] = block
@@ -174,17 +309,69 @@ module RubyLLM
174
309
  @on.key?(:sampling) && !@on[:sampling].nil?
175
310
  end
176
311
 
177
- def on_sampling(&block)
178
- @on[:sampling] = block
312
+ def on_sampling(handler_class = nil, **options, &block)
313
+ require_feature!(:sampling)
314
+
315
+ if handler_class
316
+ # Validate handler class
317
+ validate_handler_class!(handler_class, :execute)
318
+
319
+ # Handler class provided
320
+ @on[:sampling] = if options.any?
321
+ lambda do |sample|
322
+ handler_class.new(sample: sample, coordinator: @adapter.native_client, **options).call
323
+ end
324
+ else
325
+ handler_class
326
+ end
327
+ elsif block_given?
328
+ # Block provided (backward compatible)
329
+ @on[:sampling] = block
330
+ else
331
+ # Clear handler when called without arguments
332
+ @on[:sampling] = nil
333
+ end
334
+
179
335
  self
180
336
  end
181
337
 
182
338
  def elicitation_enabled?
339
+ @adapter_type == :ruby_llm && MCP.config.elicitation.enabled?
340
+ end
341
+
342
+ def elicitation_callback_enabled?
183
343
  @on.key?(:elicitation) && !@on[:elicitation].nil?
184
344
  end
185
345
 
186
- def on_elicitation(&block)
187
- @on[:elicitation] = block
346
+ def on_elicitation(handler_class = nil, **options, &block)
347
+ require_feature!(:elicitation)
348
+
349
+ if handler_class
350
+ # Validate handler class
351
+ validate_handler_class!(handler_class, :execute)
352
+
353
+ # Handler class provided
354
+ @on[:elicitation] = if options.any?
355
+ lambda do |elicitation|
356
+ handler_class.new(
357
+ elicitation: elicitation,
358
+ coordinator: @adapter.native_client,
359
+ **options
360
+ ).call
361
+ end
362
+ else
363
+ handler_class
364
+ end
365
+ elsif block_given?
366
+ # Block provided (backward compatible)
367
+ @on[:elicitation] = block
368
+ else
369
+ # Clear handler when called without arguments
370
+ @on[:elicitation] = nil
371
+ end
372
+
373
+ sync_elicitation_handler_state
374
+
188
375
  self
189
376
  end
190
377
 
@@ -212,10 +399,55 @@ module RubyLLM
212
399
 
213
400
  private
214
401
 
215
- def setup_coordinator
216
- Coordinator.new(self,
217
- transport_type: @transport_type,
218
- config: @config)
402
+ # Get OAuth provider from adapter's transport if available
403
+ # @return [OAuthProvider, BrowserOAuthProvider, nil] OAuth provider or nil
404
+ def transport_oauth_provider
405
+ return nil unless @adapter
406
+
407
+ # For RubyLLMAdapter
408
+ if @adapter.respond_to?(:native_client)
409
+ transport = @adapter.native_client.transport
410
+ transport_protocol = transport.transport_protocol
411
+ return transport_protocol.oauth_provider if transport_protocol.respond_to?(:oauth_provider)
412
+ end
413
+
414
+ # For MCPSdkAdapter with wrapped transports
415
+ if @adapter.respond_to?(:mcp_client) && @adapter.instance_variable_get(:@mcp_client)
416
+ mcp_client = @adapter.instance_variable_get(:@mcp_client)
417
+ if mcp_client&.transport.respond_to?(:native_transport)
418
+ return mcp_client.transport.native_transport.oauth_provider
419
+ end
420
+ end
421
+
422
+ nil
423
+ end
424
+
425
+ def build_adapter
426
+ case @adapter_type
427
+ when :ruby_llm
428
+ RubyLLM::MCP::Adapters::RubyLLMAdapter.new(self,
429
+ transport_type: @transport_type,
430
+ config: @config)
431
+ when :mcp_sdk
432
+ RubyLLM::MCP::Adapters::MCPSdkAdapter.new(self,
433
+ transport_type: @transport_type,
434
+ config: @config)
435
+ else
436
+ raise ArgumentError, "Unknown adapter type: #{@adapter_type}"
437
+ end
438
+ end
439
+
440
+ def require_feature!(feature)
441
+ unless @adapter.supports?(feature)
442
+ raise Errors::UnsupportedFeature.new(
443
+ message: <<~MSG.strip
444
+ Feature '#{feature}' is not supported by the #{@adapter_type} adapter.
445
+
446
+ This feature requires the :ruby_llm adapter.
447
+ Change your configuration to use adapter: :ruby_llm
448
+ MSG
449
+ )
450
+ end
219
451
  end
220
452
 
221
453
  def fetch(cache_key, refresh)
@@ -229,9 +461,9 @@ module RubyLLM
229
461
  def build_map(raw_data, klass, with_prefix: false)
230
462
  raw_data.each_with_object({}) do |item, acc|
231
463
  instance = if with_prefix
232
- klass.new(@coordinator, item, with_prefix: @with_prefix)
464
+ klass.new(@adapter, item, with_prefix: @with_prefix)
233
465
  else
234
- klass.new(@coordinator, item)
466
+ klass.new(@adapter, item)
235
467
  end
236
468
  acc[instance.name] = instance
237
469
  end
@@ -246,7 +478,7 @@ module RubyLLM
246
478
  end
247
479
 
248
480
  def setup_roots
249
- @roots = Roots.new(paths: MCP.config.roots, coordinator: @coordinator)
481
+ @roots = Roots.new(paths: MCP.config.roots, adapter: @adapter)
250
482
  end
251
483
 
252
484
  def setup_sampling
@@ -254,11 +486,56 @@ module RubyLLM
254
486
  end
255
487
 
256
488
  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
489
+ # Only setup handlers that are supported
490
+ if @adapter.supports?(:progress_tracking)
491
+ @on[:progress] = MCP.config.on_progress
492
+ if @on[:progress] && alive?
493
+ @adapter.set_progress_tracking(enabled: true)
494
+ end
495
+ end
496
+
497
+ if @adapter.supports?(:human_in_the_loop)
498
+ @on[:human_in_the_loop] = MCP.config.on_human_in_the_loop
499
+ end
500
+
501
+ if @adapter.supports?(:logging)
502
+ @on[:logging] = MCP.config.on_logging
503
+ @on_logging_level = MCP.config.on_logging_level
504
+ end
505
+
506
+ if @adapter.supports?(:elicitation)
507
+ @on[:elicitation] = MCP.config.on_elicitation
508
+ end
509
+ end
510
+
511
+ def sync_elicitation_handler_state
512
+ return unless @adapter.supports?(:elicitation)
513
+
514
+ @adapter.set_elicitation_enabled(enabled: elicitation_enabled?)
515
+ end
516
+
517
+ # Get or create OAuth storage shared with transport
518
+ def oauth_storage
519
+ # Try to get storage from transport's OAuth provider
520
+ transport_oauth = transport_oauth_provider
521
+ return transport_oauth.storage if transport_oauth
522
+
523
+ # Create new storage shared with client
524
+ @oauth_storage ||= Auth::MemoryStorage.new
525
+ end
526
+
527
+ # Validate that a handler class has required methods
528
+ # @param handler_class [Class] the handler class to validate
529
+ # @param required_method [Symbol] the method that must be defined
530
+ # @raise [ArgumentError] if validation fails
531
+ def validate_handler_class!(handler_class, required_method)
532
+ unless Handlers.handler_class?(handler_class)
533
+ raise ArgumentError, "Handler must be a class, got #{handler_class.class}"
534
+ end
535
+
536
+ unless handler_class.method_defined?(required_method)
537
+ raise ArgumentError, "Handler class #{handler_class} must define ##{required_method} method"
538
+ end
262
539
  end
263
540
  end
264
541
  end