ruby_llm-mcp 0.4.1 → 0.5.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +296 -25
  3. data/lib/ruby_llm/chat.rb +2 -1
  4. data/lib/ruby_llm/mcp/client.rb +32 -13
  5. data/lib/ruby_llm/mcp/configuration.rb +123 -3
  6. data/lib/ruby_llm/mcp/coordinator.rb +108 -115
  7. data/lib/ruby_llm/mcp/errors.rb +3 -1
  8. data/lib/ruby_llm/mcp/notification_handler.rb +84 -0
  9. data/lib/ruby_llm/mcp/{requests/cancelled_notification.rb → notifications/cancelled.rb} +2 -2
  10. data/lib/ruby_llm/mcp/{requests/initialize_notification.rb → notifications/initialize.rb} +7 -3
  11. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
  12. data/lib/ruby_llm/mcp/parameter.rb +19 -1
  13. data/lib/ruby_llm/mcp/progress.rb +3 -1
  14. data/lib/ruby_llm/mcp/prompt.rb +18 -0
  15. data/lib/ruby_llm/mcp/railtie.rb +20 -0
  16. data/lib/ruby_llm/mcp/requests/initialization.rb +8 -4
  17. data/lib/ruby_llm/mcp/requests/ping.rb +6 -2
  18. data/lib/ruby_llm/mcp/requests/prompt_list.rb +10 -2
  19. data/lib/ruby_llm/mcp/requests/resource_list.rb +12 -2
  20. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +12 -2
  21. data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
  22. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
  23. data/lib/ruby_llm/mcp/requests/tool_call.rb +1 -1
  24. data/lib/ruby_llm/mcp/requests/tool_list.rb +10 -2
  25. data/lib/ruby_llm/mcp/resource.rb +17 -0
  26. data/lib/ruby_llm/mcp/response_handler.rb +58 -0
  27. data/lib/ruby_llm/mcp/responses/error.rb +33 -0
  28. data/lib/ruby_llm/mcp/{requests/ping_response.rb → responses/ping.rb} +2 -2
  29. data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
  30. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
  31. data/lib/ruby_llm/mcp/result.rb +21 -8
  32. data/lib/ruby_llm/mcp/roots.rb +45 -0
  33. data/lib/ruby_llm/mcp/sample.rb +148 -0
  34. data/lib/ruby_llm/mcp/{capabilities.rb → server_capabilities.rb} +1 -1
  35. data/lib/ruby_llm/mcp/tool.rb +35 -4
  36. data/lib/ruby_llm/mcp/transport.rb +58 -0
  37. data/lib/ruby_llm/mcp/transports/http_client.rb +26 -0
  38. data/lib/ruby_llm/mcp/{transport → transports}/sse.rb +25 -24
  39. data/lib/ruby_llm/mcp/{transport → transports}/stdio.rb +28 -26
  40. data/lib/ruby_llm/mcp/{transport → transports}/streamable_http.rb +25 -29
  41. data/lib/ruby_llm/mcp/transports/timeout.rb +32 -0
  42. data/lib/ruby_llm/mcp/version.rb +1 -1
  43. data/lib/ruby_llm/mcp.rb +50 -9
  44. metadata +23 -12
  45. data/lib/ruby_llm/mcp/requests/base.rb +0 -31
  46. data/lib/ruby_llm/mcp/requests/meta.rb +0 -30
  47. data/lib/tasks/release.rake +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f2215062be0096c9eb63dc7b01909a2566731e9ca787572653eb51f8f6292eb
4
- data.tar.gz: 987658d5ffcf571d25a252d6ce8f53e4f141a222347ec8dad48a8fbca0344869
3
+ metadata.gz: 328e09780647e7ef9a35aac8a8fd8e0b1c84aded38239b4ad9b08803e1d638d3
4
+ data.tar.gz: f0f1022e6917f56b95ecfd4540fb517245f6b5450f9369d4cee2cc34e2eb0934
5
5
  SHA512:
