Skip to content

Commit 5f69499

Browse files
authored
Merge pull request #18 from patvice/complete-spec-refactor
Bump to v0.3 - Complete Spec Refactor!
2 parents d19c1be + a5430fe commit 5f69499

File tree

170 files changed

+15343
-408
lines changed

Some content is hidden

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

170 files changed

+15343
-408
lines changed

.github/workflows/main.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,27 @@ name: Ruby
33
on:
44
push:
55
branches:
6-
- master
7-
6+
- main
87
pull_request:
98

109
jobs:
11-
build:
10+
test:
1211
runs-on: ubuntu-latest
1312
name: Ruby ${{ matrix.ruby }}
1413
strategy:
1514
matrix:
16-
ruby:
17-
- '3.3.3'
15+
ruby: ['3.1.3', '3.2', '3.3', '3.4']
1816

1917
steps:
2018
- uses: actions/checkout@v4
19+
- uses: oven-sh/setup-bun@v2
20+
with:
21+
bun-version: 1.2.16
2122
- name: Set up Ruby
2223
uses: ruby/setup-ruby@v1
2324
with:
2425
ruby-version: ${{ matrix.ruby }}
2526
bundler-cache: true
27+
- run: bundle install
2628
- name: Run the default task
2729
run: bundle exec rake

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
.DS_Store
1616
.ruby-version
1717
Gemfile.lock
18+
19+
node_modules/

.rubocop.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,9 @@ RSpec/MultipleExpectations:
6464

6565
RSpec/MultipleMemoizedHelpers:
6666
Max: 15
67+
68+
RSpec/ExampleLength:
69+
Max: 10
70+
71+
RSpec/NestedGroups:
72+
Max: 10

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ group :development do
1616
gem "rubocop", ">= 1.76"
1717
gem "rubocop-rake", ">= 0.7"
1818
gem "rubocop-rspec", ">= 3.6"
19+
gem "simplecov"
20+
gem "vcr"
1921
end
22+
23+
gem "webmock", "~> 3.25"

README.md

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ 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-
- 🔄 **Real-time Communication**: Efficient bidirectional communication with MCP servers
1615
- 🎨 **Enhanced Chat Interface**: Extended RubyLLM chat methods for seamless MCP integration
1716
- 📚 **Simple API**: Easy-to-use interface that integrates seamlessly with RubyLLM
1817

1918
## Installation
2019

21-
Add this line to your application's Gemfile:
20+
```bash
21+
bundle add ruby_llm-mcp
22+
```
23+
24+
or add this line to your application's Gemfile:
2225

2326
```ruby
2427
gem 'ruby_llm-mcp'
@@ -152,12 +155,12 @@ MCP servers can provide access to resources - structured data that can be includ
152155
# Get available resources from the MCP server
153156
resources = client.resources
154157
puts "Available resources:"
155-
resources.each do |name, resource|
156-
puts "- #{name}: #{resource.description}"
158+
resources.each do |resource|
159+
puts "- #{resource.name}: #{resource.description}"
157160
end
158161

159-
# Access a specific resource
160-
file_resource = resources["project_readme"]
162+
# Access a specific resource by name
163+
file_resource = client.resource("project_readme")
161164
content = file_resource.content
162165
puts "Resource content: #{content}"
163166

@@ -179,11 +182,11 @@ Resource templates are parameterized resources that can be dynamically configure
179182
```ruby
180183
# Get available resource templates
181184
templates = client.resource_templates
182-
log_template = templates["application_logs"]
185+
log_template = client.resource_template("application_logs")
183186

