Skip to content

Commit 511647f

Browse files
committed
Merge remote-tracking branch 'origin/main' into list-pagination-support
2 parents ef5244d + 29f57ef commit 511647f

File tree

53 files changed

+2599
-185
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2599
-185
lines changed

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This project is a Ruby client for the [Model Context Protocol (MCP)](https://mod
1212
- 🛠️ **Tool Integration**: Automatically converts MCP tools into RubyLLM-compatible tools
1313
- 📄 **Resource Management**: Access and include MCP resources (files, data) and resource templates in conversations
1414
- 🎯 **Prompt Integration**: Use predefined MCP prompts with arguments for consistent interactions
15+
- 🎛️ **Client Features**: Support for sampling and roots
1516
- 🎨 **Enhanced Chat Interface**: Extended RubyLLM chat methods for seamless MCP integration
1617
- 📚 **Simple API**: Easy-to-use interface that integrates seamlessly with RubyLLM
1718

@@ -462,6 +463,81 @@ puts result
462463
# Result: { status: "success", data: "Processed data" }
463464
```
464465

466+
## Client Features
467+
468+
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.
469+
470+
### Roots
471+
472+
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.
473+
474+
When roots are configured, the client will:
475+
476+
- Expose roots as a supported capability to MCP servers
477+
- Support dynamic addition and removal of roots during the client lifecycle
478+
- Fire `notifications/roots/list_changed` events when roots are modified
479+
480+
#### Configuration
481+
482+
```ruby
483+
RubyLLM::MCP.config do |config|
484+
config.roots = ["to/a/path", Rails.root]
485+
end
486+
487+
client = RubyLLM::MCP::Client.new(...)
488+
```
489+
490+
#### Usage
491+
492+
```ruby
493+
# Access current root paths
494+
client.roots.paths
495+
# => ["to/a/path", #<Pathname:/to/rails/root/path>]
496+
497+
# Add a new root (fires list_changed notification)
498+
client.roots.add("new/path")
499+
client.roots.paths
500+
# => ["to/a/path", #<Pathname:/to/rails/root/path>, "new/path"]
501+
502+
# Remove a root (fires list_changed notification)
503+
client.roots.remove("to/a/path")
504+
client.roots.paths
505+
# => [#<Pathname:/to/rails/root/path>, "new/path"]
506+
```
507+
508+
### Sampling
509+
510+
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.
511+
512+
#### Configuration
513+
514+
```ruby
515+
RubyLLM::MCP.configure do |config|
516+
config.sampling.enabled = true
517+
config.sampling.preferred_model = "gpt-4.1"
518+
519+
# Optional: Use a block for dynamic model selection
520+
config.sampling.preferred_model do |model_preferences|
521+
model_preferences.hints.first
522+
end
523+
524+
# Optional: Add guards to filter sampling requests
525+
config.sampling.guard do |sample|
526+
sample.message.include("Hello")
527+
end
528+
end
529+
```
530+
531+
#### How It Works
532+
533+
With the above configuration:
534+
535+
- Clients will respond to all incoming sample requests using the specified model (`gpt-4.1`)
536+
- Sample messages will only be approved if they contain the word "Hello" (when using the guard)
537+
- The `preferred_model` can be a string or a proc that provides dynamic model selection based on MCP server characteristics
538+
539+
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.
540+
465541
## Transport Types
466542

467543
### SSE (Server-Sent Events)

lib/ruby_llm/chat.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

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.
45
module RubyLLM
56
class Chat
67
def with_resources(*resources, **args)

lib/ruby_llm/mcp.rb

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@
44
require "zeitwerk"
55
require_relative "chat"
66

7-
loader = Zeitwerk::Loader.for_gem_extension(RubyLLM)
8-
loader.inflector.inflect("mcp" => "MCP")
9-
loader.inflector.inflect("sse" => "SSE")
10-
loader.inflector.inflect("openai" => "OpenAI")
11-
loader.inflector.inflect("streamable_http" => "StreamableHTTP")
12-
13-
loader.setup
14-
loader.eager_load
15-
167
module RubyLLM
178
module MCP
189
module_function
@@ -43,3 +34,11 @@ def logger
4334
end
4435
end
4536
end
37+
38+
loader = Zeitwerk::Loader.for_gem_extension(RubyLLM)
39+
loader.inflector.inflect("mcp" => "MCP")
40+
loader.inflector.inflect("sse" => "SSE")
41+
loader.inflector.inflect("openai" => "OpenAI")
42+
loader.inflector.inflect("streamable_http" => "StreamableHTTP")
43+
44+
loader.setup

lib/ruby_llm/mcp/client.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module MCP
77
class Client
88
extend Forwardable
99

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
1111

1212
def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
1313
@name = name
@@ -25,10 +25,13 @@ def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.
2525

2626
@log_level = nil
2727

28+
setup_roots
29+
setup_sampling
30+
2831
@coordinator.start_transport if start
2932
end
3033

31-
def_delegators :@coordinator, :alive?, :capabilities, :ping
34+
def_delegators :@coordinator, :alive?, :capabilities, :ping, :client_capabilities
3235

3336
def start
3437
@coordinator.start_transport
@@ -165,6 +168,15 @@ def on_logging(level: Logging::WARNING, logger: nil, &block)
165168
self
166169
end
167170

171+
def sampling_callback_enabled?
172+
@on.key?(:sampling) && !@on[:sampling].nil?
173+
end
174+
175+
def on_sampling(&block)
176+
@on[:sampling] = block
177+
self
178+
end
179+
168180
private
169181

170182
def setup_coordinator
@@ -187,6 +199,14 @@ def build_map(raw_data, klass)
187199
acc[instance.name] = instance
188200
end
189201
end
202+
203+
def setup_roots
204+
@roots = Roots.new(paths: MCP.config.roots, coordinator: @coordinator)
205+
end
206+
207+
def setup_sampling
208+
@on[:sampling] = MCP.config.sampling.guard
209+
end
190210
end
191211
end
192212
end

lib/ruby_llm/mcp/configuration.rb

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,48 @@
33
module RubyLLM
44
module MCP
55
class Configuration
6-
attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters
6+
class Sampling
7+
attr_accessor :enabled
8+
attr_writer :preferred_model
9+
10+
def initialize
11+
set_defaults
12+
end
13+
14+
def reset!
15+
set_defaults
16+
end
17+
18+
def guard(&block)
19+
@guard = block if block_given?
20+
@guard
21+
end
22+
23+
def preferred_model(&block)
24+
@preferred_model = block if block_given?
25+
@preferred_model
26+
end
27+
28+
def enabled?
29+
@enabled
30+
end
31+
32+
private
33+
34+
def set_defaults
35+
@enabled = false
36+
@preferred_model = nil
37+
@guard = nil
38+
end
39+
end
40+
41+
attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters, :roots, :sampling
742
attr_writer :logger
843

944
REQUEST_TIMEOUT_DEFAULT = 8000
1045

1146
def initialize
47+
@sampling = Sampling.new
1248
set_defaults
1349
end
1450

@@ -60,6 +96,9 @@ def set_defaults
6096
@log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
6197
@has_support_complex_parameters = false
6298
@logger = nil
99+
@roots = []
100+
101+
@sampling.reset!
63102
end
64103
end
65104
end

0 commit comments

Comments
 (0)