Skip to content

Commit 8559e43

Browse files
authored
Feature/active record storage (#4)
* Added ActiveRecord storage adapter with basic configuration support * Add ActiveRecord storage adapter Complete implementation of ActiveRecord storage backend Auto-creation of necessary database tables with indexes Support for different data types (text, binary, JSON) Proper handling of metadata with symbol keys * Rubocop Fix * feat: add binary content handling test for ActiveRecord storage adapter * fix: update ActiveRecord spec to expect ASCII-8BIT encoding for binary content * feat: add list_content method and improve error handling in ActiveRecord storage * refactor: improve table existence check with error handling * Rubocop Fix * feat: fix RuboCop issues in ActiveRecord storage adapter * Rubocop Fix * Version 0.3.0 release * test: improve test clarity and add teardown for database cleanup * test: add additional test cases for ActiveRecord storage reliability * refactor: Improve error handling and logging in ActiveRecord storage backend * refactor: improve error handling for context and content retrieval * chore: suppress error logs during ActiveRecord spec tests * test: Clean up and enhance ActiveRecord storage tests for clarity and coverage * fix: close begin block for LoadError handling in active_record_spec.rb * fix: resolve RSpec failures by cleaning up duplicate test blocks * fix: active record specs * Added active_record storage demo script & updated README * Update README.md
1 parent 529f8db commit 8559e43

File tree

14 files changed

+1273
-48
lines changed

14 files changed

+1273
-48
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## [0.3.0] - 2023-05-01
4+
5+
### Added
6+
- ActiveRecord storage backend for database persistence
7+
- Support for Rails integration with ActiveRecord storage
8+
- Auto-creation of database tables with configurable prefixes
9+
- Proper handling of different data types (text, binary, JSON)
10+
- Symbolization of hash keys for consistent API
11+
- Comprehensive test suite for ActiveRecord storage
12+
13+
### Changed
14+
- Enhanced `StorageFactory` to support ActiveRecord backend
15+
- Updated configuration system with ActiveRecord options
16+
- Improved documentation with ActiveRecord storage examples
17+
318
## [0.2.0] - 2025-04-21
419

520
### Added

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ gem 'rake', '~> 13.0'
1010
gem 'rspec', '~> 3.0'
1111

1212
gem 'rubocop', '~> 1.21'
13+
14+
gem 'activerecord' # For database functionality
15+
gem 'dotenv' # For loading environment variables
16+
gem 'sqlite3' # For SQLite database support

README.md

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@
88
[![Test](https://github.com/nagstler/ruby_mcp/actions/workflows/test.yml/badge.svg)](https://github.com/nagstler/ruby_mcp/actions/workflows/test.yml)
99
[![codecov](https://codecov.io/github/nagstler/ruby_mcp/graph/badge.svg?token=SG4EJEIHW3)](https://codecov.io/github/nagstler/ruby_mcp)
1010

11-
<strong> 🚀 **Supercharge your Rails APIs**</strong>
12-
<p>
13-
Turn your Rails application into an MCP server with just a few lines of code.
14-
</p>
11+
<strong> **Turn your Rails APIs into an MCP server.**</strong>
12+
1513
</div>
1614

1715
## 🔍 Introduction
18-
19-
The [Model Context Protocol](https://modelcontextprotocol.io) provides a standardized way for applications to interact with language models. Similar to how REST standardized web APIs, MCP creates a consistent interface for working with providers like OpenAI and Anthropic.
16+
The [Model Context Protocol](https://modelcontextprotocol.io) standardizes how applications interact with AI models, serving as the "REST for LLMs." **MCP on Ruby** brings this standard to the Ruby ecosystem. Create contexts, manage conversations, connect to multiple providers, and handle streaming responses with clean, Ruby code.
2017

2118
![System Component Flow (Horizontal)](https://github.com/user-attachments/assets/085ad9b8-bee0-4d60-a4b7-ecf02d07f53c)
2219

@@ -39,7 +36,7 @@ The [Model Context Protocol](https://modelcontextprotocol.io) provides a standar
3936
- [Uploading Content](#uploading-content)
4037
- [Using Tool Calls](#using-tool-calls)
4138
- [🚄 Rails Integration](#-rails-integration)
42-
- [💾 Custom Storage Backend](#-storage-backends)
39+
- [💾 Storage Backend](#-storage-backends)
4340
- [🔒 Authentication](#-authentication)
4441
- [🛠️ Development](#️-development)
4542
- [🗺️ Roadmap](#️-roadmap)
@@ -113,6 +110,11 @@ ruby server.rb
113110
# Terminal 2: Run the client
114111
cd examples/simple_server
115112
ruby client.rb
113+
114+
# ActiveRecord Storage Demo
115+
# Demonstrates database storage with SQLite
116+
cd examples/simple_server
117+
ruby activerecord_demo.rb
116118
```
117119

118120
This demo provides a guided tour of the MCP functionality, showing each step of creating contexts, adding messages, and generating responses with detailed explanations.
@@ -134,9 +136,31 @@ RubyMCP.configure do |config|
134136
}
135137
}
136138

137-
# Storage backend (:memory, :redis, :active_record, or custom)
139+
# Storage backend
140+
141+
# Option 1: Memory storage (default)
138142
config.storage = :memory
139143

144+
# Option 2: Redis storage
145+
config.storage = :redis
146+
config.redis = {
147+
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
148+
namespace: 'my_app_mcp',
149+
ttl: 86400 # 1 day in seconds
150+
}
151+
152+
# Option 3: ActiveRecord storage
153+
config.storage = :active_record
154+
config.active_record = {
155+
# Connection settings (not needed in Rails)
156+
connection: {
157+
adapter: 'sqlite3',
158+
database: 'db/mcp.sqlite3'
159+
},
160+
# Table prefix to avoid name collisions
161+
table_prefix: 'mcp_'
162+
}
163+
140164
# Server settings
141165
config.server_port = 3000
142166
config.server_host = "0.0.0.0"
@@ -334,7 +358,11 @@ RubyMCP.configure do |config|
334358
if Rails.env.development? || Rails.env.test?
335359
config.storage = :memory
336360
else
337-
config.storage = :memory # Replace with persistent option when implemented
361+
# Use ActiveRecord for production (uses your Rails database)
362+
config.storage = :active_record
363+
config.active_record = {
364+
table_prefix: "mcp_#{Rails.env}_" # Environment-specific prefix
365+
}
338366
end
339367

340368
# Enable authentication in production
@@ -393,6 +421,43 @@ MCP on Ruby supports Redis as a persistent storage backend:
393421

394422
For detailed integration examples, see the [[Redis Storage](https://github.com/nagstler/mcp_on_ruby/wiki/Redis-Storage)] wiki page.
395423

424+
### ActiveRecord Storage
425+
426+
For integration with Rails or any app needing database storage:
427+
428+
```ruby
429+
# Add to Gemfile
430+
gem 'activerecord', '~> 6.1'
431+
gem 'sqlite3', '~> 1.4' # or pg, mysql2, etc.
432+
433+
# Configure RubyMCP
434+
RubyMCP.configure do |config|
435+
config.storage = :active_record
436+
config.active_record = {
437+
# Connection (not needed in Rails)
438+
connection: {
439+
adapter: 'sqlite3',
440+
database: 'db/mcp.sqlite3'
441+
},
442+
# Table prefix to avoid name collisions
443+
table_prefix: 'mcp_'
444+
}
445+
end
446+
```
447+
448+
In Rails applications, it uses your app's database connection automatically:
449+
450+
```ruby
451+
# config/initializers/ruby_mcp.rb
452+
RubyMCP.configure do |config|
453+
config.storage = :active_record
454+
config.active_record = {
455+
table_prefix: "mcp_#{Rails.env}_" # Environment-specific prefix
456+
}
457+
end
458+
```
459+
460+
The ActiveRecord adapter automatically creates the necessary tables with appropriate indexes, and handles different types of data (text, binary, JSON) appropriately.
396461

397462
### Custom storage
398463
You can implement custom storage backends by extending the base storage class:
@@ -500,7 +565,7 @@ bundle exec ruby examples/simple_server/server.rb
500565
While RubyMCP is functional for basic use cases, there are several areas planned for improvement:
501566

502567
- [x] Redis persistent storage backend
503-
- [ ] ActiveRecord storage backend
568+
- [x] ActiveRecord storage backend
504569
- [ ] Complete test coverage, including integration tests
505570
- [ ] Improved error handling and recovery strategies
506571
- [ ] Rate limiting for provider APIs
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# frozen_string_literal: true
2+
3+
# This example demonstrates how to use ActiveRecord storage with RubyMCP in a Rails application
4+
5+
# Step 1: Add gems to your Gemfile
6+
# gem 'mcp_on_ruby', '~> 0.3.0'
7+
# gem 'activerecord', '~> 6.1' # Usually already included in Rails
8+
# gem 'sqlite3', '~> 1.4' # Or your preferred database adapter
9+
10+
###############################################
11+
# Step 2: Create an initializer
12+
# config/initializers/ruby_mcp.rb
13+
###############################################
14+
15+
require 'ruby_mcp'
16+
17+
Rails.application.config.to_prepare do
18+
RubyMCP.configure do |config|
19+
# Configure LLM providers
20+
config.providers = {
21+
openai: { api_key: ENV['OPENAI_API_KEY'] },
22+
anthropic: { api_key: ENV['ANTHROPIC_API_KEY'] }
23+
}
24+
25+
# Use ActiveRecord storage with Rails database
26+
config.storage = :active_record
27+
config.active_record = {
28+
# Uses Rails database connection automatically
29+
table_prefix: "mcp_#{Rails.env}_" # Environment-specific prefix
30+
}
31+
32+
# Enable authentication in production
33+
if Rails.env.production?
34+
config.auth_required = true
35+
config.jwt_secret = ENV['JWT_SECRET']
36+
end
37+
end
38+
39+
# Log configuration
40+
Rails.logger.info "Configured RubyMCP with providers: #{RubyMCP.configuration.providers.keys.join(', ')}"
41+
Rails.logger.info "Using ActiveRecord storage with prefix: #{RubyMCP.configuration.active_record[:table_prefix]}"
42+
end
43+
44+
###############################################
45+
# Step 3: Mount the MCP server in routes.rb
46+
# config/routes.rb
47+
###############################################
48+
49+
Rails.application.routes.draw do
50+
# Mount RubyMCP at /api/mcp
51+
mount_mcp_at = '/api/mcp'
52+
53+
Rails.application.config.middleware.use Rack::Config do |env|
54+
env['SCRIPT_NAME'] = mount_mcp_at if env['PATH_INFO'].start_with?(mount_mcp_at)
55+
end
56+
57+
mount RubyMCP::Server::App.new.rack_app, at: mount_mcp_at
58+
59+
# Rest of your routes...
60+
end
61+
62+
###############################################
63+
# Step 4: (Optional) Create a migration
64+
# This is only needed if you prefer migration-based setup
65+
# instead of automatic table creation
66+
###############################################
67+
68+
# Run: rails generate migration CreateMcpTables
69+
# Then edit the migration file:
70+
71+
class CreateMcpTables < ActiveRecord::Migration[6.1]
72+
def change
73+
prefix = "mcp_#{Rails.env}_"
74+
75+
create_table "#{prefix}contexts" do |t|
76+
t.string :external_id, null: false
77+
t.text :metadata, default: '{}'
78+
t.timestamps
79+
80+
t.index :external_id, unique: true
81+
t.index :updated_at
82+
end
83+
84+
create_table "#{prefix}messages" do |t|
85+
t.references :context, null: false, foreign_key: { to_table: "#{prefix}contexts", on_delete: :cascade }
86+
t.string :external_id, null: false
87+
t.string :role, null: false
88+
t.text :content, null: false
89+
t.text :metadata, default: '{}'
90+
t.timestamps
91+
92+
t.index %i[context_id external_id], unique: true
93+
end
94+
95+
create_table "#{prefix}contents" do |t|
96+
t.references :context, null: false, foreign_key: { to_table: "#{prefix}contexts", on_delete: :cascade }
97+
t.string :external_id, null: false
98+
t.binary :data_binary
99+
t.text :data_json
100+
t.string :content_type
101+
t.timestamps
102+
103+
t.index %i[context_id external_id], unique: true
104+
end
105+
end
106+
end
107+
108+
###############################################
109+
# Step 5: Using RubyMCP with ActiveRecord in your application
110+
###############################################
111+
112+
# app/controllers/conversations_controller.rb
113+
class ConversationsController < ApplicationController
114+
def create
115+
# Create a new MCP context
116+
client = RubyMCP.client
117+
118+
# Create a context with a system message
119+
context = client.create_context(
120+
[{ role: 'system', content: 'You are a helpful assistant.' }],
121+
{ user_id: current_user.id } # Store metadata
122+
)
123+
124+
# Store the context ID in your database
125+
conversation = current_user.conversations.create(
126+
title: 'New Conversation',
127+
mcp_context_id: context.id
128+
)
129+
130+
redirect_to conversation_path(conversation)
131+
end
132+
133+
def show
134+
@conversation = current_user.conversations.find(params[:id])
135+
136+
# Get the MCP context
137+
client = RubyMCP.client
138+
@context = client.get_context(@conversation.mcp_context_id)
139+
140+
# Render the conversation UI
141+
end
142+
143+
def message
144+
@conversation = current_user.conversations.find(params[:id])
145+
146+
# Add user message to context
147+
client = RubyMCP.client
148+
client.add_message(
149+
@conversation.mcp_context_id,
150+
'user',
151+
params[:content]
152+
)
153+
154+
# Generate AI response
155+
response = client.generate(
156+
@conversation.mcp_context_id,
157+
'openai/gpt-4',
158+
temperature: 0.7
159+
)
160+
161+
# Response is automatically added to the context
162+
163+
# Return the response
164+
render json: { content: response[:content] }
165+
end
166+
end

0 commit comments

Comments
 (0)