A Ruby on Rails engine for the Model Context Protocol (MCP) - connect your Rails apps to AI tools with minimal effort.
- Active MCP π
- π 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
- π₯ Using Context in the Schema
- π‘ Best Practices
- π§ͺ Development
- π₯ Contributing
- π License
- 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
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
- 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
def tool_name
"create_note"
end
def description
"Create Note"
end
argument :title, :string, required: true
argument :content, :string, required: true
def call(title:, content:, context:)
note = Note.create(title: title, content: content)
"Created note with ID: #{note.id}"
end
end
- Create schema for your application:
class MySchema < ActiveMcp::Schema::Base
def tools
[
CreateNoteTool.new
]
end
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
Active MCP supports two connection methods:
Set your MCP client to connect directly to your Rails application:
https://your-app.example.com/mcp
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"]
}
}
}
Active MCP provides generators to help you quickly set up and extend your MCP integration:
Initialize Active MCP in your Rails application:
$ rails generate active_mcp:install
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.
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.
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
def tool_name
"Search Users"
end
def description
'Search users by criteria'
end
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)
users.attributes
end
end
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 |
Control access to tools by overriding the visible?
class method:
class AdminOnlyTool < ActiveMcp::Tool::Base
def tool_name
"admin_only_tool"
end
def description
"Admin-only tool"
end
argument :command, :string, required: true, description: "Admin command"
# Only allow admins to access this tool
def 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
server = ActiveMcp::Server.new(
name: "My Secure MCP Server",
uri: 'http://localhost:3000/mcp',
auth: {
type: :bearer,
token: ENV['MCP_AUTH_TOKEN']
}
)
server.start
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 allow you to share data and files with AI assistants. Resources have a URI, MIME type, and can return either text or binary data.
Resources are Ruby classes **Resource
:
class UserResource < ActiveMcp::Resource::Base
class << self
def mime_type
"application/json"
end
end
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 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
def resources
User.all.each do |user|
UserResource.new(id: user.id)
end
end
end
Resources can return two types of content:
- Text Content - Use the
text
method 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
blob
method to return binary files:
class ImageResource < ActiveMcp::Resource::Base
class << self
def mime_type
"image/png"
end
end
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 Teamplates allow you to define template of resources.
Resource teamplates are Ruby classes **Resource
:
class UserResource < ActiveMcp::Resource::Base
class << self
def resource_template_name
"users"
end
def uri_template
"data://localhost/users/{id}"
end
def mime_type
"application/json"
end
def description
"This is a test."
end
def visible?(context:)
# Your logic...
end
end
argument :id, complete: ->(value, context) do
User.all.pluck(:id).filter { _1.match(value) }
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
def resources
User.all.each do |user|
UserResource.new(id: user.id)
end
end
end
MCP Prompts allow you to define prompt set.
Resources are Ruby classes **Prompt
:
class HelloPrompt < ActiveMcp::Prompt::Base
argument :name, required: true, description: "User name", complete: ->(value, context) do
User.all.pluck(:name).filter { _1.match(value) }
end
def initialize(greeting:)
@greeting = greeting
end
def prompt_name
"hello"
end
def description
"This is a test."
end
def visible?(context:)
# Your logic...
end
def messages(name:)
[
ActiveMcp::Message::Text.new(
role: "user",
text: "#{@greeting} #{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
def prompts
[
HelloPrompt.new(greeting: "Hello!")
]
end
end
class MySchema < ActiveMcp::Schema::Base
def prompts
user = User.find_by_token(context[:auth_info][:token])
user.greetings.map do |greeting|
GreetingPrompt.new(greeting: greeting)
end
end
end
class GreetingPrompt < ActiveMcp::Prompt::Base
def initialize(greeting:)
@greeting = greeting
end
def prompt_name
"greeting_#{@greeting.text}"
end
def messages
# ...
end
end
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
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
Return structured responses that are easy for AI to parse:
def call(query:, context: {})
results = User.search(query)
{
content: results.to_json(only: [:id, :name, :email]),
metadata: {
count: results.size,
query: query
}
}
end
After checking out the repo, run bundle install
to install dependencies. Then, run bundle exec rake
to run the tests.
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.
The gem is available as open source under the terms of the MIT License.