Skip to content

Commit c0ef3fe

Browse files
authored
Merge pull request #39 from patvice/better-mcp-interface-rails-support
Better MCP module interface rails support
2 parents 441512c + 9530f19 commit c0ef3fe

39 files changed

+1869
-267
lines changed

.github/workflows/cicd.yml

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,48 +34,3 @@ jobs:
3434
run: bundle exec rake
3535
env:
3636
GITHUB_ACTIONS: "true"
37-
38-
39-
release:
40-
name: Release Gem to RubyGems
41-
needs: test
42-
if: github.ref == 'refs/heads/main' && success()
43-
runs-on: ubuntu-latest
44-
45-
steps:
46-
- uses: actions/checkout@v4
47-
48-
- name: Set up Ruby
49-
uses: ruby/setup-ruby@v1
50-
with:
51-
ruby-version: '3.4'
52-
bundler-cache: true
53-
54-
- name: Build the gem
55-
run: |
56-
gem build *.gemspec
57-
gem_name=$(ls *.gem | head -n1)
58-
echo "GEM_FILE=$gem_name" >> $GITHUB_ENV
59-
60-
- name: Check if gem version already exists
61-
id: check_version
62-
run: |
63-
gem_name=$(basename $GEM_FILE .gem)
64-
name=$(echo $gem_name | cut -d'-' -f1)
65-
version=$(echo $gem_name | cut -d'-' -f2-)
66-
echo "Gem: $name, Version: $version"
67-
68-
if gem list ^$name$ -r -a | grep -q $version; then
69-
echo "Gem version $version already exists on RubyGems."
70-
echo "already_published=true" >> $GITHUB_OUTPUT
71-
else
72-
echo "Gem version $version is new."
73-
echo "already_published=false" >> $GITHUB_OUTPUT
74-
fi
75-
76-
- name: Push to RubyGems
77-
if: steps.check_version.outputs.already_published == 'false'
78-
env:
79-
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
80-
run: |
81-
gem push "$GEM_FILE"

README.md

Lines changed: 211 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# RubyLLM::MCP
22

3-
Aiming to make using MCP with RubyLLM as easy as possible.
3+
Aiming to make using MCPs with RubyLLM as easy as possible.
44

55
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.
66

77
**Note:** This project is still under development and the API is subject to change.
88

99
## Features
1010

11-
- 🔌 **Multiple Transport Types**: Support for SSE (Server-Sent Events), Streamable HTTP, and stdio transports
11+
- 🔌 **Multiple Transport Types**: Streamable HTTP, and STDIO and legacy SSE transports
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
@@ -123,21 +123,19 @@ puts result # 3
123123
# If the human in the loop returns false, the tool call will be cancelled
124124
result = tool.execute(a: 2, b: 2)
125125
puts result # Tool execution error: Tool call was cancelled by the client
126-
```
127126

128127
tool = client.tool("add")
129128
result = tool.execute(a: 1, b: 2)
130129
puts result
131-
132-
````
130+
```
133131

134132
### Support Complex Parameters
135133

136134
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.
137135

138136
```ruby
139137
RubyLLM::MCP.support_complex_parameters!
140-
````
138+
```
141139

142140
### Streaming Responses with Tool Calls
143141

@@ -338,6 +336,69 @@ chat.with_prompt(prompt, arguments: { name: "Alice" })
338336
response = chat.ask_prompt(prompt, arguments: { name: "Alice" })
339337
```
340338

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+
341402
## Client Lifecycle Management
342403

343404
You can manage the MCP client connection lifecycle:
@@ -576,6 +637,150 @@ client = RubyLLM::MCP.client(
576637
)
577638
```
578639

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+
579784
## RubyLLM::MCP and Client Configuration Options
580785

581786
MCP comes with some common configuration options that can be set on the client.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
require "rails/generators/base"
4+
5+
module RubyLlm
6+
module Mcp
7+
module Generators
8+
class InstallGenerator < Rails::Generators::Base
9+
source_root File.expand_path("templates", __dir__)
10+
11+
desc "Install RubyLLM MCP configuration files"
12+
13+
def create_initializer
14+
template "initializer.rb", "config/initializers/ruby_llm_mcp.rb"
15+
end
16+
17+
def create_config_file
18+
template "mcps.yml", "config/mcps.yml"
19+
end
20+
21+
def display_readme
22+
readme "README.txt" if behavior == :invoke
23+
end
24+
end
25+
end
26+
end
27+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
RubyLLM MCP has been successfully installed!
2+
3+
The following files have been created:
4+
5+
config/initializers/ruby_llm_mcp.rb - Main configuration file
6+
config/mcps.json - MCP servers configuration
7+
8+
Next steps:
9+
10+
1. Edit config/initializers/ruby_llm_mcp.rb to configure your MCP settings
11+
2. Edit config/mcps.json to define your MCP servers
12+
3. Install any MCP servers you want to use (e.g., npm install @modelcontextprotocol/server-filesystem) or use remote MCPs
13+
4. Update environment variables for any MCP servers that require authentication
14+
15+
Example usage in your Rails application:
16+
17+
# With Ruby::MCP installed in a controller or service
18+
clients = RubyLLM::MCP.clients
19+
20+
# Get all tools use the configured client
21+
tools = RubyLLM::MCP.tools
22+
23+
# Or use the configured client
24+
client = RubyLLM::MCP.clients["file-system"]
25+
26+
# Or use the configured client
27+
tools = client.tools
28+
29+
30+
For more information, visit: https://github.com/patvice/ruby_llm-mcp
31+
32+
===============================================================================

0 commit comments

Comments
 (0)