6
- metadata.gz: 46f94ef9c8a9522a4c66998354755818607874269306bfc91e9fccfde76b783a9e5003306e636c164dc2c01258fa586a054e083abf73f0fe562a887d956238da
7
- data.tar.gz: b1cb8a80607903af6bd87c19f745b35285b07e51902af40f2617645bf15ff463e3285cc44759cd33968cad6f5a91059744cd5ce23fa2d8cf4b14a738960eab39
6
+ metadata.gz: d52fcbfb4fe1c1ebae1fece801a0f9b20a9aa467f7a3333c65e0db4fcc1ee0038df571ad4d7cdae2d8863d4d4ddd29c019a3ff245de5970142dcdccf7228ae45
7
+ data.tar.gz: 471a551100918f7a86c6fe22b0065f340ec7ed87a4b1d709f49ce8c782b564532e840b6f160d5c082ef5f08734d1bd3ad59fb514f119150ec37f42e626678cc4
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RubyLLM::MCP
2
2
 
3
- Aiming to make using MCP with RubyLLM as easy as possible.
3
+ Aiming to make using MCPs with RubyLLM as easy as possible.
4
4
 
5
5
  This project is a Ruby client for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), designed to work seamlessly with [RubyLLM](https://github.com/crmne/ruby_llm). This gem enables Ruby applications to connect to MCP servers and use their tools, resources and prompts as part of LLM conversations.
6
6
 
@@ -8,11 +8,13 @@ This project is a Ruby client for the [Model Context Protocol (MCP)](https://mod
8
8
 
9
9
  ## Features
10
10
 
11
- - 🔌 **Multiple Transport Types**: Support for SSE (Server-Sent Events), Streamable HTTP, and stdio transports
11
+ - 🔌 **Multiple Transport Types**: Streamable HTTP, and STDIO and legacy SSE transports
12
12
  - 🛠️ **Tool Integration**: Automatically converts MCP tools into RubyLLM-compatible tools
13
13
  - 📄 **Resource Management**: Access and include MCP resources (files, data) and resource templates in conversations
14
14
  - 🎯 **Prompt Integration**: Use predefined MCP prompts with arguments for consistent interactions
15
+ - 🎛️ **Client Features**: Support for sampling and roots
15
16
  - 🎨 **Enhanced Chat Interface**: Extended RubyLLM chat methods for seamless MCP integration
17
+ - 🔄 **Multiple Client Management**: Create and manage multiple MCP clients simultaneously for different servers and purposes
16
18
  - 📚 **Simple API**: Easy-to-use interface that integrates seamlessly with RubyLLM
17
19
 
18
20
  ## Installation
@@ -121,13 +123,11 @@ puts result # 3
121
123
  # If the human in the loop returns false, the tool call will be cancelled
122
124
  result = tool.execute(a: 2, b: 2)
123
125
  puts result # Tool execution error: Tool call was cancelled by the client
124
- ```
125
126
 
126
127
  tool = client.tool("add")
127
128
  result = tool.execute(a: 1, b: 2)
128
129
  puts result
129
-
130
- ````
130
+ ```
131
131
 
132
132
  ### Support Complex Parameters
133
133
 
@@ -135,7 +135,7 @@ If you want to support complex parameters, like an array of objects it currently
135
135
 
136
136
  ```ruby
137
137
  RubyLLM::MCP.support_complex_parameters!
138
- ````
138
+ ```
139
139
 
140
140
  ### Streaming Responses with Tool Calls
141
141
 
@@ -228,23 +228,6 @@ content = log_template.to_content(arguments: {
228
228
  puts content
229
229
  ```
230
230
 
231
- #### Resource Argument Completion
232
-
233
- For resource templates, you can get suggested values for arguments:
234
-
235
- ```ruby
236
- template = client.resource_template("user_profile")
237
-
238
- # Search for possible values for a specific argument
239
- suggestions = template.complete("username", "john")
240
- puts "Suggested usernames:"
241
- suggestions.values.each do |value|
242
- puts "- #{value}"
243
- end
244
- puts "Total matches: #{suggestions.total}"
245
- puts "Has more: #{suggestions.has_more}"
246
- ```
247
-
248
231
  ### Working with Prompts
249
232
 
250
233
  MCP servers can provide predefined prompts that can be used in conversations:
@@ -307,7 +290,7 @@ response = chat.ask("Please review the recent commits using the checklist and su
307
290
  puts response
308
291
  ```
309
292
 
310
- ## Argument Completion
293
+ ### Argument Completion
311
294
 
312
295
  Some MCP servers support argument completion for prompts and resource templates:
313
296
 
@@ -324,7 +307,13 @@ puts "Total matches: #{suggestions.total}"
324
307
  puts "Has more results: #{suggestions.has_more}"
325
308
  ```
326
309
 
327
- ## Additional Chat Methods
310
+ ### Pagination
311
+
312
+ MCP servers can support pagination for their lists. The client will automatically paginate the lists to include all items from the list you wanted to pull.
313
+
314
+ Pagination is supported for tools, resources, prompts, and resource templates.
315
+
316
+ ### Additional Chat Methods
328
317
 
329
318
  The gem extends RubyLLM's chat interface with convenient methods for MCP integration:
330
319
 
@@ -347,6 +336,69 @@ chat.with_prompt(prompt, arguments: { name: "Alice" })
347
336
  response = chat.ask_prompt(prompt, arguments: { name: "Alice" })
348
337
  ```
349
338
 
339
+ ## Rails Integration
340
+
341
+ RubyLLM MCP provides seamless Rails integration through a Railtie and generator system.
342
+
343
+ ### Setup
344
+
345
+ Generate the configuration files:
346
+
347
+ ```bash
348
+ rails generate ruby_llm:mcp:install
349
+ ```
350
+
351
+ This creates:
352
+
353
+ - `config/initializers/ruby_llm_mcp.rb` - Main configuration
354
+ - `config/mcps.yml` - MCP servers configuration
355
+
356
+ ### MCP Server Configuration
357
+
358
+ Configure your MCP servers in `config/mcps.yml`:
359
+
360
+ ```yaml
361
+ mcp_servers:
362
+ filesystem:
363
+ transport_type: stdio
364
+ command: npx
365
+ args:
366
+ - "@modelcontextprotocol/server-filesystem"
367
+ - "<%= Rails.root %>"
368
+ env: {}
369
+ with_prefix: true
370
+
371
+ api_server:
372
+ transport_type: sse
373
+ url: "https://api.example.com/mcp/sse"
374
+ headers:
375
+ Authorization: "Bearer <%= ENV['API_TOKEN'] %>"
376
+ ```
377
+
378
+ ### Automatic Client Management
379
+
380
+ With `launch_control: :automatic`, Rails will:
381
+
382
+ - Start all configured MCP clients when the application initializes
383
+ - Gracefully shut down clients when the application exits
384
+ - Handle client lifecycle automatically
385
+
386
+ However, it's very command to due to the performace of LLM calls that are made in the background.
387
+
388
+ For this, we recommend using `launch_control: :manual` and use `establish_connection` method to manage the client lifecycle manually inside your background jobs. It will provide you active connections to the MCP servers, and take care of closing them when the job is done.
389
+
390
+ ```ruby
391
+ RubyLLM::MCP.establish_connection do |clients|
392
+ chat = RubyLLM.chat(model: "gpt-4")
393
+ chat.with_tools(*clients.tools)
394
+
395
+ response = chat.ask("Hello, world!")
396
+ puts response
397
+ end
398
+ ```
399
+
400
+ You can also avoid this completely manually start and stop the clients if you so choose.
401
+
350
402
  ## Client Lifecycle Management
351
403
 
352
404
  You can manage the MCP client connection lifecycle:
@@ -462,6 +514,81 @@ puts result
462
514
  # Result: { status: "success", data: "Processed data" }
463
515
  ```
464
516
 
517
+ ## Client Features
518
+
519
+ The RubyLLM::MCP client provides support functionality that can be exposed to MCP servers. These features must be explicitly configured before creating client objects to ensure you're opting into this functionality.
520
+
521
+ ### Roots
522
+
523
+ Roots provide MCP servers with access to underlying file system information. The implementation starts with a lightweight approach due to the MCP specification's current limitations on root usage.
524
+
525
+ When roots are configured, the client will:
526
+
527
+ - Expose roots as a supported capability to MCP servers
528
+ - Support dynamic addition and removal of roots during the client lifecycle
529
+ - Fire `notifications/roots/list_changed` events when roots are modified
530
+
531
+ #### Configuration
532
+
533
+ ```ruby
534
+ RubyLLM::MCP.config do |config|
535
+ config.roots = ["to/a/path", Rails.root]
536
+ end
537
+
538
+ client = RubyLLM::MCP::Client.new(...)
539
+ ```
540
+
541
+ #### Usage
542
+
543
+ ```ruby
544
+ # Access current root paths
545
+ client.roots.paths
546
+ # => ["to/a/path", #<Pathname:/to/rails/root/path>]
547
+
548
+ # Add a new root (fires list_changed notification)
549
+ client.roots.add("new/path")
550
+ client.roots.paths
551
+ # => ["to/a/path", #<Pathname:/to/rails/root/path>, "new/path"]
552
+
553
+ # Remove a root (fires list_changed notification)
554
+ client.roots.remove("to/a/path")
555
+ client.roots.paths
556
+ # => [#<Pathname:/to/rails/root/path>, "new/path"]
557
+ ```
558
+
559
+ ### Sampling
560
+
561
+ Sampling allows MCP servers to offload LLM requests to the MCP client rather than making them directly from the server. This enables MCP servers to optionally use LLM connections through the client.
562
+
563
+ #### Configuration
564
+
565
+ ```ruby
566
+ RubyLLM::MCP.configure do |config|
567
+ config.sampling.enabled = true
568
+ config.sampling.preferred_model = "gpt-4.1"
569
+
570
+ # Optional: Use a block for dynamic model selection
571
+ config.sampling.preferred_model do |model_preferences|
572
+ model_preferences.hints.first
573
+ end
574
+
575
+ # Optional: Add guards to filter sampling requests
576
+ config.sampling.guard do |sample|
577
+ sample.message.include("Hello")
578
+ end
579
+ end
580
+ ```
581
+
582
+ #### How It Works
583
+
584
+ With the above configuration:
585
+
586
+ - Clients will respond to all incoming sample requests using the specified model (`gpt-4.1`)
587
+ - Sample messages will only be approved if they contain the word "Hello" (when using the guard)
588
+ - The `preferred_model` can be a string or a proc that provides dynamic model selection based on MCP server characteristics
589
+
590
+ The `preferred_model` proc receives model preferences from the MCP server, allowing you to make intelligent model selection decisions based on the server's requirements for success.
591
+
465
592
  ## Transport Types
466
593
 
467
594
  ### SSE (Server-Sent Events)
@@ -510,6 +637,150 @@ client = RubyLLM::MCP.client(
510
637
  )
511
638
  ```
512
639
 
640
+ ## Creating Custom Transports
641
+
642
+ Part of the MCP specification outlines that custom transports can be used for some MCP servers. Out of the box, RubyLLM::MCP supports Streamable HTTP transports, STDIO and the legacy SSE transport.
643
+
644
+ You can create custom transport implementations to support additional communication protocols or specialized connection methods.
645
+
646
+ ### Transport Registration
647
+
648
+ Register your custom transport with the transport factory:
649
+
650
+ ```ruby
651
+ # Define your custom transport class
652
+ class MyCustomTransport
653
+ # Implementation details...
654
+ end
655
+
656
+ # Register it with the factory
657
+ RubyLLM::MCP::Transport.register_transport(:my_custom, MyCustomTransport)
658
+
659
+ # Now you can use it
660
+ client = RubyLLM::MCP.client(
661
+ name: "custom-server",
662
+ transport_type: :my_custom,
663
+ config: {
664
+ # Your custom configuration
665
+ }
666
+ )
667
+ ```
668
+
669
+ ### Required Interface
670
+
671
+ All transport implementations must implement the following interface:
672
+
673
+ ```ruby
674
+ class MyCustomTransport
675
+ # Initialize the transport
676
+ def initialize(coordinator:, **config)
677
+ @coordinator = coordinator # Uses for communication between the client and the MCP server
678
+ @config = config # Transport-specific configuration
679
+ end
680
+
681
+ # Send a request and optionally wait for response
682
+ # Returns a RubyLLM::MCP::Result object
683
+ # body: the request body
684
+ # add_id: true will add an id to the request
685
+ # wait_for_response: true will wait for a response from the MCP server
686
+ # Returns a RubyLLM::MCP::Result object
687
+ def request(body, add_id: true, wait_for_response: true)
688
+ # Implementation: send request and return result
689
+ data = some_method_to_send_request_and_get_result(body)
690
+ # Use Result object to make working with the protocol easier
691
+ result = RubyLLM::MCP::Result.new(data)
692
+
693
+ # Call the coordinator to process the result
694
+ @coordinator.process_result(result)
695
+ return if result.nil? # Some results are related to notifications and should not be returned to the client, but processed by the coordinator instead
696
+
697
+ # Return the result
698
+ result
699
+ end
700
+
701
+ # Check if transport is alive/connected
702
+ def alive?
703
+ # Implementation: return true if connected
704
+ end
705
+
706
+ # Start the transport connection
707
+ def start
708
+ # Implementation: establish connection
709
+ end
710
+
711
+ # Close the transport connection
712
+ def close
713
+ # Implementation: cleanup and close connection
714
+ end
715
+
716
+ # Set the MCP protocol version, used in some transports to identify the agreed upon protocol version
717
+ def set_protocol_version(version)
718
+ @protocol_version = version
719
+ end
720
+ end
721
+ ```
722
+
723
+ ### The Result Object
724
+
725
+ The `RubyLLM::MCP::Result` class wraps MCP responses and provides convenient methods:
726
+
727
+ ```ruby
728
+ result = transport.request(body)
729
+
730
+ # Core properties
731
+ result.id # Request ID
732
+ result.method # Request method
733
+ result.result # Result data (hash)
734
+ result.params # Request parameters
735
+ result.error # Error data (hash)
736
+ result.session_id # Session ID (if applicable)
737
+
738
+ # Type checking
739
+ result.success? # Has result data
740
+ result.error? # Has error data
741
+ result.notification? # Is a notification
742
+ result.request? # Is a request
743
+ result.response? # Is a response
744
+
745
+ # Specialized methods
746
+ result.tool_success? # Successful tool execution
747
+ result.execution_error? # Tool execution failed
748
+ result.matching_id?(id) # Matches request ID
749
+ result.next_cursor? # Has pagination cursor
750
+
751
+ # Error handling
752
+ result.raise_error! # Raise exception if error
753
+ result.to_error # Convert to Error object
754
+
755
+ # Notifications
756
+ result.notification # Get notification object
757
+ ```
758
+
759
+ ### Error Handling
760
+
761
+ Custom transports should handle errors appropriately. If request fails, you should raise a `RubyLLM::MCP::Errors::TransportError` exception. If the request times out, you should raise a `RubyLLM::MCP::Errors::TimeoutError` exception. This will ensure that a cancellation notification is sent to the MCP server correctly.
762
+
763
+ ```ruby
764
+ def request(body, add_id: true, wait_for_response: true)
765
+ begin
766
+ # Send request
767
+ send_request(body)
768
+ rescue SomeConnectionError => e
769
+ # Convert to MCP transport error
770
+ raise RubyLLM::MCP::Errors::TransportError.new(
771
+ message: "Connection failed: #{e.message}",
772
+ error: e
773
+ )
774
+ rescue Timeout::Error => e
775
+ # Convert to MCP timeout error
776
+ raise RubyLLM::MCP::Errors::TimeoutError.new(
777
+ message: "Request timeout after #{@request_timeout}ms",
778
+ request_id: body["id"]
779
+ )
780
+ end
781
+ end
782
+ ```
783
+
513
784
  ## RubyLLM::MCP and Client Configuration Options
514
785
 
515
786
  MCP comes with some common configuration options that can be set on the client.
data/lib/ruby_llm/chat.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This is an override of the RubyLLM::Chat class to convient methods for easy MCP support
3
+ # This is an override of the RubyLLM::Chat class to add convenient methods to more
4
+ # easily work with the MCP clients.
4
5
  module RubyLLM
5
6
  class Chat
6
7
  def with_resources(*resources, **args)
@@ -7,10 +7,11 @@ module RubyLLM
7
7
  class Client
8
8
  extend Forwardable
9
9
 
10
- attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on
10
+ attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on, :roots
11
11
 
12
12
  def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
13
13
  @name = name
14
+ @with_prefix = config.delete(:with_prefix) || false
14
15
  @config = config.merge(request_timeout: request_timeout)
15
16
  @transport_type = transport_type.to_sym
16
17
  @request_timeout = request_timeout
@@ -25,10 +26,13 @@ module RubyLLM
25
26
 
26
27
  @log_level = nil
27
28
 
29
+ setup_roots
30
+ setup_sampling
31
+
28
32
  @coordinator.start_transport if start
29
33
  end
30
34
 
31
- def_delegators :@coordinator, :alive?, :capabilities, :ping
35
+ def_delegators :@coordinator, :alive?, :capabilities, :ping, :client_capabilities
32
36
 
33
37
  def start
34
38
  @coordinator.start_transport
@@ -47,7 +51,7 @@ module RubyLLM
47
51
 
48
52
  fetch(:tools, refresh) do
49
53
  tools = @coordinator.tool_list
50
- build_map(tools, MCP::Tool)
54
+ build_map(tools, MCP::Tool, with_prefix: @with_prefix)
51
55
  end
52
56
 
53
57
  @tools.values
@@ -152,16 +156,19 @@ module RubyLLM
152
156
  !@log_level.nil?
153
157
  end
154
158
 
155
- def on_logging(level: Logging::WARNING, logger: nil, &block)
159
+ def on_logging(level: Logging::WARNING, &block)
156
160
  @coordinator.set_logging(level: level)
157
161
 
158
- @on[:logging] = if block_given?
159
- block
160
- else
161
- lambda do |notification|
162
- @coordinator.default_process_logging_message(notification, logger: logger)
163
- end
164
- end
162
+ @on[:logging] = block
163
+ self
164
+ end
165
+
166
+ def sampling_callback_enabled?
167
+ @on.key?(:sampling) && !@on[:sampling].nil?
168
+ end
169
+
170
+ def on_sampling(&block)
171
+ @on[:sampling] = block
165
172
  self
166
173
  end
167
174
 
@@ -181,12 +188,24 @@ module RubyLLM
181
188
  instance_variable_get("@#{cache_key}")
182
189
  end
183
190
 
184
- def build_map(raw_data, klass)
191
+ def build_map(raw_data, klass, with_prefix: false)
185
192
  raw_data.each_with_object({}) do |item, acc|
186
- instance = klass.new(@coordinator, item)
193
+ instance = if with_prefix
194
+ klass.new(@coordinator, item, with_prefix: @with_prefix)
195
+ else
196
+ klass.new(@coordinator, item)
197
+ end
187
198
  acc[instance.name] = instance
188
199
  end
189
200
  end
201
+
202
+ def setup_roots
203
+ @roots = Roots.new(paths: MCP.config.roots, coordinator: @coordinator)
204
+ end
205
+
206
+ def setup_sampling
207
+ @on[:sampling] = MCP.config.sampling.guard
208
+ end
190
209
  end
191
210
  end
192
211
  end
@@ -1,14 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "yaml"
5
+ require "erb"
6
+
3
7
  module RubyLLM
4
8
  module MCP
5
9
  class Configuration
6
- attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters
7
- attr_writer :logger
10
+ class Sampling
11
+ attr_accessor :enabled
12
+ attr_writer :preferred_model
13
+
14
+ def initialize
15
+ set_defaults
16
+ end
17
+
18
+ def reset!
19
+ set_defaults
20
+ end
21
+
22
+ def guard(&block)
23
+ @guard = block if block_given?
24
+ @guard
25
+ end
26
+
27
+ def preferred_model(&block)
28
+ @preferred_model = block if block_given?
29
+ @preferred_model
30
+ end
31
+
32
+ def enabled?
33
+ @enabled
34
+ end
35
+
36
+ private
37
+
38
+ def set_defaults
39
+ @enabled = false
40
+ @preferred_model = nil
41
+ @guard = nil
42
+ end
43
+ end
44
+
45
+ class ConfigFile
46
+ attr_reader :file_path
47
+
48
+ def initialize(file_path)
49
+ @file_path = file_path
50
+ end
51
+
52
+ def parse
53
+ @parse ||= if @file_path && File.exist?(@file_path)
54
+ config = parse_config_file
55
+ load_mcps_config(config)
56
+ else
57
+ []
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def parse_config_file
64
+ output = ERB.new(File.read(@file_path)).result
65
+
66
+ if [".yaml", ".yml"].include?(File.extname(@file_path))
67
+ YAML.safe_load(output, symbolize_names: true)
68
+ else
69
+ JSON.parse(output, symbolize_names: true)
70
+ end
71
+ end
72
+
73
+ def load_mcps_config(config)
74
+ return [] unless config.key?(:mcp_servers)
75
+
76
+ config[:mcp_servers].map do |name, configuration|
77
+ {
78
+ name: name,
79
+ transport_type: configuration.delete(:transport_type),
80
+ start: false,
81
+ config: configuration
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ attr_accessor :request_timeout,
88
+ :log_file,
89
+ :log_level,
90
+ :has_support_complex_parameters,
91
+ :roots,
92
+ :sampling,
93
+ :max_connections,
94
+ :pool_timeout,
95
+ :config_path,
96
+ :launch_control
97
+
98
+ attr_writer :logger, :mcp_configuration
8
99
 
9
100
  REQUEST_TIMEOUT_DEFAULT = 8000
10
101
 
11
102
  def initialize
103
+ @sampling = Sampling.new
12
104
  set_defaults
13
105
  end
14
106
 
@@ -31,6 +123,10 @@ module RubyLLM
31
123
  )
32
124
  end
33
125
 
126
+ def mcp_configuration
127
+ @mcp_configuration + load_mcps_config
128
+ end
129
+
34
130
  def inspect
35
131
  redacted = lambda do |name, value|
36
132
  if name.match?(/_id|_key|_secret|_token$/)
@@ -51,15 +147,39 @@ module RubyLLM
51
147
 
52
148
  private
53
149
 
150
+ def load_mcps_config
151
+ @config_file ||= ConfigFile.new(config_path)
152
+ @config_file.parse
153
+ end
154
+
54
155
  def set_defaults
55
156
  # Connection configuration
56
157
  @request_timeout = REQUEST_TIMEOUT_DEFAULT
57
158
 
159
+ # Connection Pool
160
+ @max_connections = Float::INFINITY
161
+ @pool_timeout = 5
162
+
58
163
  # Logging configuration
59
164
  @log_file = $stdout
60
165
  @log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
61
- @has_support_complex_parameters = false
62
166
  @logger = nil
167
+
168
+ # Complex parameters support
169
+ @has_support_complex_parameters = false
170
+
171
+ # MCPs configuration
172
+ @mcps_config_path = nil
173
+ @mcp_configuration = []
174
+
175
+ # Rails specific configuration
176
+ @launch_control = :automatic
177
+
178
+ # Roots configuration
179
+ @roots = []
180
+
181
+ # Sampling configuration
182
+ @sampling.reset!
63
183
  end
64
184
  end
65
185
  end