ruby_llm-mcp 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -25
  3. data/lib/generators/ruby_llm/mcp/install_generator.rb +27 -0
  4. data/lib/generators/ruby_llm/mcp/templates/README.txt +32 -0
  5. data/lib/generators/ruby_llm/mcp/templates/initializer.rb +42 -0
  6. data/lib/generators/ruby_llm/mcp/templates/mcps.yml +9 -0
  7. data/lib/ruby_llm/chat.rb +2 -1
  8. data/lib/ruby_llm/mcp/client.rb +32 -13
  9. data/lib/ruby_llm/mcp/configuration.rb +123 -3
  10. data/lib/ruby_llm/mcp/coordinator.rb +108 -115
  11. data/lib/ruby_llm/mcp/errors.rb +3 -1
  12. data/lib/ruby_llm/mcp/notification_handler.rb +84 -0
  13. data/lib/ruby_llm/mcp/{requests/cancelled_notification.rb → notifications/cancelled.rb} +2 -2
  14. data/lib/ruby_llm/mcp/{requests/initialize_notification.rb → notifications/initialize.rb} +7 -3
  15. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
  16. data/lib/ruby_llm/mcp/parameter.rb +19 -1
  17. data/lib/ruby_llm/mcp/progress.rb +3 -1
  18. data/lib/ruby_llm/mcp/prompt.rb +18 -0
  19. data/lib/ruby_llm/mcp/railtie.rb +20 -0
  20. data/lib/ruby_llm/mcp/requests/initialization.rb +8 -4
  21. data/lib/ruby_llm/mcp/requests/ping.rb +6 -2
  22. data/lib/ruby_llm/mcp/requests/prompt_list.rb +10 -2
  23. data/lib/ruby_llm/mcp/requests/resource_list.rb +12 -2
  24. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +12 -2
  25. data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
  26. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
  27. data/lib/ruby_llm/mcp/requests/tool_call.rb +1 -1
  28. data/lib/ruby_llm/mcp/requests/tool_list.rb +10 -2
  29. data/lib/ruby_llm/mcp/resource.rb +17 -0
  30. data/lib/ruby_llm/mcp/response_handler.rb +58 -0
  31. data/lib/ruby_llm/mcp/responses/error.rb +33 -0
  32. data/lib/ruby_llm/mcp/{requests/ping_response.rb → responses/ping.rb} +2 -2
  33. data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
  34. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
  35. data/lib/ruby_llm/mcp/result.rb +21 -8
  36. data/lib/ruby_llm/mcp/roots.rb +45 -0
  37. data/lib/ruby_llm/mcp/sample.rb +148 -0
  38. data/lib/ruby_llm/mcp/{capabilities.rb → server_capabilities.rb} +1 -1
  39. data/lib/ruby_llm/mcp/tool.rb +35 -4
  40. data/lib/ruby_llm/mcp/transport.rb +58 -0
  41. data/lib/ruby_llm/mcp/transports/http_client.rb +26 -0
  42. data/lib/ruby_llm/mcp/{transport → transports}/sse.rb +25 -24
  43. data/lib/ruby_llm/mcp/{transport → transports}/stdio.rb +28 -26
  44. data/lib/ruby_llm/mcp/{transport → transports}/streamable_http.rb +25 -29
  45. data/lib/ruby_llm/mcp/transports/timeout.rb +32 -0
  46. data/lib/ruby_llm/mcp/version.rb +1 -1
  47. data/lib/ruby_llm/mcp.rb +60 -9
  48. metadata +27 -11
  49. data/lib/ruby_llm/mcp/requests/base.rb +0 -31
  50. data/lib/ruby_llm/mcp/requests/meta.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f2215062be0096c9eb63dc7b01909a2566731e9ca787572653eb51f8f6292eb
4
- data.tar.gz: 987658d5ffcf571d25a252d6ce8f53e4f141a222347ec8dad48a8fbca0344869
3
+ metadata.gz: 871fadee0f0166df56c6e8ef4ce2a6598c7875af9ca05631687cab1966ccb4e9
4
+ data.tar.gz: 6cbefa984fd7b541005be0f3f05c3970ab511c90afec9d135212dca33127d992
5
5
  SHA512:
