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.
- checksums.yaml +4 -4
- data/README.md +296 -25
- data/lib/ruby_llm/chat.rb +2 -1
- data/lib/ruby_llm/mcp/client.rb +32 -13
- data/lib/ruby_llm/mcp/configuration.rb +123 -3
- data/lib/ruby_llm/mcp/coordinator.rb +108 -115
- data/lib/ruby_llm/mcp/errors.rb +3 -1
- data/lib/ruby_llm/mcp/notification_handler.rb +84 -0
- data/lib/ruby_llm/mcp/{requests/cancelled_notification.rb → notifications/cancelled.rb} +2 -2
- data/lib/ruby_llm/mcp/{requests/initialize_notification.rb → notifications/initialize.rb} +7 -3
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
- data/lib/ruby_llm/mcp/parameter.rb +19 -1
- data/lib/ruby_llm/mcp/progress.rb +3 -1
- data/lib/ruby_llm/mcp/prompt.rb +18 -0
- data/lib/ruby_llm/mcp/railtie.rb +20 -0
- data/lib/ruby_llm/mcp/requests/initialization.rb +8 -4
- data/lib/ruby_llm/mcp/requests/ping.rb +6 -2
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +10 -2
- data/lib/ruby_llm/mcp/requests/resource_list.rb +12 -2
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +12 -2
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
- data/lib/ruby_llm/mcp/requests/tool_call.rb +1 -1
- data/lib/ruby_llm/mcp/requests/tool_list.rb +10 -2
- data/lib/ruby_llm/mcp/resource.rb +17 -0
- data/lib/ruby_llm/mcp/response_handler.rb +58 -0
- data/lib/ruby_llm/mcp/responses/error.rb +33 -0
- data/lib/ruby_llm/mcp/{requests/ping_response.rb → responses/ping.rb} +2 -2
- data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
- data/lib/ruby_llm/mcp/result.rb +21 -8
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +148 -0
- data/lib/ruby_llm/mcp/{capabilities.rb → server_capabilities.rb} +1 -1
- data/lib/ruby_llm/mcp/tool.rb +35 -4
- data/lib/ruby_llm/mcp/transport.rb +58 -0
- data/lib/ruby_llm/mcp/transports/http_client.rb +26 -0
- data/lib/ruby_llm/mcp/{transport → transports}/sse.rb +25 -24
- data/lib/ruby_llm/mcp/{transport → transports}/stdio.rb +28 -26
- data/lib/ruby_llm/mcp/{transport → transports}/streamable_http.rb +25 -29
- data/lib/ruby_llm/mcp/transports/timeout.rb +32 -0
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +50 -9
- metadata +23 -12
- data/lib/ruby_llm/mcp/requests/base.rb +0 -31
- data/lib/ruby_llm/mcp/requests/meta.rb +0 -30
- data/lib/tasks/release.rake +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 328e09780647e7ef9a35aac8a8fd8e0b1c84aded38239b4ad9b08803e1d638d3
|
4
|
+
data.tar.gz: f0f1022e6917f56b95ecfd4540fb517245f6b5450f9369d4cee2cc34e2eb0934
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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**:
|
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
|
-
|
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
|
-
|
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
|
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)
|
data/lib/ruby_llm/mcp/client.rb
CHANGED
@@ -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,
|
159
|
+
def on_logging(level: Logging::WARNING, &block)
|
156
160
|
@coordinator.set_logging(level: level)
|
157
161
|
|
158
|
-
@on[:logging] =
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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 =
|
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
|
-
|
7
|
-
|
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
|