ruby_llm-mcp 0.3.1 → 0.4.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 +121 -2
- data/lib/ruby_llm/mcp/capabilities.rb +22 -2
- data/lib/ruby_llm/mcp/client.rb +106 -18
- data/lib/ruby_llm/mcp/configuration.rb +66 -0
- data/lib/ruby_llm/mcp/coordinator.rb +197 -33
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +37 -4
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/parameter.rb +2 -0
- data/lib/ruby_llm/mcp/progress.rb +33 -0
- data/lib/ruby_llm/mcp/prompt.rb +12 -5
- data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +5 -2
- data/lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb +6 -3
- data/lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb +6 -3
- data/lib/ruby_llm/mcp/requests/base.rb +3 -3
- data/lib/ruby_llm/mcp/requests/cancelled_notification.rb +32 -0
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +3 -3
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +3 -3
- data/lib/ruby_llm/mcp/requests/initialization.rb +24 -18
- data/lib/ruby_llm/mcp/requests/initialize_notification.rb +15 -9
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
- data/lib/ruby_llm/mcp/requests/meta.rb +30 -0
- data/lib/ruby_llm/mcp/requests/ping.rb +20 -0
- data/lib/ruby_llm/mcp/requests/ping_response.rb +28 -0
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +3 -3
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +1 -1
- data/lib/ruby_llm/mcp/requests/resource_list.rb +1 -1
- data/lib/ruby_llm/mcp/requests/resource_read.rb +4 -4
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +1 -1
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
- data/lib/ruby_llm/mcp/requests/tool_call.rb +6 -3
- data/lib/ruby_llm/mcp/requests/tool_list.rb +17 -11
- data/lib/ruby_llm/mcp/resource.rb +26 -5
- data/lib/ruby_llm/mcp/resource_template.rb +11 -6
- data/lib/ruby_llm/mcp/result.rb +90 -0
- data/lib/ruby_llm/mcp/tool.rb +28 -3
- data/lib/ruby_llm/mcp/transport/sse.rb +81 -75
- data/lib/ruby_llm/mcp/transport/stdio.rb +33 -17
- data/lib/ruby_llm/mcp/transport/streamable_http.rb +647 -0
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +18 -0
- data/lib/tasks/release.rake +23 -0
- metadata +20 -50
- data/lib/ruby_llm/mcp/transport/streamable.rb +0 -299
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd0be50f01aa4fd126828c9c4f81d2e5a7b3cb35f4b32a4ed8e3e4deb731202
|
4
|
+
data.tar.gz: d6efbfbb3345544da2e201968ff26ff1e238a72efc95e7eb5cc2cb77adaaaf94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 769aa222a4f5afd5dc124f1fa1bb62771fd2d2dd5d60211fa3f86e664aa030811600f7ee448ec7607a7d244d2d6f961a9ded088f3db7eaa611fff8041528b6c1
|
7
|
+
data.tar.gz: d3a494430dcce6c0ee9246f6378d682771458c0009779c3e7f34aba43309b2ba6b72722ddbbc9e8d2b84061358cc8375f5fedd19c76a187795fce5adfd49338b
|
data/README.md
CHANGED
@@ -103,13 +103,39 @@ response = chat.ask("Can you help me search for recent files in my project?")
|
|
103
103
|
puts response
|
104
104
|
```
|
105
105
|
|
106
|
+
### Human in the Loop
|
107
|
+
|
108
|
+
You can use the `on_human_in_the_loop` callback to allow the human to intervene in the tool call. This is useful for tools that require human input or programic input to verify if the tool should be executed.
|
109
|
+
|
110
|
+
For tool calls that have access to do important operations, there SHOULD always be a human in the loop with the ability to deny tool invocations.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
client.on_human_in_the_loop do |name, params|
|
114
|
+
name == "add" && params[:a] == 1 && params[:b] == 2
|
115
|
+
end
|
116
|
+
|
117
|
+
tool = client.tool("add")
|
118
|
+
result = tool.execute(a: 1, b: 2)
|
119
|
+
puts result # 3
|
120
|
+
|
121
|
+
# If the human in the loop returns false, the tool call will be cancelled
|
122
|
+
result = tool.execute(a: 2, b: 2)
|
123
|
+
puts result # Tool execution error: Tool call was cancelled by the client
|
124
|
+
```
|
125
|
+
|
126
|
+
tool = client.tool("add")
|
127
|
+
result = tool.execute(a: 1, b: 2)
|
128
|
+
puts result
|
129
|
+
|
130
|
+
````
|
131
|
+
|
106
132
|
### Support Complex Parameters
|
107
133
|
|
108
134
|
If you want to support complex parameters, like an array of objects it currently requires a patch to RubyLLM itself. This is planned to be temporary until the RubyLLM is updated.
|
109
135
|
|
110
136
|
```ruby
|
111
137
|
RubyLLM::MCP.support_complex_parameters!
|
112
|
-
|
138
|
+
````
|
113
139
|
|
114
140
|
### Streaming Responses with Tool Calls
|
115
141
|
|
@@ -341,6 +367,14 @@ client.restart!
|
|
341
367
|
client.stop
|
342
368
|
```
|
343
369
|
|
370
|
+
### Ping
|
371
|
+
|
372
|
+
You can ping the MCP server to check if it is alive:
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
client.ping # => true or false
|
376
|
+
```
|
377
|
+
|
344
378
|
## Refreshing Cached Data
|
345
379
|
|
346
380
|
The client caches tools, resources, prompts, and resource templates list calls are cached to reduce round trips back to the MCP server. You can refresh this cache:
|
@@ -363,6 +397,71 @@ prompt = client.prompt("daily_greeting", refresh: true)
|
|
363
397
|
template = client.resource_template("user_logs", refresh: true)
|
364
398
|
```
|
365
399
|
|
400
|
+
## Notifications
|
401
|
+
|
402
|
+
MCPs can produce notifications that happen in an async nature outside normal calls to the MCP server.
|
403
|
+
|
404
|
+
### Subscribing to a Resource Update
|
405
|
+
|
406
|
+
By default, the client will look for any resource cha to resource updates and refresh the resource content when it changes.
|
407
|
+
|
408
|
+
### Logging Notifications
|
409
|
+
|
410
|
+
MCPs can produce logging notifications for long-running tool operations. Logging notifications allow tools to send real-time updates about their execution status.
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
client.on_logging do |logging|
|
414
|
+
puts "Logging: #{logging.level} - #{logging.message}"
|
415
|
+
end
|
416
|
+
|
417
|
+
# Execute a tool that supports logging notifications
|
418
|
+
tool = client.tool("long_running_operation")
|
419
|
+
result = tool.execute(operation: "data_processing")
|
420
|
+
|
421
|
+
# Logging: info - Processing data...
|
422
|
+
# Logging: info - Processing data...
|
423
|
+
# Logging: warning - Something went wrong but not major...
|
424
|
+
```
|
425
|
+
|
426
|
+
Different levels of logging are supported:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
client.on_logging(RubyLLM::MCP::Logging::WARNING) do |logging|
|
430
|
+
puts "Logging: #{logging.level} - #{logging.message}"
|
431
|
+
end
|
432
|
+
|
433
|
+
# Execute a tool that supports logging notifications
|
434
|
+
tool = client.tool("long_running_operation")
|
435
|
+
result = tool.execute(operation: "data_processing")
|
436
|
+
|
437
|
+
# Logging: warning - Something went wrong but not major...
|
438
|
+
```
|
439
|
+
|
440
|
+
### Progress Notifications
|
441
|
+
|
442
|
+
MCPs can produce progress notifications for long-running tool operations. Progress notifications allow tools to send real-time updates about their execution status.
|
443
|
+
|
444
|
+
**Note:** that we only support progress notifications for tool calls today.
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
# Set up progress tracking
|
448
|
+
client.on_progress do |progress|
|
449
|
+
puts "Progress: #{progress.progress}% - #{progress.message}"
|
450
|
+
end
|
451
|
+
|
452
|
+
# Execute a tool that supports progress notifications
|
453
|
+
tool = client.tool("long_running_operation")
|
454
|
+
result = tool.execute(operation: "data_processing")
|
455
|
+
|
456
|
+
# Progress 25% - Processing data...
|
457
|
+
# Progress 50% - Processing data...
|
458
|
+
# Progress 75% - Processing data...
|
459
|
+
# Progress 100% - Processing data...
|
460
|
+
puts result
|
461
|
+
|
462
|
+
# Result: { status: "success", data: "Processed data" }
|
463
|
+
```
|
464
|
+
|
366
465
|
## Transport Types
|
367
466
|
|
368
467
|
### SSE (Server-Sent Events)
|
@@ -411,7 +510,27 @@ client = RubyLLM::MCP.client(
|
|
411
510
|
)
|
412
511
|
```
|
413
512
|
|
414
|
-
## Configuration Options
|
513
|
+
## RubyLLM::MCP and Client Configuration Options
|
514
|
+
|
515
|
+
MCP comes with some common configuration options that can be set on the client.
|
516
|
+
|
517
|
+
```ruby
|
518
|
+
RubyLLM::MCP.configure do |config|
|
519
|
+
# Set the progress handler
|
520
|
+
config.support_complex_parameters!
|
521
|
+
|
522
|
+
# Set parameters on the built in logger
|
523
|
+
config.log_file = $stdout
|
524
|
+
config.log_level = Logger::ERROR
|
525
|
+
|
526
|
+
# Or add a custom logger
|
527
|
+
config.logger = Logger.new(STDOUT)
|
528
|
+
end
|
529
|
+
```
|
530
|
+
|
531
|
+
### MCP Client Options
|
532
|
+
|
533
|
+
MCP client options are set on the client itself.
|
415
534
|
|
416
535
|
- `name`: A unique identifier for your MCP client
|
417
536
|
- `transport_type`: Either `:sse`, `:streamable`, or `:stdio`
|
@@ -9,7 +9,11 @@ module RubyLLM
|
|
9
9
|
@capabilities = capabilities
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
12
|
+
def resources_list?
|
13
|
+
!@capabilities["resources"].nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def resources_list_changes?
|
13
17
|
@capabilities.dig("resources", "listChanged") || false
|
14
18
|
end
|
15
19
|
|
@@ -17,13 +21,29 @@ module RubyLLM
|
|
17
21
|
@capabilities.dig("resources", "subscribe") || false
|
18
22
|
end
|
19
23
|
|
20
|
-
def
|
24
|
+
def tools_list?
|
25
|
+
!@capabilities["tools"].nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def tools_list_changes?
|
21
29
|
@capabilities.dig("tools", "listChanged") || false
|
22
30
|
end
|
23
31
|
|
32
|
+
def prompt_list?
|
33
|
+
!@capabilities["prompts"].nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def prompt_list_changes?
|
37
|
+
@capabilities.dig("prompts", "listChanged") || false
|
38
|
+
end
|
39
|
+
|
24
40
|
def completion?
|
25
41
|
!@capabilities["completions"].nil?
|
26
42
|
end
|
43
|
+
|
44
|
+
def logging?
|
45
|
+
!@capabilities["logging"].nil?
|
46
|
+
end
|
27
47
|
end
|
28
48
|
end
|
29
49
|
end
|
data/lib/ruby_llm/mcp/client.rb
CHANGED
@@ -7,29 +7,47 @@ module RubyLLM
|
|
7
7
|
class Client
|
8
8
|
extend Forwardable
|
9
9
|
|
10
|
-
attr_reader :name, :config, :transport_type, :request_timeout
|
10
|
+
attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on
|
11
11
|
|
12
|
-
def initialize(name:, transport_type:, start: true, request_timeout:
|
12
|
+
def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
|
13
13
|
@name = name
|
14
14
|
@config = config.merge(request_timeout: request_timeout)
|
15
15
|
@transport_type = transport_type.to_sym
|
16
16
|
@request_timeout = request_timeout
|
17
17
|
|
18
|
-
@coordinator =
|
18
|
+
@coordinator = setup_coordinator
|
19
19
|
|
20
|
-
|
20
|
+
@on = {}
|
21
|
+
@tools = {}
|
22
|
+
@resources = {}
|
23
|
+
@resource_templates = {}
|
24
|
+
@prompts = {}
|
25
|
+
|
26
|
+
@log_level = nil
|
27
|
+
|
28
|
+
@coordinator.start_transport if start
|
21
29
|
end
|
22
30
|
|
23
|
-
def_delegators :@coordinator, :
|
31
|
+
def_delegators :@coordinator, :alive?, :capabilities, :ping
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
33
|
+
def start
|
34
|
+
@coordinator.start_transport
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
@coordinator.stop_transport
|
39
|
+
end
|
40
|
+
|
41
|
+
def restart!
|
42
|
+
@coordinator.restart_transport
|
43
|
+
end
|
28
44
|
|
29
45
|
def tools(refresh: false)
|
46
|
+
return [] unless capabilities.tools_list?
|
47
|
+
|
30
48
|
fetch(:tools, refresh) do
|
31
|
-
|
32
|
-
build_map(
|
49
|
+
tools = @coordinator.tool_list
|
50
|
+
build_map(tools, MCP::Tool)
|
33
51
|
end
|
34
52
|
|
35
53
|
@tools.values
|
@@ -41,10 +59,16 @@ module RubyLLM
|
|
41
59
|
@tools[name]
|
42
60
|
end
|
43
61
|
|
62
|
+
def reset_tools!
|
63
|
+
@tools = {}
|
64
|
+
end
|
65
|
+
|
44
66
|
def resources(refresh: false)
|
67
|
+
return [] unless capabilities.resources_list?
|
68
|
+
|
45
69
|
fetch(:resources, refresh) do
|
46
|
-
|
47
|
-
build_map(
|
70
|
+
resources = @coordinator.resource_list
|
71
|
+
build_map(resources, MCP::Resource)
|
48
72
|
end
|
49
73
|
|
50
74
|
@resources.values
|
@@ -56,10 +80,16 @@ module RubyLLM
|
|
56
80
|
@resources[name]
|
57
81
|
end
|
58
82
|
|
83
|
+
def reset_resources!
|
84
|
+
@resources = {}
|
85
|
+
end
|
86
|
+
|
59
87
|
def resource_templates(refresh: false)
|
88
|
+
return [] unless capabilities.resources_list?
|
89
|
+
|
60
90
|
fetch(:resource_templates, refresh) do
|
61
|
-
|
62
|
-
build_map(
|
91
|
+
resource_templates = @coordinator.resource_template_list
|
92
|
+
build_map(resource_templates, MCP::ResourceTemplate)
|
63
93
|
end
|
64
94
|
|
65
95
|
@resource_templates.values
|
@@ -71,10 +101,16 @@ module RubyLLM
|
|
71
101
|
@resource_templates[name]
|
72
102
|
end
|
73
103
|
|
104
|
+
def reset_resource_templates!
|
105
|
+
@resource_templates = {}
|
106
|
+
end
|
107
|
+
|
74
108
|
def prompts(refresh: false)
|
109
|
+
return [] unless capabilities.prompt_list?
|
110
|
+
|
75
111
|
fetch(:prompts, refresh) do
|
76
|
-
|
77
|
-
build_map(
|
112
|
+
prompts = @coordinator.prompt_list
|
113
|
+
build_map(prompts, MCP::Prompt)
|
78
114
|
end
|
79
115
|
|
80
116
|
@prompts.values
|
@@ -86,11 +122,63 @@ module RubyLLM
|
|
86
122
|
@prompts[name]
|
87
123
|
end
|
88
124
|
|
125
|
+
def reset_prompts!
|
126
|
+
@prompts = {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def tracking_progress?
|
130
|
+
@on.key?(:progress) && !@on[:progress].nil?
|
131
|
+
end
|
132
|
+
|
133
|
+
def on_progress(&block)
|
134
|
+
@on[:progress] = block
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
def human_in_the_loop?
|
139
|
+
@on.key?(:human_in_the_loop) && !@on[:human_in_the_loop].nil?
|
140
|
+
end
|
141
|
+
|
142
|
+
def on_human_in_the_loop(&block)
|
143
|
+
@on[:human_in_the_loop] = block
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
def logging_handler_enabled?
|
148
|
+
@on.key?(:logging) && !@on[:logging].nil?
|
149
|
+
end
|
150
|
+
|
151
|
+
def logging_enabled?
|
152
|
+
!@log_level.nil?
|
153
|
+
end
|
154
|
+
|
155
|
+
def on_logging(level: Logging::WARNING, logger: nil, &block)
|
156
|
+
@coordinator.set_logging(level: level)
|
157
|
+
|
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
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
89
168
|
private
|
90
169
|
|
170
|
+
def setup_coordinator
|
171
|
+
Coordinator.new(self,
|
172
|
+
transport_type: @transport_type,
|
173
|
+
config: @config)
|
174
|
+
end
|
175
|
+
|
91
176
|
def fetch(cache_key, refresh)
|
92
|
-
instance_variable_set("@#{cache_key}",
|
93
|
-
instance_variable_get("@#{cache_key}")
|
177
|
+
instance_variable_set("@#{cache_key}", {}) if refresh
|
178
|
+
if instance_variable_get("@#{cache_key}").empty?
|
179
|
+
instance_variable_set("@#{cache_key}", yield)
|
180
|
+
end
|
181
|
+
instance_variable_get("@#{cache_key}")
|
94
182
|
end
|
95
183
|
|
96
184
|
def build_map(raw_data, klass)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters
|
7
|
+
attr_writer :logger
|
8
|
+
|
9
|
+
REQUEST_TIMEOUT_DEFAULT = 8000
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset!
|
16
|
+
set_defaults
|
17
|
+
end
|
18
|
+
|
19
|
+
def support_complex_parameters!
|
20
|
+
return if @has_support_complex_parameters
|
21
|
+
|
22
|
+
@has_support_complex_parameters = true
|
23
|
+
RubyLLM::MCP.support_complex_parameters!
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger ||= Logger.new(
|
28
|
+
log_file,
|
29
|
+
progname: "RubyLLM::MCP",
|
30
|
+
level: log_level
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
redacted = lambda do |name, value|
|
36
|
+
if name.match?(/_id|_key|_secret|_token$/)
|
37
|
+
value.nil? ? "nil" : "[FILTERED]"
|
38
|
+
else
|
39
|
+
value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
inspection = instance_variables.map do |ivar|
|
44
|
+
name = ivar.to_s.delete_prefix("@")
|
45
|
+
value = redacted[name, instance_variable_get(ivar)]
|
46
|
+
"#{name}: #{value}"
|
47
|
+
end.join(", ")
|
48
|
+
|
49
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def set_defaults
|
55
|
+
# Connection configuration
|
56
|
+
@request_timeout = REQUEST_TIMEOUT_DEFAULT
|
57
|
+
|
58
|
+
# Logging configuration
|
59
|
+
@log_file = $stdout
|
60
|
+
@log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
|
61
|
+
@has_support_complex_parameters = false
|
62
|
+
@logger = nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|