6
- metadata.gz: 46f94ef9c8a9522a4c66998354755818607874269306bfc91e9fccfde76b783a9e5003306e636c164dc2c01258fa586a054e083abf73f0fe562a887d956238da
7
- data.tar.gz: b1cb8a80607903af6bd87c19f745b35285b07e51902af40f2617645bf15ff463e3285cc44759cd33968cad6f5a91059744cd5ce23fa2d8cf4b14a738960eab39
6
+ metadata.gz: 43b1fcb2d2fd8d6d1ebf7b8a43266443e72be5551ae9866a565bd38f7a90b98832294aadadb562d58a82dae24ff72251fe9eda91aab06a75df061413885c0560
7
+ data.tar.gz: 9a72ae094803de509b3ec447a43899a001ebd857d7f1085cb31f36a923d189324f7766ed37249866f0fc38e82fb962c11b00737f6821a7e6d723c6a30aa537aa
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,86 @@ 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
+
402
+ If you want to use the clients outside of the block, you can use the `clients` method to get the clients.
403
+
404
+ ```ruby
405
+ clients = RubyLLM::MCP.establish_connection
406
+ chat = RubyLLM.chat(model: "gpt-4")
407
+ chat.with_tools(*clients.tools)
408
+
409
+ response = chat.ask("Hello, world!")
410
+ puts response
411
+ ```
412
+
413
+ However, you will be responsible for closing the connection when you are done with it.
414
+
415
+ ```ruby
416
+ RubyLLM::MCP.close_connection
417
+ ```
418
+
350
419
  ## Client Lifecycle Management
351
420
 
352
421
  You can manage the MCP client connection lifecycle:
@@ -462,6 +531,81 @@ puts result
462
531
  # Result: { status: "success", data: "Processed data" }
463
532
  ```
464
533
 
534
+ ## Client Features
535
+
536
+ 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.
537
+
538
+ ### Roots
539
+
540
+ 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.
541
+
542
+ When roots are configured, the client will:
543
+
544
+ - Expose roots as a supported capability to MCP servers
545
+ - Support dynamic addition and removal of roots during the client lifecycle
546
+ - Fire `notifications/roots/list_changed` events when roots are modified
547
+
548
+ #### Configuration
549
+
550
+ ```ruby
551
+ RubyLLM::MCP.config do |config|
552
+ config.roots = ["to/a/path", Rails.root]
553
+ end
554
+
555
+ client = RubyLLM::MCP::Client.new(...)
556
+ ```
557
+
558
+ #### Usage
559
+
560
+ ```ruby
561
+ # Access current root paths
562
+ client.roots.paths
563
+ # => ["to/a/path", #<Pathname:/to/rails/root/path>]
564
+
565
+ # Add a new root (fires list_changed notification)
566
+ client.roots.add("new/path")
567
+ client.roots.paths
568
+ # => ["to/a/path", #<Pathname:/to/rails/root/path>, "new/path"]
569
+
570
+ # Remove a root (fires list_changed notification)
571
+ client.roots.remove("to/a/path")
572
+ client.roots.paths
573
+ # => [#<Pathname:/to/rails/root/path>, "new/path"]
574
+ ```
575
+
576
+ ### Sampling
577
+
578
+ 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.
579
+
580
+ #### Configuration
581
+
582
+ ```ruby
583
+ RubyLLM::MCP.configure do |config|
584
+ config.sampling.enabled = true
585
+ config.sampling.preferred_model = "gpt-4.1"
586
+
587
+ # Optional: Use a block for dynamic model selection
588
+ config.sampling.preferred_model do |model_preferences|
589
+ model_preferences.hints.first
590
+ end
591
+
592
+ # Optional: Add guards to filter sampling requests
593
+ config.sampling.guard do |sample|
594
+ sample.message.include("Hello")
595
+ end
596
+ end
597
+ ```
598
+
599
+ #### How It Works
600
+
601
+ With the above configuration:
602
+
603
+ - Clients will respond to all incoming sample requests using the specified model (`gpt-4.1`)
604
+ - Sample messages will only be approved if they contain the word "Hello" (when using the guard)
605
+ - The `preferred_model` can be a string or a proc that provides dynamic model selection based on MCP server characteristics
606
+
607
+ 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.
608
+
465
609
  ## Transport Types
466
610
 
467
611
  ### SSE (Server-Sent Events)
@@ -510,6 +654,150 @@ client = RubyLLM::MCP.client(
510
654
  )
511
655
  ```
