Active MCP
[](https://badge.fury.io/rb/active_mcp) [](LICENSE) [](https://rubyonrails.org/) A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)๐ Table of Contents
- ๐ Table of Contents
- โจ Features
- ๐ฆ Installation
- ๐ Setup
- ๐ MCP Connection Methods
- ๐ Rails Generators
- ๐งฐ Creating MCP Tools
- ๐ Input Schema
- ๐ Authorization & Authentication
- ๐ฆ MCP Resources
- ๐ฆ MCP Resource Templates
- ๐ฌ MCP Prompts
- ๐ก Best Practices
- ๐งช Development
- ๐ฅ Contributing
- ๐ License
โจ Features
- Simple Integration: Easily expose Rails functionality as MCP tools
- Resource Support: Share files and data with AI assistants through MCP resources
- Powerful Generators: Quickly scaffold MCP tools and resources with Rails generators
- Authentication Support: Built-in authentication and authorization capabilities
- Flexible Configuration: Multiple deployment and connection options
๐ฆ Installation
Add this line to your application's Gemfile:
gem 'active_mcp'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install active_mcp
๐ Setup
- Initialize
The easiest way to set up Active MCP in your Rails application is to use the install generator:
$ rails generate active_mcp:install
This generator will create a configuration initializer at config/initializers/active_mcp.rb
- Create a tool by inheriting from
ActiveMcp::Tool::Base:
$ rails generate active_mcp:tool create_note
class CreateNoteTool < ActiveMcp::Tool::Base
tool_name "create_note"
description "Create Note"
argument :title, :string, required: true
argument :content, :string, required: true
argument :published_at, :string, required: true,
visible: ->(context) { context[:role] == "admin" }
def call(title:, content:, context:)
note = Note.create(title: title, content: content)
[{ type: "text", text: "Created note with ID: #{note.id}" }]
end
end
- Create schema for your application:
class MySchema < ActiveMcp::Schema::Base
tools CreateNoteTool
end
- Create controller ans set up routing:
class MyMcpController < ActiveMcp::BaseController
private
def schema
MySchema.new(context:)
end
end
Rails.application.routes.draw do
post "/mcp", to: "my_mcp#index"
# Your other routes
end
๐ MCP Connection Methods
Active MCP supports two connection methods:
1. Direct HTTP Connection
Set your MCP client to connect directly to your Rails application:
https://your-app.example.com/mcp
2. Standalone MCP Server
Start a dedicated MCP server that communicates with your Rails app:
# script/mcp_server.rb
server = ActiveMcp::Server.new(
name: "My App MCP Server",
uri: 'https://your-app.example.com/mcp'
)
server.start
Then configure your MCP client:
{
"mcpServers": {
"my-rails-app": {
"command": "/path/to/ruby",
"args": ["/path/to/script/mcp_server.rb"]
}
}
}
๐ Rails Generators
Active MCP provides generators to help you quickly set up and extend your MCP integration:
Install Generator
Initialize Active MCP in your Rails application:
$ rails generate active_mcp:install
Tool Generator
Create new MCP tools quickly:
$ rails generate active_mcp:tool search_users
This creates a new tool file at app/mcp/tools/search_users_tool.rb with ready-to-customize starter code.
Resource Generator
Generate new MCP resources to share data with AI:
$ rails generate active_mcp:resource profile_image
This creates a new resource file at app/mcp/resources/profile_image_resource.rb that you can customize to provide various types of content to AI assistants.
๐งฐ Creating MCP Tools
MCP tools are Ruby classes that inherit from ActiveMcp::Tool::Base and define an interface for AI to interact with your application:
class SearchUsersTool < ActiveMcp::Tool::Base
tool_name "Search Users"
description 'Search users by criteria'
argument :email, :string, required: false, description: 'Email to search for'
argument :name, :string, required: false, description: 'Name to search for'
argument :limit, :integer, required: false, description: 'Maximum number of records to return'
def call(email: nil, name: nil, limit: 10, context: {})
criteria = {}
criteria[:email] = email if email.present?
criteria[:name] = name if name.present?
users = User.where(criteria).limit(limit)
[{ type: "text", text: users.attributes.to_json }]
end
end
๐ Input Schema
Define arguments for your tools using the argument method:
argument :name, :string, required: true, description: 'User name'
argument :age, :integer, required: false, description: 'User age'
argument :addresses, :array, required: false, description: 'User addresses'
argument :preferences, :object, required: false, description: 'User preferences'
Supported types:
| Type | Description |
|---|---|
:string |
Text values |
:integer |
Whole numbers |
:number |
Decimal numbers (float/decimal) |
:boolean |
True/false values |
:array |
Lists of values |
:object |
Hash/dictionary structures |
:null |
Null values |
๐ Authorization & Authentication
Authorization for Tools
Control access to tools by overriding the visible? class method:
class AdminOnlyTool < ActiveMcp::Tool::Base
tool_name "admin_only_tool"
description "Admin-only tool"
argument :command, :string, required: true, description: "Admin command"
# Only allow admins to access this tool
def self.visible?(context:)
return false unless context
return false unless context[:auth_info][:type] == :bearer
# Check if the token belongs to an admin
context[:auth_info] == "admin-token" || User.find_by_token(context[:auth_info])&.admin?
end
def call(command:, context: {})
# Tool implementation
end
end
Authentication Options
1. Server Configuration
server = ActiveMcp::Server.new(
name: "My Secure MCP Server",
uri: 'http://localhost:3000/mcp',
auth: {
type: :bearer,
token: ENV['MCP_AUTH_TOKEN']
}
)
server.start
2. Token Verification in Tools
def call(resource_id:, context: {})
# Check if authentication is provided
unless context[:auth_info].present?
raise "Authentication required"
end
# Verify the token
user = User.authenticate_with_token(context[:auth_info][:token])
unless user
raise "Invalid authentication token"
end
# Proceed with authenticated operation
# ...
end
๐ฆ MCP Resources
MCP Resources allow you to share data and files with AI assistants. Resources have a URI, MIME type, and can return either text or binary data.
Creating Resources
Resources are Ruby classes **Resource:
class UserResource < ActiveMcp::Resource::Base
mime_type "application/json"
def initialize(id:)
@user = User.find(id)
end
def resource_name
@user.name
end
def uri
"data://localhost/users/#{@user.id}"
end
def description
@user.profile
end
def self.visible?(context:)
# Your logic...
end
def text
# Return JSON data
{
id: @user.id,
name: @user.name,
email: @user.email,
created_at: @user.created_at
}
end
end
class MySchema < ActiveMcp::Schema::Base
resource UserResource, items: User.all.each do |user|
{ id: user.id }
end
end
Resource Types
Resources can return two types of content:
- Text Content - Use the
textmethod to return structured data:
def text
# Return strings, arrays, hashes, or any JSON-serializable object
{ items: Product.all.map(&:attributes) }
end
- Binary Content - Use the
blobmethod to return binary files:
class ImageResource < ActiveMcp::Resource::Base
mime_type "image/png"
def resource_name
"profile_image"
end
def uri
"data://localhost/image"
end
def description
"Profile image"
end
def blob
# Return binary file content
File.read(Rails.root.join("public", "profile.png"))
end
end
Resources can be protected using the same authorization mechanism as tools:
def visible?(context: {})
return false unless context
return false unless context[:auth_info][:type] == :bearer
# Check if the token belongs to an admin
User.find_by_token(context[:auth_info][:token])&.admin?
end
๐ฆ MCP Resource Templates
MCP Resource Teamplates allow you to define template of resources.
Creating Resource Templates
Resource teamplates are Ruby classes **Resource:
class UserResource < ActiveMcp::Resource::Base
resource_template_name "users"
uri_template "data://localhost/users/{id}"
mime_type "application/json"
description "This is a test."
argument :id, complete: ->(value, context) do
User.all.pluck(:id).filter { _1.match(value) }
end
def self.visible?(context:)
# Your logic...
end
def initialize(id:)
@user = User.find(id)
end
def resource_name
@user.name
end
def description
@user.profile
end
def uri
"data://localhost/users/#{@user.name}"
end
def text
{ name: @user.name }
end
end
class MySchema < ActiveMcp::Schema::Base
resource UserResource, items: User.all.each do |user|
{ id: user.id }
end
end
๐ฌ MCP Prompts
MCP Prompts allow you to define prompt set.
Creating Prompt
Resources are Ruby classes **Prompt:
class HelloPrompt < ActiveMcp::Prompt::Base
prompt_name "hello"
description "This is a test."
argument :name, required: true, description: "User name", complete: ->(value, context) do
User.all.pluck(:name).filter { _1.match(value) }
end
def self.visible?(context:)
# Your logic...
end
def (name:)
[
ActiveMcp::Message::Text.new(
role: "user",
text: "Hello! #{name}"
),
ActiveMcp::Message::Image.new(
role: "assistant",
data: File.read(file),
mime_type: "image/png"
),
ActiveMcp::Message::Audio.new(
role: "user",
data: File.read(file),
mime_type: "audio/mpeg"
),
ActiveMcp::Message::Resource.new(
role: "assistant",
resource: UserResource.new(name: @name)
)
]
end
end
class MySchema < ActiveMcp::Schema::Base
prompt HelloPrompt
end
๐ก Best Practices
1. Create Specific Tool Classes
Create dedicated tool classes for each model or operation instead of generic tools:
# โ
GOOD: Specific tool for a single purpose
class SearchUsersTool < ActiveMcp::Tool::Base
# ...specific implementation
end
# โ BAD: Generic tool that dynamically loads models
class GenericSearchTool < ActiveMcp::Tool::Base
# Avoid this pattern - security and maintainability issues
end
2. Validate and Sanitize Inputs
Always validate and sanitize inputs in your tool implementations:
def call(user_id:, context: {})
# Validate input
unless user_id.is_a?(Integer) || user_id.to_s.match?(/^\d+$/)
raise "Invalid user ID format"
end
# Proceed with validated data
user = User.find_by(id: user_id)
# ...
end
3. Return Structured Responses
Return structured responses that are easy for AI to parse:
def call(query:, context: {})
results = User.search(query)
[{
type: "text",
text: {
content: results.to_json(only: [:id, :name, :email]),
metadata: {
count: results.size,
query: query
}
}.to_json
}]
end
๐งช Development
After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rake to run the tests.
๐ฅ Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/moekiorg/active_mcp. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
๐ License
The gem is available as open source under the terms of the MIT License.