The project is in a healthy, maintained state
Rails engine exposing Administrate dashboards over the Model Context Protocol, with API key and OAuth 2.1 authentication.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 1.0.0.beta3, < 2
>= 2.7
~> 1.5
>= 8.1
 Project Readme

administrate-mcp

A Rails engine that exposes Administrate dashboards over the Model Context Protocol, so an MCP client such as Claude can list, show and search your admin data, and run the write actions you opt in, with the same permissions the admin UI enforces.

Gem Version CI License: MIT Ruby

Table of contents

  • Why
  • Features
  • Demo
  • Requirements
  • Installation
  • Quick start
  • Configuration
  • Routes
  • Dashboard declarations
  • Authentication
  • OAuth
  • Improving the server from its own use
  • Rate limiting
  • Admin integration
  • Development
  • Security
  • Contributing
  • Changelog
  • Licence

Why

Administrate dashboards are built for a person clicking through a browser. This gem reads the same dashboard declarations, the same Pundit policies and the same scoped queries, and publishes them as MCP tools, so an LLM client can answer questions about your admin data and, where you allow it, act on it, without a second implementation of your authorization rules. Nothing in the engine knows about your application; everything host-specific goes through Administrate::MCP.configure.

Features

  • Three generic tools built from every Administrate dashboard: admin_resource_list_resources, admin_resource_list, admin_resource_show.
  • Write actions declared per dashboard with mcp_action, each published as its own tool, gated by the write scope (an API key with write access, or an OAuth token granted it) and the resource's own authorization predicate on the loaded record.
  • Three ways to authenticate a caller: API keys stored as a digest, an external identity provider through identity_fallback (a Cloudflare Access verifier ships with the gem), and an optional built-in OAuth 2.1 server with Dynamic Client Registration, PKCE and refresh tokens, on by default and disabled with one setting.
  • Pundit-aware authorization by default: reads run through the same index? and show? predicates as the admin UI, so a resource an admin cannot see in the browser is not exposed over MCP either.
  • Search, filters and field selection built from the dashboard's own declarations, plus foreign key filters that need no declaration at all.
  • A feedback tool, report_mcp_improvement, so the client can tell you which of your descriptions, filters and fields let it down, and you can fix them. See Improving the server from its own use. The signal is always available through config.on_feedback; storing it, the dashboard and the clean-up service are a batteries-included option a host turns on with config.persist_feedback.
  • The admin console for its own tables, as two dashboards and two controller concerns you include in controllers of your own: issuing an API key, revoking one, and reading the feedback, without giving up your base controller, your authentication or your policies.
  • Optional Sidekiq introspection tools, sidekiq_stats and sidekiq_retries, wired to a stats provider object you supply.
  • No reference to a constant it does not own: field serializers are keyed on class names, dashboards opt in per attribute, and every host-specific behaviour is configuration, not a subclass.

Demo

A tools/list call against the JSON-RPC endpoint, authenticated with an API key:

curl https://admin-mcp.example.com/ \
  -H 'Authorization: Bearer amcp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'

returns the generic tools built from your dashboards, plus any mcp_action you declared, among them:

{
  "result": {
    "tools": [
      { "name": "admin_resource_list_resources" },
      { "name": "admin_resource_list" },
      { "name": "admin_resource_show" },
      { "name": "report_mcp_improvement" }
    ]
  }
}

A tools/call against admin_resource_list, restricted to three fields:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "id": 2,
  "params": {
    "name": "admin_resource_list",
    "arguments": { "resource": "widget", "fields": ["id", "name", "status"] }
  }
}

returns columns, rows and pagination metadata built from the dashboard's COLLECTION_ATTRIBUTES:

{
  "columns": ["url", "id", "name", "status"],
  "rows": [
    [
      "https://admin.example.com/admin/widgets/1",
      "1",
      "Turbo encabulator",
      "published"
    ],
    [
      "https://admin.example.com/admin/widgets/2",
      "2",
      "Flux capacitor",
      "draft"
    ]
  ],
  "meta": { "page": 1, "per_page": 10, "total_count": 2, "total_pages": 1 }
}

Requirements

  • Ruby 3.2 or newer.
  • Rails 8.1 or newer.
  • Administrate 1.0.0.beta3 or newer, below 2.0 (the search implementation calls methods Administrate::Search treats as internal, see Search).
  • PostgreSQL. The migrations create uuid primary keys defaulted with gen_random_uuid() and store the OAuth redirect_uris and grant_types as array columns.

Installation

# Gemfile
gem 'administrate-mcp'

Copy the migrations and run them:

bundle install
bin/rails administrate_mcp:install:migrations
bin/rails db:migrate

The tables are administrate_mcp_api_keys, administrate_mcp_feedbacks, administrate_mcp_oauth_applications, administrate_mcp_oauth_access_grants and administrate_mcp_oauth_access_tokens. They use uuid primary keys and a uuid column, admin_id by default, that is indexed but carries no foreign key constraint, so the engine works with any admin table. Set config.admin_foreign_key before running the migrations if the host's own admin table already uses a different column name and renaming it is not an option, for example a live credentials table.