512
656
 
657
+ ## Creating Custom Transports
658
+
659
+ 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.
660
+
661
+ You can create custom transport implementations to support additional communication protocols or specialized connection methods.
662
+
663
+ ### Transport Registration
664
+
665
+ Register your custom transport with the transport factory:
666
+
667
+ ```ruby
668
+ # Define your custom transport class
669
+ class MyCustomTransport
670
+ # Implementation details...
671
+ end
672
+
673
+ # Register it with the factory
674
+ RubyLLM::MCP::Transport.register_transport(:my_custom, MyCustomTransport)
675
+
676
+ # Now you can use it
677
+ client = RubyLLM::MCP.client(
678
+ name: "custom-server",
679
+ transport_type: :my_custom,
680
+ config: {
681
+ # Your custom configuration
682
+ }
683
+ )
684
+ ```
685
+
686
+ ### Required Interface
687
+
688
+ All transport implementations must implement the following interface:
689
+
690
+ ```ruby
691
+ class MyCustomTransport
692
+ # Initialize the transport
693
+ def initialize(coordinator:, **config)
694
+ @coordinator = coordinator # Uses for communication between the client and the MCP server
695
+ @config = config # Transport-specific configuration
696
+ end
697
+
698
+ # Send a request and optionally wait for response
699
+ # Returns a RubyLLM::MCP::Result object
700
+ # body: the request body
701
+ # add_id: true will add an id to the request
702
+ # wait_for_response: true will wait for a response from the MCP server
703
+ # Returns a RubyLLM::MCP::Result object
704
+ def request(body, add_id: true, wait_for_response: true)
705
+ # Implementation: send request and return result
706
+ data = some_method_to_send_request_and_get_result(body)
707
+ # Use Result object to make working with the protocol easier
708
+ result = RubyLLM::MCP::Result.new(data)
709
+
710
+ # Call the coordinator to process the result
711
+ @coordinator.process_result(result)
712
+ return if result.nil? # Some results are related to notifications and should not be returned to the client, but processed by the coordinator instead
713
+
714
+ # Return the result
715
+ result
716
+ end
717
+
718
+ # Check if transport is alive/connected
719
+ def alive?
720
+ # Implementation: return true if connected
721
+ end
722
+
723
+ # Start the transport connection
724
+ def start
725
+ # Implementation: establish connection
726
+ end
727
+
728
+ # Close the transport connection
729
+ def close
730
+ # Implementation: cleanup and close connection
731
+ end
732
+
733
+ # Set the MCP protocol version, used in some transports to identify the agreed upon protocol version
734
+ def set_protocol_version(version)
735
+ @protocol_version = version
736
+ end
737
+ end
738
+ ```
739
+
740
+ ### The Result Object
741
+
742
+ The `RubyLLM::MCP::Result` class wraps MCP responses and provides convenient methods:
743
+
744
+ ```ruby
745
+ result = transport.request(body)
746
+
747
+ # Core properties
748
+ result.id # Request ID
749
+ result.method # Request method
750
+ result.result # Result data (hash)
751
+ result.params # Request parameters
752
+ result.error # Error data (hash)
753
+ result.session_id # Session ID (if applicable)
754
+
755
+ # Type checking
756
+ result.success? # Has result data
757
+ result.error? # Has error data
758
+ result.notification? # Is a notification
759
+ result.request? # Is a request
760
+ result.response? # Is a response
761
+
762
+ # Specialized methods
763
+ result.tool_success? # Successful tool execution
764
+ result.execution_error? # Tool execution failed
765
+ result.matching_id?(id) # Matches request ID
766
+ result.next_cursor? # Has pagination cursor
767
+
768
+ # Error handling
769
+ result.raise_error! # Raise exception if error
770
+ result.to_error # Convert to Error object
771
+
772
+ # Notifications
773
+ result.notification # Get notification object
774
+ ```
775
+
776
+ ### Error Handling
777
+
778
+ 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.
779
+
780
+ ```ruby
781
+ def request(body, add_id: true, wait_for_response: true)
782
+ begin
783
+ # Send request
784
+ send_request(body)
785
+ rescue SomeConnectionError => e
786
+ # Convert to MCP transport error
787
+ raise RubyLLM::MCP::Errors::TransportError.new(
788
+ message: "Connection failed: #{e.message}",
789
+ error: e
790
+ )
791
+ rescue Timeout::Error => e
792
+ # Convert to MCP timeout error
793
+ raise RubyLLM::MCP::Errors::TimeoutError.new(
794
+ message: "Request timeout after #{@request_timeout}ms",
795
+ request_id: body["id"]
796
+ )
797
+ end
798
+ end
799
+ ```
800
+
513
801
  ## RubyLLM::MCP and Client Configuration Options
