Skip to content

Commit ff35f49

Browse files
authored
Merge pull request #26 from patvice/errors-and-utilities
v0.4.0 - Errors, MCP utilities and new StreamabeHTTP
2 parents 4617ebb + 160ec6b commit ff35f49

File tree

95 files changed

+6780
-616
lines changed

Some content is hidden

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

95 files changed

+6780
-616
lines changed

.rubocop.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,19 @@ Metrics/BlockLength:
5656
Exclude:
5757
- 'spec/**/*'
5858

59+
5960
RSpec/DescribedClass:
6061
Enabled: false
6162

63+
RSpec/ExampleLength:
64+
Max: 30
65+
66+
6267
RSpec/MultipleExpectations:
6368
Max: 5
6469

6570
RSpec/MultipleMemoizedHelpers:
6671
Max: 15
6772

68-
RSpec/ExampleLength:
69-
Max: 10
70-
7173
RSpec/NestedGroups:
7274
Max: 10

Gemfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ group :development do
1818
gem "rubocop-rspec", ">= 3.6"
1919
gem "simplecov"
2020
gem "vcr"
21-
end
21+
gem "webmock", "~> 3.25"
2222

23-
gem "webmock", "~> 3.25"
23+
# For another MCP server test
24+
gem "fast-mcp"
25+
gem "puma"
26+
gem "rack"
27+
end

README.md

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,39 @@ response = chat.ask("Can you help me search for recent files in my project?")
103103
puts response
104104
```
105105

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+
106132
### Support Complex Parameters
107133
108134
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.
109135
110136
```ruby
111137
RubyLLM::MCP.support_complex_parameters!
112-
```
138+
````
113139

114140
### Streaming Responses with Tool Calls
115141

@@ -341,6 +367,14 @@ client.restart!
341367
client.stop
342368
```
343369

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+
344378
## Refreshing Cached Data
345379

346380
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)
363397
template = client.resource_template("user_logs", refresh: true)
364398
```
365399

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+
366465
## Transport Types
367466

368467
### SSE (Server-Sent Events)
@@ -411,7 +510,27 @@ client = RubyLLM::MCP.client(
411510
)
412511
```
413512

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.
415534

416535
- `name`: A unique identifier for your MCP client
417536
- `transport_type`: Either `:sse`, `:streamable`, or `:stdio`

Rakefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require "bundler/gem_tasks"
44
require "rspec/core/rake_task"
55

6+
Dir.glob("lib/tasks/*.rake").each { |file| load file }
7+
68
RSpec::Core::RakeTask.new(:spec)
79

810
require "rubocop/rake_task"

examples/tools/local_mcp.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
config.openai_api_key = ENV.fetch("OPENAI_API_KEY", nil)
1212
end
1313

14-
RubyLLM::MCP.support_complex_parameters!
14+
RubyLLM::MCP.configure do |config|
15+
config.support_complex_parameters!
16+
config.log_level = Logger::ERROR
17+
end
1518

1619
# Test with filesystem MCP server using stdio transport
1720
client = RubyLLM::MCP.client(
@@ -31,17 +34,16 @@
3134
puts "Connected to filesystem MCP server"
3235
puts "Available tools:"
3336
tools = client.tools
34-
puts tools.map(&:name).join("\n")
37+
puts tools.map { |tool| "#{tool.display_name}: #{tool.description}" }.join("\n")
3538
puts "-" * 50
3639

37-
tool = client.tool("firecrawl_crawl")
38-
chat = RubyLLM.chat(model: "gpt-4.1")
39-
chat.with_tool(tool)
40-
41-
message = "Can you crawl the website http://www.fullscript.com and tell me what fullscript does?"
40+
message = "Can you list the files in the current directly and give me a summary of the contents of each file?"
4241
puts "Asking: #{message}"
4342
puts "-" * 50
4443

44+
chat = RubyLLM.chat(model: "gpt-4.1")
45+
chat.with_tools(*tools)
46+
4547
chat.ask(message) do |chunk|
4648
if chunk.tool_call?
4749
chunk.tool_calls.each do |key, tool_call|

examples/utilities/progress.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
require "bundler/setup"
4+
require "ruby_llm/mcp"
5+
require "debug"
6+
require "dotenv"
7+
8+
Dotenv.load
9+
10+
RubyLLM.configure do |config|
11+
config.openai_api_key = ENV.fetch("OPENAI_API_KEY", nil)
12+
end
13+
14+
RubyLLM::MCP.configure do |config|
15+
config.support_complex_parameters!
16+
config.log_level = Logger::ERROR
17+
end
18+
19+
# Test with streamable HTTP transport
20+
client = RubyLLM::MCP.client(
21+
name: "streamable_mcp",
22+
transport_type: :streamable,
23+
config: {
24+
url: "http://localhost:3005/mcp"
25+
}
26+
)
27+
28+
puts "Connected to streamable MCP server"
29+
client.on_progress do |progress|
30+
puts "Progress: #{progress.progress}%"
31+
end
32+
33+
result = client.tool("progress").execute(operation: "processing", steps: 3)
34+
puts "Result: #{result}"

lib/ruby_llm/mcp.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
loader.inflector.inflect("mcp" => "MCP")
99
loader.inflector.inflect("sse" => "SSE")
1010
loader.inflector.inflect("openai" => "OpenAI")
11+
loader.inflector.inflect("streamable_http" => "StreamableHTTP")
12+
1113
loader.setup
14+
loader.eager_load
1215

1316
module RubyLLM
1417
module MCP
@@ -23,5 +26,20 @@ def support_complex_parameters!
2326
require_relative "mcp/providers/anthropic/complex_parameter_support"
2427
require_relative "mcp/providers/gemini/complex_parameter_support"
2528
end
29+
30+
def configure
31+
yield config
32+
end
33+
34+
def config
35+
@config ||= Configuration.new
36+
end
37+
38+
alias configuration config
39+
module_function :configuration
40+
41+
def logger
42+
config.logger
43+
end
2644
end
2745
end

lib/ruby_llm/mcp/capabilities.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,41 @@ def initialize(capabilities = {})
99
@capabilities = capabilities
1010
end
1111

12-
def resources_list_changed?
12+
def resources_list?
13+
!@capabilities["resources"].nil?
14+
end
15+
16+
def resources_list_changes?
1317
@capabilities.dig("resources", "listChanged") || false
1418
end
1519

1620
def resource_subscribe?
1721
@capabilities.dig("resources", "subscribe") || false
1822
end
1923

20-
def tools_list_changed?
24+
def tools_list?
25+
!@capabilities["tools"].nil?
26+
end
27+
28+
def tools_list_changes?
2129
@capabilities.dig("tools", "listChanged") || false
2230
end
2331

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+
2440
def completion?
2541
!@capabilities["completions"].nil?
2642
end
43+
44+
def logging?
45+
!@capabilities["logging"].nil?
46+
end
2747
end
2848
end
2949
end

0 commit comments

Comments
 (0)