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,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module RubyLLM
6
+ module MCP
7
+ class Client
8
+ extend Forwardable
9
+
10
+ attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on, :roots, :adapter,
11
+ :on_logging_level
12
+ attr_accessor :linked_resources
13
+
14
+ def initialize(name:, transport_type:, sdk: nil, adapter: nil, start: true, # rubocop:disable Metrics/ParameterLists
15
+ request_timeout: MCP.config.request_timeout, config: {})
16
+ @name = name
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
+ @config = config.merge(request_timeout: request_timeout)
28
+ @request_timeout = request_timeout
29
+
30
+ # Store OAuth config for later use
31
+ @oauth_config = config[:oauth] || config["oauth"]
32
+ @oauth_provider = nil
33
+ @oauth_storage = nil
34
+
35
+ @on = {}
36
+ @tools = {}
37
+ @resources = {}
38
+ @resource_templates = {}
39
+ @prompts = {}
40
+
41
+ @log_level = nil
42
+
43
+ @linked_resources = []
44
+
45
+ # Build adapter based on configuration
46
+ @adapter = build_adapter
47
+
48
+ setup_roots if @adapter.supports?(:roots)
49
+ setup_sampling if @adapter.supports?(:sampling)
50
+ setup_event_handlers
51
+
52
+ @adapter.start if start
53
+ end
54
+
55
+ def_delegators :@adapter, :alive?, :capabilities, :ping, :client_capabilities,
56
+ :register_in_flight_request, :unregister_in_flight_request,
57
+ :cancel_in_flight_request
58
+
59
+ def start
60
+ @adapter.start
61
+ end
62
+
63
+ def stop
64
+ @adapter.stop
65
+ end
66
+
67
+ def restart!
68
+ @adapter.restart!
69
+ end
70
+
71
+ # Get or create OAuth provider for this client
72
+ # @param type [Symbol] OAuth provider type (:standard or :browser, defaults to :standard)
73
+ # @param options [Hash] additional options passed to provider
74
+ # @return [OAuthProvider, BrowserOAuthProvider] OAuth provider instance
75
+ def oauth(type: :standard, **options)
76
+ # Return existing provider if already created
77
+ return @oauth_provider if @oauth_provider
78
+
79
+ # Get provider from transport if it already exists
80
+ transport_oauth = transport_oauth_provider
81
+ return transport_oauth if transport_oauth
82
+
83
+ # Create new provider lazily
84
+ server_url = @config[:url] || @config["url"]
85
+ unless server_url
86
+ raise Errors::ConfigurationError.new(
87
+ message: "Cannot create OAuth provider without server URL in config"
88
+ )
89
+ end
90
+
91
+ oauth_options = {
92
+ server_url: server_url,
93
+ scope: @oauth_config&.dig(:scope) || @oauth_config&.dig("scope"),
94
+ storage: oauth_storage,
95
+ **options
96
+ }
97
+
98
+ @oauth_provider = Auth.create_oauth(
99
+ server_url,
100
+ type: type,
101
+ **oauth_options
102
+ )
103
+ end
104
+
105
+ def tools(refresh: false)
106
+ require_feature!(:tools)
107
+ return [] unless capabilities.tools_list?
108
+
109
+ fetch(:tools, refresh) do
110
+ tools = @adapter.tool_list
111
+ build_map(tools, MCP::Tool, with_prefix: @with_prefix)
112
+ end
113
+
114
+ @tools.values
115
+ end
116
+
117
+ def tool(name, refresh: false)
118
+ tools(refresh: refresh)
119
+
120
+ @tools[name]
121
+ end
122
+
123
+ def reset_tools!
124
+ @tools = {}
125
+ end
126
+
127
+ def resources(refresh: false)
128
+ require_feature!(:resources)
129
+ return [] unless capabilities.resources_list?
130
+
131
+ fetch(:resources, refresh) do
132
+ resources = @adapter.resource_list
133
+ resources = build_map(resources, MCP::Resource)
134
+ include_linked_resources(resources)
135
+ end
136
+
137
+ @resources.values
138
+ end
139
+
140
+ def resource(name, refresh: false)
141
+ resources(refresh: refresh)
142
+
143
+ @resources[name]
144
+ end
145
+
146
+ def reset_resources!
147
+ @resources = {}
148
+ end
149
+
150
+ def resource_templates(refresh: false)
151
+ require_feature!(:resource_templates)
152
+ return [] unless capabilities.resources_list?
153
+
154
+ fetch(:resource_templates, refresh) do
155
+ resource_templates = @adapter.resource_template_list
156
+ build_map(resource_templates, MCP::ResourceTemplate)
157
+ end
158
+
159
+ @resource_templates.values
160
+ end
161
+
162
+ def resource_template(name, refresh: false)
163
+ resource_templates(refresh: refresh)
164
+
165
+ @resource_templates[name]
166
+ end
167
+
168
+ def reset_resource_templates!
169
+ @resource_templates = {}
170
+ end
171
+
172
+ def prompts(refresh: false)
173
+ require_feature!(:prompts)
174
+ return [] unless capabilities.prompt_list?
175
+
176
+ fetch(:prompts, refresh) do
177
+ prompts = @adapter.prompt_list
178
+ build_map(prompts, MCP::Prompt)
179
+ end
180
+
181
+ @prompts.values
182
+ end
183
+
184
+ def prompt(name, refresh: false)
185
+ prompts(refresh: refresh)
186
+
187
+ @prompts[name]
188
+ end
189
+
190
+ def reset_prompts!
191
+ @prompts = {}
192
+ end
193
+
194
+ def tracking_progress?
195
+ @on.key?(:progress) && !@on[:progress].nil?
196
+ end
197
+
198
+ def on_progress(&block)
199
+ require_feature!(:progress_tracking)
200
+ if alive?
201
+ @adapter.set_progress_tracking(enabled: true)
202
+ end
203
+
204
+ @on[:progress] = block
205
+ self
206
+ end
207
+
208
+ def human_in_the_loop?
209
+ @on.key?(:human_in_the_loop) && !@on[:human_in_the_loop].nil?
210
+ end
211
+
212
+ def on_human_in_the_loop(&block)
213
+ require_feature!(:human_in_the_loop)
214
+ @on[:human_in_the_loop] = block
215
+ self
216
+ end
217
+
218
+ def logging_handler_enabled?
219
+ @on.key?(:logging) && !@on[:logging].nil?
220
+ end
221
+
222
+ def logging_enabled?
223
+ !@log_level.nil?
224
+ end
225
+
226
+ def on_logging(level: Logging::WARNING, &block)
227
+ require_feature!(:logging)
228
+ @on_logging_level = level
229
+ if alive?
230
+ @adapter.set_logging(level: level)
231
+ end
232
+
233
+ @on[:logging] = block
234
+ self
235
+ end
236
+
237
+ def sampling_callback_enabled?
238
+ @on.key?(:sampling) && !@on[:sampling].nil?
239
+ end
240
+
241
+ def on_sampling(&block)
242
+ require_feature!(:sampling)
243
+ @on[:sampling] = block
244
+ self
245
+ end
246
+
247
+ def elicitation_enabled?
248
+ @on.key?(:elicitation) && !@on[:elicitation].nil?
249
+ end
250
+
251
+ def on_elicitation(&block)
252
+ require_feature!(:elicitation)
253
+ @on[:elicitation] = block
254
+ self
255
+ end
256
+
257
+ def to_h
258
+ {
259
+ name: @name,
260
+ transport_type: @transport_type,
261
+ request_timeout: @request_timeout,
262
+ start: @start,
263
+ config: @config,
264
+ on: @on,
265
+ tools: @tools,
266
+ resources: @resources,
267
+ resource_templates: @resource_templates,
268
+ prompts: @prompts,
269
+ log_level: @log_level
270
+ }
271
+ end
272
+
273
+ alias as_json to_h
274
+
275
+ def inspect
276
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} #{to_h.map { |k, v| "#{k}: #{v}" }.join(', ')}>"
277
+ end
278
+
279
+ private
280
+
281
+ # Get OAuth provider from adapter's transport if available
282
+ # @return [OAuthProvider, BrowserOAuthProvider, nil] OAuth provider or nil
283
+ def transport_oauth_provider
284
+ return nil unless @adapter
285
+
286
+ # For RubyLLMAdapter
287
+ if @adapter.respond_to?(:native_client)
288
+ transport = @adapter.native_client.transport
289
+ transport_protocol = transport.transport_protocol
290
+ return transport_protocol.oauth_provider if transport_protocol.respond_to?(:oauth_provider)
291
+ end
292
+
293
+ # For MCPSdkAdapter with wrapped transports
294
+ if @adapter.respond_to?(:mcp_client) && @adapter.instance_variable_get(:@mcp_client)
295
+ mcp_client = @adapter.instance_variable_get(:@mcp_client)
296
+ if mcp_client&.transport.respond_to?(:native_transport)
297
+ return mcp_client.transport.native_transport.oauth_provider
298
+ end
299
+ end
300
+
301
+ nil
302
+ end
303
+
304
+ def build_adapter
305
+ case @adapter_type
306
+ when :ruby_llm
307
+ RubyLLM::MCP::Adapters::RubyLLMAdapter.new(self,
308
+ transport_type: @transport_type,
309
+ config: @config)
310
+ when :mcp_sdk
311
+ RubyLLM::MCP::Adapters::MCPSdkAdapter.new(self,
312
+ transport_type: @transport_type,
313
+ config: @config)
314
+ else
315
+ raise ArgumentError, "Unknown adapter type: #{@adapter_type}"
316
+ end
317
+ end
318
+
319
+ def require_feature!(feature)
320
+ unless @adapter.supports?(feature)
321
+ raise Errors::UnsupportedFeature.new(
322
+ message: <<~MSG.strip
323
+ Feature '#{feature}' is not supported by the #{@adapter_type} adapter.
324
+
325
+ This feature requires the :ruby_llm adapter.
326
+ Change your configuration to use adapter: :ruby_llm
327
+ MSG
328
+ )
329
+ end
330
+ end
331
+
332
+ def fetch(cache_key, refresh)
333
+ instance_variable_set("@#{cache_key}", {}) if refresh
334
+ if instance_variable_get("@#{cache_key}").empty?
335
+ instance_variable_set("@#{cache_key}", yield)
336
+ end
337
+ instance_variable_get("@#{cache_key}")
338
+ end
339
+
340
+ def build_map(raw_data, klass, with_prefix: false)
341
+ raw_data.each_with_object({}) do |item, acc|
342
+ instance = if with_prefix
343
+ klass.new(@adapter, item, with_prefix: @with_prefix)
344
+ else
345
+ klass.new(@adapter, item)
346
+ end
347
+ acc[instance.name] = instance
348
+ end
349
+ end
350
+
351
+ def include_linked_resources(resources)
352
+ @linked_resources.each do |resource|
353
+ resources[resource.name] = resource
354
+ end
355
+
356
+ resources
357
+ end
358
+
359
+ def setup_roots
360
+ @roots = Roots.new(paths: MCP.config.roots, adapter: @adapter)
361
+ end
362
+
363
+ def setup_sampling
364
+ @on[:sampling] = MCP.config.sampling.guard
365
+ end
366
+
367
+ def setup_event_handlers
368
+ # Only setup handlers that are supported
369
+ if @adapter.supports?(:progress_tracking)
370
+ @on[:progress] = MCP.config.on_progress
371
+ if @on[:progress] && alive?
372
+ @adapter.set_progress_tracking(enabled: true)
373
+ end
374
+ end
375
+
376
+ if @adapter.supports?(:human_in_the_loop)
377
+ @on[:human_in_the_loop] = MCP.config.on_human_in_the_loop
378
+ end
379
+
380
+ if @adapter.supports?(:logging)
381
+ @on[:logging] = MCP.config.on_logging
382
+ @on_logging_level = MCP.config.on_logging_level
383
+ end
384
+
385
+ if @adapter.supports?(:elicitation)
386
+ @on[:elicitation] = MCP.config.on_elicitation
387
+ end
388
+ end
389
+
390
+ # Get or create OAuth storage shared with transport
391
+ def oauth_storage
392
+ # Try to get storage from transport's OAuth provider
393
+ transport_oauth = transport_oauth_provider
394
+ return transport_oauth.storage if transport_oauth
395
+
396
+ # Create new storage shared with client
397
+ @oauth_storage ||= Auth::MemoryStorage.new
398
+ end
399
+ end
400
+ end
401
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Completion
6
+ attr_reader :argument, :values, :total, :has_more
7
+
8
+ def initialize(argument:, values:, total:, has_more:)
9
+ @argument = argument
10
+ @values = values
11
+ @total = total
12
+ @has_more = has_more
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Configuration
6
+ class AdapterConfig
7
+ VALID_ADAPTERS = %i[ruby_llm mcp_sdk].freeze
8
+ VALID_TRANSPORTS = %i[stdio sse streamable streamable_http http].freeze
9
+
10
+ attr_accessor :default_adapter
11
+
12
+ def initialize
13
+ @default_adapter = :ruby_llm
14
+ end
15
+
16
+ def validate!(adapter:, transport:)
17
+ validate_adapter!(adapter)
18
+ validate_transport!(transport)
19
+ validate_adapter_transport_combination!(adapter, transport)
20
+ end
21
+
22
+ def adapter_for(config)
23
+ config[:sdk] || config[:adapter] || @default_adapter
24
+ end
25
+
26
+ private
27
+
28
+ def validate_adapter!(adapter)
29
+ unless VALID_ADAPTERS.include?(adapter)
30
+ raise Errors::AdapterConfigurationError.new(
31
+ message: "Invalid adapter '#{adapter}'. Valid options: #{VALID_ADAPTERS.join(', ')}"
32
+ )
33
+ end
34
+ end
35
+
36
+ def validate_transport!(transport)
37
+ unless VALID_TRANSPORTS.include?(transport)
38
+ raise Errors::AdapterConfigurationError.new(
39
+ message: "Invalid transport '#{transport}'. Valid options: #{VALID_TRANSPORTS.join(', ')}"
40
+ )
41
+ end
42
+ end
43
+
44
+ def validate_adapter_transport_combination!(adapter, transport)
45
+ # SSE is supported by both ruby_llm and mcp_sdk adapters
46
+ # No validation needed at this time
47
+ end
48
+ end
49
+
50
+ class Sampling
51
+ attr_accessor :enabled
52
+ attr_writer :preferred_model
53
+
54
+ def initialize
55
+ set_defaults
56
+ end
57
+
58
+ def reset!
59
+ set_defaults
60
+ end
61
+
62
+ def guard(&block)
63
+ @guard = block if block_given?
64
+ @guard
65
+ end
66
+
67
+ def preferred_model(&block)
68
+ @preferred_model = block if block_given?
69
+ @preferred_model
70
+ end
71
+
72
+ def enabled?
73
+ @enabled
74
+ end
75
+
76
+ private
77
+
78
+ def set_defaults
79
+ @enabled = false
80
+ @preferred_model = nil
81
+ @guard = nil
82
+ end
83
+ end
84
+
85
+ class OAuth
86
+ attr_accessor :client_name,
87
+ :client_uri,
88
+ :software_id,
89
+ :software_version,
90
+ :logo_uri,
91
+ :contacts,
92
+ :tos_uri,
93
+ :policy_uri,
94
+ :jwks_uri,
95
+ :jwks,
96
+ :browser_success_page,
97
+ :browser_error_page
98
+
99
+ def initialize
100
+ @client_name = "RubyLLM MCP Client"
101
+ @client_uri = nil
102
+ @software_id = "ruby_llm-mcp"
103
+ @software_version = RubyLLM::MCP::VERSION
104
+ @logo_uri = nil
105
+ @contacts = nil
106
+ @tos_uri = nil
107
+ @policy_uri = nil
108
+ @jwks_uri = nil
109
+ @jwks = nil
110
+ @browser_success_page = nil
111
+ @browser_error_page = nil
112
+ end
113
+ end
114
+
115
+ class ConfigFile
116
+ attr_reader :file_path
117
+
118
+ def initialize(file_path)
119
+ @file_path = file_path
120
+ end
121
+
122
+ def parse
123
+ @parse ||= if @file_path && File.exist?(@file_path)
124
+ config = parse_config_file
125
+ load_mcps_config(config)
126
+ else
127
+ []
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def parse_config_file
134
+ output = ERB.new(File.read(@file_path)).result
135
+
136
+ if [".yaml", ".yml"].include?(File.extname(@file_path))
137
+ YAML.safe_load(output, symbolize_names: true)
138
+ else
139
+ JSON.parse(output, symbolize_names: true)
140
+ end
141
+ end
142
+
143
+ def load_mcps_config(config)
144
+ return [] unless config.key?(:mcp_servers)
145
+
146
+ config[:mcp_servers].map do |name, configuration|
147
+ {
148
+ name: name,
149
+ transport_type: configuration.delete(:transport_type),
150
+ start: false,
151
+ config: configuration
152
+ }
153
+ end
154
+ end
155
+ end
156
+
157
+ attr_accessor :request_timeout,
158
+ :log_file,
159
+ :log_level,
160
+ :roots,
161
+ :sampling,
162
+ :max_connections,
163
+ :pool_timeout,
164
+ :protocol_version,
165
+ :config_path,
166
+ :launch_control,
167
+ :on_logging_level,
168
+ :adapter_config,
169
+ :oauth
170
+
171
+ attr_writer :logger, :mcp_configuration
172
+
173
+ REQUEST_TIMEOUT_DEFAULT = 8000
174
+
175
+ def initialize
176
+ @sampling = Sampling.new
177
+ @adapter_config = AdapterConfig.new
178
+ @oauth = OAuth.new
179
+ set_defaults
180
+ end
181
+
182
+ def reset!
183
+ set_defaults
184
+ end
185
+
186
+ def logger
187
+ @logger ||= Logger.new(
188
+ log_file,
189
+ progname: "RubyLLM::MCP",
190
+ level: log_level
191
+ )
192
+ end
193
+
194
+ # Convenience method for setting default adapter
195
+ def default_adapter=(adapter)
196
+ @adapter_config.default_adapter = adapter
197
+ end
198
+
199
+ def default_adapter
200
+ @adapter_config.default_adapter
201
+ end
202
+
203
+ # Validate MCP configuration before use
204
+ def mcp_configuration
205
+ configs = @mcp_configuration + load_mcps_config
206
+ validate_configurations!(configs)
207
+ configs
208
+ end
209
+
210
+ def on_progress(&block)
211
+ @on_progress = block if block_given?
212
+ @on_progress
213
+ end
214
+
215
+ def on_human_in_the_loop(&block)
216
+ @on_human_in_the_loop = block if block_given?
217
+ @on_human_in_the_loop
218
+ end
219
+
220
+ def on_logging(&block)
221
+ @on_logging = block if block_given?
222
+ @on_logging
223
+ end
224
+
225
+ def on_elicitation(&block)
226
+ @on_elicitation = block if block_given?
227
+ @on_elicitation
228
+ end
229
+
230
+ def inspect
231
+ redacted = lambda do |name, value|
232
+ if name.match?(/_id|_key|_secret|_token$/)
233
+ value.nil? ? "nil" : "[FILTERED]"
234
+ else
235
+ value
236
+ end
237
+ end
238
+
239
+ inspection = instance_variables.map do |ivar|
240
+ name = ivar.to_s.delete_prefix("@")
241
+ value = redacted[name, instance_variable_get(ivar)]
242
+ "#{name}: #{value}"
243
+ end.join(", ")
244
+
245
+ "#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
246
+ end
247
+
248
+ private
249
+
250
+ def validate_configurations!(configs)
251
+ configs.each do |config|
252
+ adapter = @adapter_config.adapter_for(config)
253
+ transport = config[:transport_type]
254
+ # Convert string to symbol if needed
255
+ transport = transport.to_sym if transport.is_a?(String)
256
+
257
+ @adapter_config.validate!(
258
+ adapter: adapter,
259
+ transport: transport
260
+ )
261
+ end
262
+ end
263
+
264
+ def load_mcps_config
265
+ @config_file ||= ConfigFile.new(config_path)
266
+ @config_file.parse
267
+ end
268
+
269
+ def set_defaults
270
+ # Connection configuration
271
+ @request_timeout = REQUEST_TIMEOUT_DEFAULT
272
+
273
+ # Connection Pool
274
+ @max_connections = Float::INFINITY
275
+ @pool_timeout = 5
276
+
277
+ # Logging configuration
278
+ @log_file = $stdout
279
+ @log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
280
+ @logger = nil
281
+
282
+ # MCPs configuration
283
+ @mcps_config_path = nil
284
+ @mcp_configuration = []
285
+
286
+ # Rails specific configuration
287
+ @launch_control = :automatic
288
+
289
+ # Roots configuration
290
+ @roots = []
291
+
292
+ # Protocol configuration
293
+ @protocol_version = Native::Protocol.latest_version
294
+
295
+ # OAuth configuration
296
+ @oauth = OAuth.new
297
+
298
+ # Sampling configuration
299
+ @sampling.reset!
300
+
301
+ # Event handlers
302
+ @on_progress = nil
303
+ @on_human_in_the_loop = nil
304
+ @on_elicitation = nil
305
+ @on_logging_level = nil
306
+ @on_logging = nil
307
+ end
308
+ end
309
+ end
310
+ end