514
802
 
515
803
  MCP comes with some common configuration options that can be set on the client.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyLlm
6
+ module Mcp
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Install RubyLLM MCP configuration files"
12
+
13
+ def create_initializer
14
+ template "initializer.rb", "config/initializers/ruby_llm_mcp.rb"
15
+ end
16
+
17
+ def create_config_file
18
+ template "mcps.yml", "config/mcps.yml"
19
+ end
20
+
21
+ def display_readme
22
+ readme "README.txt" if behavior == :invoke
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ RubyLLM MCP has been successfully installed!
2
+
3
+ The following files have been created:
4
+
5
+ config/initializers/ruby_llm_mcp.rb - Main configuration file
6
+ config/mcps.json - MCP servers configuration
7
+
8
+ Next steps:
9
+
10
+ 1. Edit config/initializers/ruby_llm_mcp.rb to configure your MCP settings
11
+ 2. Edit config/mcps.json to define your MCP servers
12
+ 3. Install any MCP servers you want to use (e.g., npm install @modelcontextprotocol/server-filesystem) or use remote MCPs
13
+ 4. Update environment variables for any MCP servers that require authentication
14
+
15
+ Example usage in your Rails application:
16
+
17
+ # With Ruby::MCP installed in a controller or service
18
+ clients = RubyLLM::MCP.clients
19
+
20
+ # Get all tools use the configured client
21
+ tools = RubyLLM::MCP.tools
22
+
23
+ # Or use the configured client
24
+ client = RubyLLM::MCP.clients["file-system"]
25
+
26
+ # Or use the configured client
27
+ tools = client.tools
28
+
29
+
30
+ For more information, visit: https://github.com/patvice/ruby_llm-mcp
31
+
32
+ ===============================================================================
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure RubyLLM MCP
4
+ RubyLLM::MCP.configure do |config|
5
+ # Request timeout in milliseconds
6
+ config.request_timeout = 8000
7
+
8
+ # Maximum connections in the pool
9
+ config.max_connections = Float::INFINITY
10
+
11
+ # Pool timeout in seconds
12
+ config.pool_timeout = 5
13
+
14
+ # Enable complex parameter support for various providers
15
+ config.support_complex_parameters!
16
+
17
+ # Path to MCPs configuration file
18
+ config.config_path = Rails.root.join("config", "mcps.yml")
19
+
20
+ # Launch MCPs (:automatic, :manual)
21
+ config.launch_control = :automatic
22
+
23
+ # Configure roots for file system access
24
+ # config.roots = [
25
+ # Rails.root.to_s
26
+ # ]
27
+
28
+ # Configure sampling (optional)
29
+ config.sampling.enabled = false
30
+
31
+ # Set preferred model for sampling
32
+ # config.sampling.preferred_model do
33
+ # # Return the preferred model name
34
+ # "claude-3-5-sonnet-20240620"
35
+ # end
36
+
37
+ # Set a guard for sampling
38
+ # config.sampling.guard do
39
+ # # Return true to enable sampling, false to disable
40
+ # Rails.env.development?
41
+ # end
42
+ end
@@ -0,0 +1,9 @@
1
+ mcp_servers:
2
+ filesystem:
3
+ transport_type: stdio
4
+ command: npx
5
+ args:
6
+ - "@modelcontextprotocol/server-filesystem"
7
+ - "<%%= Rails.root %>"
8
+ env: {}
9
+ with_prefix: true
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