Admin MCP Server
Petal Pro ships with a built-in Model Context Protocol (MCP) server that exposes your app’s admin tools to external AI clients such as Claude Code and Claude Desktop. Instead of logging into an admin panel to look up a user or check billing stats, you can ask your AI assistant directly — it talks to your running app over JSON-RPC 2.0, executes the relevant tool, and hands you back real data. This is especially useful during development and operations: you can interrogate production (carefully) or a staging environment without leaving your editor.
The Endpoint
All MCP traffic goes through a single HTTP endpoint:
POST /api/mcp
Authorization: Bearer <MCP_ADMIN_TOKEN>
Content-Type: application/json
The server speaks JSON-RPC 2.0 and accepts both single messages and batched arrays. Supported methods:
| Method | Description |
|---|---|
initialize |
Protocol handshake — returns server name, version, and capabilities |
tools/list |
Returns all registered tools with their input schemas |
tools/call |
Executes a named tool with arguments |
ping |
Health check |
Quick sanity check with curl:
curl -X POST http://localhost:4000/api/mcp \
-H "Authorization: Bearer dev-mcp-token" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Setup
The Token
The server authenticates via a static bearer token read from the MCP_ADMIN_TOKEN environment variable. In dev, a default of "dev-mcp-token" is used so you can get started immediately. Set a real secret for staging and production:
# runtime.exs already reads this — just set the env var
export MCP_ADMIN_TOKEN="your-long-random-secret"
On Fly.io:
fly secrets set MCP_ADMIN_TOKEN="your-long-random-secret"
The token is compared with Plug.Crypto.secure_compare/2 to avoid timing attacks.
The Route
The route is already wired in the router. You don’t need to add anything. For reference, the controller that handles requests is PetalProWeb.McpController.
Connecting Claude Code
Create a .mcp.json file at the root of your project (it’s gitignored by default):
{
"mcpServers": {
"petal-pro-dev": {
"type": "http",
"url": "http://localhost:4000/api/mcp",
"headers": {
"Authorization": "Bearer dev-mcp-token"
}
},
"petal-pro-prod": {
"type": "http",
"url": "https://yourapp.fly.dev/api/mcp",
"headers": {
"Authorization": "Bearer your-long-random-secret"
}
}
}
}
After saving, run claude mcp list or restart Claude Code — the petal-pro-dev server should appear. You can now ask things like “list the 5 most recent users” or “suspend user with id abc-123” directly in the chat.
Connecting Claude Desktop
Add the server to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"petal-pro": {
"type": "http",
"url": "https://yourapp.fly.dev/api/mcp",
"headers": {
"Authorization": "Bearer your-long-random-secret"
}
}
}
}
Restart Claude Desktop and the tools will appear in the tools panel.
Built-in Tools
The following tools ship with Petal Pro. They’re all native Jido.Action modules under lib/petal_pro/ai/admin_chat/actions/ and registered in PetalPro.AI.AdminChat.ActionRegistry.
get_site_stats
Returns platform metrics — total users, total orgs, and new signups/orgs for a given period (today, week, month, all). Useful for a quick health check.
list_recent_users
Lists recently registered users with basic profile info.
search_users
Text search across user accounts with optional status filters.
manage_user
Performs account management actions on a single user identified by UUID. Available actions:
| Action | Effect |
|---|---|
suspend |
Prevents login |
unsuspend |
Re-enables login |
delete |
Soft-deletes the account |
undelete |
Reverses soft-delete |
make_admin |
Grants admin role |
remove_admin |
Revokes admin role |
All actions are audit-logged via Logs.log_async/2.
list_orgs
Lists organizations with member counts. Accepts an optional search term and result limit.
manage_org
Manages an organization by slug or UUID. Available actions: rename (requires new_name), delete, list_members.
get_ai_call_stats
Returns AI usage and cost statistics for a configurable lookback period (default 30 days, max 365). Includes total calls, token counts, costs in both cents and dollars, and breakdowns by model and operation.
list_changelog_updates, create_changelog_update, manage_changelog
Tools for reading and managing your app’s changelog entries from the AI client.
Adding a Custom Tool
Tools are native Jido.Action modules. Define one under lib/petal_pro/ai/admin_chat/actions/:
defmodule MyApp.AI.AdminChat.Actions.GetRevenueStats do
@moduledoc "Fetch monthly revenue figures."
use Jido.Action,
name: "get_revenue_stats",
description:
"Get monthly recurring revenue and new subscription counts. " <>
"Specify a number of months to look back (default 3).",
schema: [
months: [
type: :pos_integer,
default: 3,
doc: "Months to look back (default 3)."
]
]
alias MyApp.Billing
@impl true
def run(%{months: months}, _context) do
{:ok, Billing.monthly_revenue_stats(months)}
end
end
The schema is a NimbleOptions keyword list — Jido validates incoming arguments against it and emits a JSON Schema for the LLM. Use :in constraints (type: {:in, ["a", "b"]}) to surface enum choices to clients.
Then append it to @actions in PetalPro.AI.AdminChat.ActionRegistry:
@actions [
# ... existing actions ...
MyApp.AI.AdminChat.Actions.GetRevenueStats
]
That’s it. The tool immediately appears in tools/list responses and can be called via tools/call. Both the admin chat UI and the MCP server read from the same registry, so one registration covers both interfaces.
Add a test under test/petal_pro/ai/admin_chat/actions/ — call your action’s run/2 directly to assert behaviour, and let Jido handle schema validation.
Security Considerations
-
The token is the only auth mechanism. Treat
MCP_ADMIN_TOKENlike a root password — use a long random string in production and rotate it if compromised. - Tools run with full database access. Every tool executes in your app process with your Repo. Don’t add tools that expose raw SQL execution or unfiltered bulk data.
-
Destructive actions are logged.
manage_userandmanage_orgboth callLogs.log_async/2so you have an audit trail. -
Don’t commit
.mcp.json. It’s gitignored by default. If you ever accidentally commit it with a production token, rotate the token immediately. - Consider network restrictions for production. If your MCP client only needs to reach production from your own IP, add a firewall rule or Fly.io private networking rather than exposing the endpoint publicly.