Porting Playbook

Goal

Recreate the generic single-tenant, user-owned MCP feature in another Rails project without copying any domain-specific business logic.

Pre-Flight Decisions

Answer these before you port anything:

  1. What is the authenticated user model: User, Account, Member, something else? (The generic package assumes User.)
  2. What authentication primitive is already in place: Devise, Rails 8 built-in auth, custom sessions? You need current_user available in controllers.
  3. What resource will you expose first through the CRUD tools? (Rename Item to your real resource before you start.)
  4. Do you need token CRUD in the web UI, or is token management API-only?
  5. Will the target app support mutation tools on day one, or read-only at first?
  6. Do you need a Current model for audit tagging (Current.user), or will the tool logs be enough?

Phase 1: Bring Over The Shared Runtime

Add the mcp gem to the Gemfile and run bundle.

gem "mcp", "~> 0.10.0"

Create the Mcp namespace and copy these files first:

  • app/middleware/mcp/base_rack_app.rb
  • app/models/mcp/token.rb + migration
  • app/models/mcp/activity_log.rb + migration
  • app/models/mcp/tools/concerns/formattable.rb
  • app/models/mcp/tools/concerns/listable.rb
  • app/models/mcp/tools/concerns/tool_metadata.rb

At the end of this phase, the target app has the primitives needed to authenticate tokens and log tool calls, but the endpoint is not mounted yet.

Run the migrations:

bin/rails db:migrate

Phase 2: Define The User Context

Create app/models/mcp/user_context.rb. Keep it small โ€” it exists only to bundle the acting user and token for thread-local access during a single request.

class Mcp::UserContext
  attr_reader :user, :token

  def initialize(user, token:)
    @user = user
    @token = token
    raise ArgumentError, "user is required" if @user.nil?
  end
end

If your target app uses a different name for the actor (Member, Account), rename the attribute but keep the object minimal.

Phase 3: Wire The Rack Endpoint

Create app/middleware/mcp/rack_app.rb. It should answer three questions:

  1. Is the token authorized for this endpoint? (has an associated user, scope "user", not revoked)
  2. How do I build the server for this token? (via Mcp::ServerBuilder.build(context))
  3. What request-local flags do I need before and after dispatch? (set and clear Current.user and Mcp::BaseTool.user_context)

Mount it in config/routes.rb above authenticated blocks:

mount Mcp::RackApp.new, at: "/mcp"

After this phase, a POST /mcp/some-token request returns 401 or hits the builder.

Phase 4: Port The Base Tool And Server Builder

Bring over:

  • app/models/mcp/base_tool.rb
  • app/models/mcp/server_builder.rb

Adapt these pieces carefully:

  • the list of tool namespaces in TOOL_NAMESPACES
  • the context accessor names on Mcp::BaseTool if you changed them (user, token)
  • the permission predicate used by the builder (token.permits?(domain))
  • the CallLogging module โ€” keep it exactly as-is unless you have a reason to change the logging contract

Once this is done, the target project serves tools/list (empty) even before any tool classes exist.

Phase 5: Build The First CRUD Domain

Do not port multiple domains at once. Rename Item to your real first resource and port these five tools in order:

  1. list_<resource>
  2. get_<resource>
  3. create_<resource>
  4. update_<resource>
  5. delete_<resource>

Use the patterns from 04_tool_patterns.md:

  • each tool class handles one action
  • each tool has a precise tool_name
  • each tool declares mcp_domain and mcp_action
  • every query starts from user.<association>, never from the unscoped model class
  • list tools include ID: in every row so the AI can feed ids into follow-up tools

Register the namespace in Mcp::ServerBuilder::TOOL_NAMESPACES:

TOOL_NAMESPACES = %w[
  Mcp::Tools::Items
].freeze

Phase 6: Add Token Management

If the target app needs human-operated token lifecycle management, add:

  • Mcp::BaseController (authenticates the user, applies the app layout)
  • Mcp::TokensController (index / new / create / edit / update / destroy / regenerate)
  • Mcp::TokenPolicy (Pundit policy so users only see their own tokens)
  • Views for the token list, new token form, and permission toggles
  • Nested routes under namespace :mcp, path: "settings/mcp"

The runtime works without the UI. Production teams usually need a safe place to issue and rotate tokens, so this is strongly recommended.

Phase 7: Test Before Adding More Tool Surface

Before the tool catalog grows, validate these scenarios:

  1. Valid token can call tools/list and sees exactly the permitted tools
  2. Revoked token returns 401
  3. Missing token returns 401
  4. Rate limit returns 429 after the configured threshold (60/minute/token by default)
  5. tools/call on a permitted tool creates an Mcp::ActivityLog row
  6. Tool cannot access another user's records (scope leak test)

See 05_security_ops_and_testing.md for the full test matrix.

Phase 8: Expand The Tool Catalog

Once the Item domain is stable, add more domains one at a time. For each:

  • create app/models/mcp/tools/<domain>/ with up to five CRUD tool classes
  • add the namespace string to Mcp::ServerBuilder::TOOL_NAMESPACES
  • add the permission key to Mcp::Token::PERMISSION_KEYS
  • update the token form checkboxes so users can opt in

Copy Matrix

Copy almost directly

  • Mcp::BaseRackApp
  • Mcp::BaseTool
  • Mcp::UserContext
  • Mcp::Tools::Concerns::Formattable
  • Mcp::Tools::Concerns::Listable
  • Mcp::Tools::Concerns::ToolMetadata

Copy, then adapt to the target domain

  • Mcp::Token (rename permission keys to match your domains)
  • Mcp::ActivityLog (adjust sanitization patterns if needed)
  • Mcp::RackApp (rename Current.user if your app uses something else)
  • Mcp::ServerBuilder (fill in TOOL_NAMESPACES)
  • routes and controllers

Rebuild from scratch

  • tool classes for each domain you expose
  • policies tied to your own authorization model
  • token UI styled to your own design system

Suggested Rollout In Another Project

Smallest viable export

  • one MCP endpoint
  • token model + activity log
  • one domain (your first real resource) with list and get

Parity export

  • full CRUD for one resource (list, get, create, update, delete)
  • token management UI
  • activity viewer
  • per-domain permission toggles
  • rate limiting validated under production-like load

Common Adaptation Points

Different actor model

If the target app authenticates something other than User (for example Member, Account), rename the attribute on Mcp::UserContext and the accessor on Mcp::BaseTool. Keep the pattern identical.

No Current model

You can omit Current.user at first. The MCP runtime does not require it. It is only used so downstream write activity can be tagged as MCP-originated.

Different auth stack

The MCP runtime does not care how tokens are created โ€” only that they exist and have a user_id. If your target app uses Rails 8 built-in auth, Devise, or a custom session layer, the only thing that changes is the before_action :authenticate_user! in Mcp::BaseController.

Different authorization stack

If you do not use Pundit, replace Mcp::TokenPolicy with inline scoping in the controller: current_user.mcp_tokens.find(params[:id]).