Skip to content

Admin MCP server

/api/mcp/admin exposes the To-Dos, Triage and Issues sections of admin.gevell.com to Claude (Claude Code, Claude Desktop, claude.ai) as MCP tools. It is a separate server from the public toolchain MCP at /api/mcp (pdf_*, url_*, acr_*) and never shares tools with it.

Tracking issue: #4097.

Auth

  • Authorization: Bearer <api-key>, the same platform API keys as /api/mcp (api_keys table, shared rate-limit bucket).
  • Plus an admin check on every RPC (isUserAdmin, i.e. an admin email or profiles.role = 'admin'). Non-admin keys get 403 even for initialize and tools/list, so they can’t see the tool names.
  • Writes are attributed to the key owner’s email in the issue timeline and alerts, exactly as if they’d used the admin UI.

Tools

FamilyToolsNotes
To-dos (CRM tasks)todos_list, todos_get, todos_create, todos_update, todos_listNotes, todos_addNotetodos_update with status: "done" completes a to-do
Triagetriage_listThreads, triage_getThread, triage_statsRead-only
Issuesissues_list, issues_get, issues_create, issues_update, issues_addNote, issues_listLabelsresolved/closed require close_reason

Deliberately not exposed (admin UI only): deletes, approving or rejecting triage actions, posting triage messages (that impersonates the submitter and starts a paid agent turn), reordering, label management and attachments.

How it works

Claude ──JSON-RPC──▢ /api/mcp/admin (createMcpRoutes + adminRegistry)
β”‚ requireBearerAuth β†’ requireAdminKey
β–Ό
mcp/admin/tools.ts ──callAdmin()──▢ in-process Hono app
β”œβ”€ /crm β†’ adminCrm
β”œβ”€ /issues β†’ adminIssues
└─ /triage β†’ adminTriage
  • Tools don’t re-implement anything: callAdmin (mcp/admin/dispatch.ts) invokes the same route sub-apps the admin UI calls, so validation, state rules, the activity log and assignment/comment alerts stay in one place.
  • Those sub-apps start with requireAuth, which expects a Supabase JWT. The dispatcher instead attaches the authenticated actor to a copy of env under a module-private Symbol (middleware/trusted-actor.ts). Runtime bindings and HTTP input can’t create symbol keys, so this can’t be forged from outside the process. requireAuth still runs the deleted-account check, and each sub-app’s own requireAdmin still runs.
  • The transport (mcp/transports/mcp-http.ts) is a factory, createMcpRoutes({ registry, serverName, resourcePath, instructions, authorize }). /api/mcp and /api/mcp/admin are two instances of it.

Adding a tool

  1. Register it in workers/api/src/mcp/admin/tools.ts on adminRegistry, delegating to an existing admin route via callAdmin.
  2. If the route isn’t mounted in the dispatcher’s inner app yet, add it in mcp/admin/dispatch.ts.
  3. Add a case to src/__tests__/mcp/admin-mcp.test.ts.

Keep destructive or paid operations out unless there’s an explicit decision to expose them.

Connecting Claude Code

Create an API key for an admin account in the dashboard, then:

Terminal window
claude mcp add --transport http gevell-admin https://api.gevell.com/api/mcp/admin --header "Authorization: Bearer <admin-api-key>"