administrate_mcp_feedbacks is only needed when config.persist_feedback is true; leave it false, the default, and the migration ships but the table is never read from or written to.

Quick start

The smallest configuration that works. Save it as config/initializers/administrate_mcp.rb; it must run before the engine's models load, because the admin association reads admin_class_name and admin_foreign_key:

Administrate::MCP.configure do |c|
  c.admin_class_name = 'Administrator'
  c.current_admin = ->(controller) { controller.send(:warden)&.authenticate(scope: :administrator) }
  c.issuer = 'https://admin-mcp.example.com'
  c.admin_origin = 'https://admin.example.com'
end

Then draw the routes, split across the MCP origin and the admin origin (see Routes for why):

# config/routes.rb
Rails.application.routes.draw do
  constraints ->(request) { request.subdomain == 'admin-mcp' } do
    Administrate::MCP::Routes.draw_mcp_origin(self)
  end

  constraints subdomain: 'admin' do
    Administrate::MCP::Routes.draw_admin_origin(self)
  end
end

This draws a full OAuth 2.1 server by default (see OAuth to turn it off) and grants every authenticated admin access to every dashboard until you set c.authorization. For the full picture, including hooks, authorization adapters, field serializers and identity fallback, see the reference sections below.

Configuration

The full Configuration object, the settings table, the authorization adapters, and how to register, skip or reclassify a field class: docs/configuration.md.

Routes

Why the consent screen and the JSON-RPC endpoint are drawn on separate origins, and what each route helper adds: docs/routes.md.

Dashboard declarations

MCP_DESCRIPTION, MCP_BASE_SCOPE, MCP_SKIPPED_ATTRIBUTES, MCP_EXPOSED, COLLECTION_FILTERS and mcp_action, the constants and macro that turn one dashboard into an MCP resource with its own readable fields and writable actions: docs/dashboards.md.

Authentication

How a request is authenticated (API key, then OAuth token, then identity_fallback), how to issue and rotate API keys, and the identity_fallback recipe for an identity resolved in front of the application: docs/authentication.md. A Cloudflare Access verifier ships with the gem for hosts that run edge-managed OAuth in front of the application: docs/authentication.md#identity-fallback.

OAuth

The built-in OAuth 2.1 server, what turning it off with c.oauth = false changes, and when a host should: docs/oauth.md.

Improving the server from its own use

Every tool here is built from your dashboards: the resource names, the field lists, the filters and the MCP_DESCRIPTION you wrote. The client calling those tools is the one that gets misled when any of it is wrong, and it is the only party that knows which call it was trying to make. The engine has no way to detect this on its own: a vague description is not an error, it is a successful call that returned the wrong thing or a query the caller gave up on.

report_mcp_improvement is how the client tells you. Its categories are deliberately not free text. Each one names a change you make in a dashboard:

Category What it points at
description MCP_DESCRIPTION is missing, vague or actively misleading
missing_filter the query needed a filter the dashboard does not declare
missing_field a field the caller needed is not on the show page
missing_resource a dashboard is not exposed, or does not exist
serialization a field came back unreadable and needs a serializer registered
other anything the categories above do not cover

A report arrives with the category, the resource it concerns and the client's own account of what it wanted, which is most of a change request already. Wire config.on_feedback to somewhere your team will actually read, work through what arrives, and the next caller gets a server that describes itself better. Turning on config.persist_feedback keeps the reports in a table so you can triage a batch at a time rather than react to each one.

This is the loop the tool exists for. It is worth running deliberately rather than waiting for complaints: point a client at the server, give it real tasks, and collect what it could not do.

Rate limiting

The rack-attack throttles recommended for the OAuth endpoints, and the helper that registers them for you: docs/oauth.md#rate-limiting.

Admin integration

Including the API key and feedback consoles the engine ships, what a host overrides in them, listing the engine's own tables over the protocol, customising the consent screen, and cleaning up old feedback: docs/admin-integration.md.

Development

bundle install
bundle exec rspec
bundle exec rubocop

Needs a reachable PostgreSQL server; the test suite creates its own database on first run. See docs/development.md for the dummy application and the Postgres environment variables.

Security

See SECURITY.md for how to report a vulnerability.

The JSON-RPC endpoint authenticates by bearer token only. It never falls back to a session cookie, so a browser signed into the admin UI cannot drive the protocol endpoint. API keys, OAuth access and refresh tokens, and OAuth authorization codes are all stored as SHA-256 digests, never in plaintext. The Cloudflare Access verifier fails closed: a blank team domain or audience makes it refuse every request rather than admit an unverified one. A credential does not outlive the admin who holds it, because admin_active is checked on every call, whether the credential is an API key, an OAuth token, or an identity resolved through identity_fallback.

Contributing

Bug reports and pull requests are welcome on GitHub. See CONTRIBUTING.md for the development workflow, and CODE_OF_CONDUCT.md for how we expect people to treat each other in this project's spaces.

Changelog

See CHANGELOG.md for a history of releases.

Licence

MIT. See LICENSE.txt. Copyright Sorare.