184187
# Use a template with parameters
185188
chat = RubyLLM.chat(model: "gpt-4")
186-
chat.with_resource(log_template, arguments: {
189+
chat.with_resource_template(log_template, arguments: {
187190
date: "2024-01-15",
188191
level: "error"
189192
})
@@ -192,7 +195,7 @@ response = chat.ask("What errors occurred on this date?")
192195
puts response
193196

194197
# You can also get templated content directly
195-
content = log_template.content(arguments: {
198+
content = log_template.to_content(arguments: {
196199
date: "2024-01-15",
197200
level: "error"
198201
})
@@ -204,12 +207,12 @@ puts content
204207
For resource templates, you can get suggested values for arguments:
205208

206209
```ruby
207-
template = client.resource_templates["user_profile"]
210+
template = client.resource_template("user_profile")
208211

209212
# Search for possible values for a specific argument
210-
suggestions = template.arguments_search("username", "john")
213+
suggestions = template.complete("username", "john")
211214
puts "Suggested usernames:"
212-
suggestions.arg_values.each do |value|
215+
suggestions.values.each do |value|
213216
puts "- #{value}"
214217
end
215218
puts "Total matches: #{suggestions.total}"
@@ -224,15 +227,15 @@ MCP servers can provide predefined prompts that can be used in conversations:
224227
# Get available prompts from the MCP server
225228
prompts = client.prompts
226229
puts "Available prompts:"
227-
prompts.each do |name, prompt|
228-
puts "- #{name}: #{prompt.description}"
230+
prompts.each do |prompt|
231+
puts "- #{prompt.name}: #{prompt.description}"
229232
prompt.arguments.each do |arg|
230233
puts " - #{arg.name}: #{arg.description} (required: #{arg.required})"
231234
end
232235
end
233236

234237
# Use a prompt in a conversation
235-
greeting_prompt = prompts["daily_greeting"]
238+
greeting_prompt = client.prompt("daily_greeting")
236239
chat = RubyLLM.chat(model: "gpt-4")
237240

238241
# Method 1: Ask prompt directly
@@ -261,15 +264,15 @@ chat = RubyLLM.chat(model: "gpt-4")
261264
chat.with_tools(*client.tools)
262265

263266
# Add resources for context
264-
chat.with_resource(client.resources["project_structure"])
267+
chat.with_resource(client.resource("project_structure"))
265268
chat.with_resource(
266-
client.resource_templates["recent_commits"],
269+
client.resource_template("recent_commits"),
267270
arguments: { days: 7 }
268271
)
269272

270273
# Add prompts for guidance
271274
chat.with_prompt(
272-
client.prompts["code_review_checklist"],
275+
client.prompt("code_review_checklist"),
273276
arguments: { focus: "security" }
274277
)
275278

@@ -278,6 +281,88 @@ response = chat.ask("Please review the recent commits using the checklist and su
278281
puts response
279282
```
280283

284+
## Argument Completion
285+
286+
Some MCP servers support argument completion for prompts and resource templates:
287+
288+
```ruby
289+
# For prompts
290+
prompt = client.prompt("user_search")
291+
suggestions = prompt.complete("username", "jo")
292+
puts "Suggestions: #{suggestions.values}" # ["john", "joanna", "joseph"]
293+
294+
# For resource templates
295+
template = client.resource_template("user_logs")
296+
suggestions = template.complete("user_id", "123")
297+
puts "Total matches: #{suggestions.total}"
298+
puts "Has more results: #{suggestions.has_more}"
299+
```
300+
301+
## Additional Chat Methods
302+
303+
The gem extends RubyLLM's chat interface with convenient methods for MCP integration:
304+
305+
```ruby
306+
chat = RubyLLM.chat(model: "gpt-4")
307+
308+
# Add a single resource
309+
chat.with_resource(resource)
310+
311+
# Add multiple resources
312+
chat.with_resources(resource1, resource2, resource3)
313+
314+
# Add a resource template with arguments
315+
chat.with_resource_template(resource_template, arguments: { key: "value" })
316+
317+
# Add a prompt with arguments
318+
chat.with_prompt(prompt, arguments: { name: "Alice" })
319+
320+
# Ask using a prompt directly
321+
response = chat.ask_prompt(prompt, arguments: { name: "Alice" })
322+
```
323+
324+
## Client Lifecycle Management
325+
326+
You can manage the MCP client connection lifecycle:
327+
328+
```ruby
329+
client = RubyLLM::MCP.client(name: "my-server", transport_type: :stdio, start: false, config: {...})
330+
331+
# Manually start the connection
332+
client.start
333+
334+
# Check if connection is alive
335+
puts client.alive?
336+
337+
# Restart the connection
338+
client.restart!
339+
340+
# Stop the connection
341+
client.stop
342+
```
343+
344+
## Refreshing Cached Data
345+
346+
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:
347+
348+
```ruby
349+
# Refresh all cached tools
350+
tools = client.tools(refresh: true)
351+
352+
# Refresh a specific tool
353+
tool = client.tool("search_files", refresh: true)
354+
355+
# Same pattern works for resources, prompts, and resource templates
356+
resources = client.resources(refresh: true)
357+
prompts = client.prompts(refresh: true)
358+
templates = client.resource_templates(refresh: true)
359+
360+
# Or refresh specific items
361+
resource = client.resource("project_readme", refresh: true)
362+
prompt = client.prompt("daily_greeting", refresh: true)
363+
template = client.resource_template("user_logs", refresh: true)
364+
```
365+
281366
## Transport Types
282367

283368
### SSE (Server-Sent Events)
@@ -289,7 +374,8 @@ client = RubyLLM::MCP.client(
289374
name: "web-mcp-server",
290375
transport_type: :sse,
291376
config: {
292-
url: "https://your-mcp-server.com/mcp/sse"
377+
url: "https://your-mcp-server.com/mcp/sse",
378+
headers: { "Authorization" => "Bearer your-token" }
293379
}
294380
)
295381
```
@@ -329,23 +415,27 @@ client = RubyLLM::MCP.client(
329415

330416
- `name`: A unique identifier for your MCP client
331417
- `transport_type`: Either `:sse`, `:streamable`, or `:stdio`
418+
- `start`: Whether to automatically start the connection (default: true)
332419
- `request_timeout`: Timeout for requests in milliseconds (default: 8000)
333420
- `config`: Transport-specific configuration
334-
- For SSE: `{ url: "http://..." }`
421+
- For SSE: `{ url: "http://...", headers: {...} }`
335422
- For Streamable: `{ url: "http://...", headers: {...} }`
336423
- For stdio: `{ command: "...", args: [...], env: {...} }`
337424

338425
## Development
339426

340427
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
341428

342-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
429+
To install this gem onto your local machine, run `bundle exec rake install`. Run `bundle exec rake` to test specs and run linters. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
343430

344431
## Examples
345432

346433
Check out the `examples/` directory for more detailed usage examples:
347434

348-
- `examples/test_local_mcp.rb` - Complete example with SSE transport
435+
- `examples/tools/local_mcp.rb` - Complete example with stdio transport
436+
- `examples/tools/sse_mcp_with_gpt.rb` - Example using SSE transport with GPT
437+
- `examples/resources/list_resources.rb` - Example of listing and using resources
438+
- `examples/prompts/streamable_prompt_call.rb` - Example of using prompts with streamable transport
349439

350440
## Contributing
351441

examples/tools/local_mcp.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
name: "filesystem",
1919
transport_type: :stdio,
2020
config: {
21-
command: "npx",
21+
command: "bunx",
2222
args: [
2323
"@modelcontextprotocol/server-filesystem",
2424
File.expand_path("..", __dir__) # Allow access to the current directory
@@ -31,13 +31,14 @@
3131
puts "Connected to filesystem MCP server"
3232
puts "Available tools:"
3333
tools = client.tools
34-
puts tools.map { |tool| " - #{tool.name}: #{tool.description}" }.join("\n")
34+
puts tools.map(&:name).join("\n")
3535
puts "-" * 50
3636

37+
tool = client.tool("firecrawl_crawl")
3738
chat = RubyLLM.chat(model: "gpt-4.1")
38-
chat.with_tools(*client.tools)
39+
chat.with_tool(tool)
3940

40-
message = "Can you list the files in the current directory and tell me what's in the README file if it exists?"
41+
message = "Can you crawl the website http://www.fullscript.com and tell me what fullscript does?"
4142
puts "Asking: #{message}"
4243
puts "-" * 50
4344

lib/ruby_llm/chat.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ def with_resources(*resources, **args)
1010
self
1111
end
1212

13-
def with_resource(resource, **args)
14-
resource.include(self, **args)
13+
def with_resource(resource)
14+
resource.include(self)
15+
self
16+
end
17+
18+
def with_resource_template(resource_template, arguments: {})
19+
resource = resource_template.fetch_resource(arguments: arguments)
20+
resource.include(self)
1521
self
1622
end
1723

lib/ruby_llm/mcp/capabilities.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
module RubyLLM
44
module MCP
55
class Capabilities
6-
attr_reader :capabilities
6+
attr_accessor :capabilities
77

8-
def initialize(capabilities)
8+
def initialize(capabilities = {})
99
@capabilities = capabilities
1010
end
1111

@@ -22,7 +22,7 @@ def tools_list_changed?
2222
end
2323

2424
def completion?
25-
@capabilities["completion"].present?
25+
!@capabilities["completions"].nil?
2626
end
2727
end
2828
end

0 commit comments

Comments
 (0)