Active MCP

A Ruby on Rails engine that provides Model Context Protocol (MCP) capabilities to Rails applications. This gem allows you to easily create and expose MCP-compatible tools from your Rails application.

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

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:

  1. Create a configuration initializer at config/initializers/active_mcp.rb
  2. Mount the ActiveMcp engine in your routes

After running the generator, follow the displayed instructions to create and configure your MCP tools.

Manual Setup

If you prefer to set up manually:

  1. Mount the ActiveMcp engine in your config/routes.rb:
Rails.application.routes.draw do
  mount ActiveMcp::Engine, at: "/mcp"

  # Your other routes
end
  1. Create a tool by inheriting from ActiveMcp::Tool:
class CreateNoteTool < ActiveMcp::Tool
  description "Create Note!!"

  argument :title, :string
  argument :content, :string

  def call(title:, content:)
    Note.create(title:, content:)

    "Created!"
  end
end

with streamable HTTP

Set MCP destination to https:your-app.example.com/mcp

with independent MCP Server

Start the MCP server:

# server.rb
server = ActiveMcp::Server.new(
  name: "ActiveMcp DEMO",
  uri: 'https://your-app.example.com/mcp'
)
server.start

Set up MCP Client

{
  "mcpServers": {
    "active-mcp-demo": {
      "command": "/path/to/ruby",
      "args": ["/path/to/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

This sets up all necessary configuration files and mounts the MCP engine in your routes.

Tool Generator

Create new MCP tools quickly:

# Generate a new MCP tool
$ rails generate active_mcp:tool search_users

This creates a new tool file at app/tools/search_users_tool.rb with the following starter code:

class SearchUsersTool < ActiveMcp::Tool
  description 'Search users'

  argument :param1, :string, required: true, description: 'First parameter description'
  argument :param2, :string, required: false, description: 'Second parameter description'
  # Add more parameters as needed

  def call(param1:, param2: nil, auth_info: nil, **args)
    # auth_info = { type: :bearer, token: 'xxx', header: 'Bearer xxx' }

    # Implement your tool logic here
    "Tool executed successfully with #{param1}"
  end
end

You can then customize the generated tool to fit your needs.

Input Schema

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 include:

  • :string
  • :integer
  • :number (float/decimal)
  • :boolean
  • :array
  • :object (hash/dictionary)
  • :null

Using with MCP Clients

Any MCP-compatible client can connect to your server. The most common way is to provide the MCP server URL:

http://your-app.example.com/mcp

Clients will discover the available tools and their input schemas automatically through the MCP protocol.

Authorization & Authentication

ActiveMcp supports both authentication (verifying who a user is) and authorization (controlling what resources they can access).

Authorization for Tools

You can control which tools are visible and accessible to different users by overriding the visible? class method:

class AdminOnlyTool < ActiveMcp::Tool
  description "This tool is only accessible by admins"

  argument :command, :string, required: true, description: "Admin command to execute"

  # Define authorization logic - only admin tokens can access this tool
  def self.visible?(auth_info)
    return false unless auth_info
    return false unless auth_info[:type] == :bearer

    # Check if the token belongs to an admin
    auth_info[:token] == "admin-token" || User.find_by_token(auth_info[:token])&.admin?
  end

  def call(command:, auth_info: nil)
    # Tool implementation
  end
end

When a user makes a request to the MCP server:

  1. Only tools that return true from their authorized? method will be included in the tools list
  2. Users can only call tools that they're authorized to use
  3. Unauthorized access attempts will return a 403 Forbidden response

This makes it easy to create role-based access control for your MCP tools.

Authentication Flow

ActiveMcp supports receiving authentication credentials from MCP clients and forwarding them to your Rails application. There are two ways to handle authentication:

1. Using Server Configuration

When creating your MCP server, you can pass authentication options that will be included in every request:

server = ActiveMcp::Server.new(
  name: "ActiveMcp DEMO",
  uri: 'http://localhost:3000/mcp',
  auth: {
    type: :bearer, # or :basic
    token: ENV[:ACCESS_TOKEN]
  }
)
server.start

2. Custom Controller with Auth Handling

For more advanced authentication, create a custom controller that handles the authentication flow:

class CustomController < ActiveMcpController
  before_action :authenticate

  private

  def authenticate
    # Extract auth from MCP request
    auth_header = request.headers['Authorization']

    if auth_header.present?
      # Process the auth header (Bearer token, etc.)
      token = auth_header.split(' ').last

      # Validate the token against your auth system
      user = User.find_by_token(token)

      unless user
        render_error(-32600, "Authentication failed")
        return false
      end

      # Set current user for tool access
      Current.user = user
    else
      render_error(-32600, "Authentication required")
      return false
    end
  end
end

3. Using Auth in Tools

Authentication information is automatically passed to your tools through the auth_info parameter:

class SecuredDataTool < ActiveMcp::Tool
  description 'Access secured data'

  argument :resource_id, :string, required: true, description: 'ID of the resource to access'

  def call(resource_id:, auth_info: nil, **args)
    # Check if auth info exists
    unless auth_info.present?
      raise "Authentication required to access this resource"
    end

    # Extract token from auth info
    token = auth_info[:token]

    # Validate token and get user
    user = User.authenticate_with_token(token)

    unless user
      raise "Invalid authentication token"
    end

    # Check if user has access to the resource
    resource = Resource.find(resource_id)

    if resource.user_id != user.id
      raise "Access denied to this resource"
    end

    # Return the secured data
    {
      type: "text",
      content: resource.to_json
    }
  end
end

Advanced Configuration

Custom Controller

If you need to customize the MCP controller behavior, you can create your own controller that inherits from ActiveMcpController:

class CustomController < ActiveContexController
  # Add custom behavior, authentication, etc.
end

And update your routes:

Rails.application.routes.draw do
  post "/mcp", to: "custom_mcp#index"
end

Best Practices

Create a Tool for Each Model

For security reasons, it's recommended to create specific tools for each model rather than generic tools that dynamically determine the model class. This approach:

  1. Increases security by avoiding dynamic class loading
  2. Makes your tools more explicit and easier to understand
  3. Provides better validation and error handling specific to each model

For example, instead of creating a generic search tool, create specific search tools for each model:

# Good: Specific tool for searching users
class SearchUsersTool < ActiveMcp::Tool
  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)
    criteria = {}
    criteria[:email] = email if email.present?
    criteria[:name] = name if name.present?

    users = User.where(criteria).limit(limit)

    {
      type: "text",
      content: users.to_json(only: [:id, :name, :email, :created_at])
    }
  end
end

# Good: Specific tool for searching posts
class SearchPostsTool < ActiveMcp::Tool
  description 'Search posts by criteria'

  argument :title, :string, required: false, description: 'Title to search for'
  argument :author_id, :integer, required: false, description: 'Author ID to filter by'
  argument :limit, :integer, required: false, description: 'Maximum number of records to return'

  def call(title: nil, author_id: nil, limit: 10)
    criteria = {}
    criteria[:title] = title if title.present?
    criteria[:author_id] = author_id if author_id.present?

    posts = Post.where(criteria).limit(limit)

    {
      type: "text",
      content: posts.to_json(only: [:id, :title, :author_id, :created_at])
    }
  end
end

Development

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

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kawakamimoeki/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.