Skip to main content
View as Markdown

Auth, RBAC & Rate Limiting

An MCP connection is an authenticated actor calling your data, and it is treated as one. There is no AI-specific bypass anywhere in this path: a tool call runs through the same role permissions and row-level rules as the equivalent HTTP request from a human session.

Two Credentials, Selected by Header

There is no strategy to configure. /mcp decides which verifier to use from the header the request actually carries, so both credentials are live at the same time — a Claude Desktop user on OAuth and a CI job holding an API key are the same instance's callers, and neither has to be switched on.

Header Verified as Best for
x-api-key A self-service API key. Scripts, CI jobs, anything non-interactive.
Authorization: Bearer An OAuth 2.1 access token, verified upstream. Desktop and IDE clients that can sign in.

Because header dispatch is decided per request, there is no fallback chain in which one verifier's failure can quietly mask the other's.

Every caller is a user

This is the substantive change, not a change of spelling. Both credentials name a real user, and that user's role is resolved live on every call:

  • Demote someone and every key they hold narrows with them — no re-issuing, no restart.
  • Ban them and their keys stop working, on the next request.
  • Signing out deactivates an OAuth access token at once, because every token is re-checked against the live session rather than trusted until it expires.
  • Keys are hashed at rest, shown once at creation, and individually revocable.
  • Row-level record permissions finally apply. A rule scoped to "records this user owns" needs a user to scope to. The retired static tokens carried none, so that tier never ran for a token-authenticated caller at all — the model saw every row the role could see, not every row the person could.

That last point is why the static MCP_TOKEN_* variables were removed rather than deprecated. They were not merely coarse; they silently skipped a permission tier the equivalent human request went through.

Migrating from MCP_TOKEN_*

The static tokens are gone and a leftover one refuses the boot when MCP_ENABLED=true — deliberately, so the failure lands where the change was made rather than in someone's CI a week later. Three steps:

  1. Unset MCP_TOKEN_ADMIN, MCP_TOKEN_MEMBER, MCP_TOKEN_VIEWER and MCP_AUTH_STRATEGY.
  2. Make sure the app has an app.auth block, with apiKeys: true for non-interactive callers.
  3. Sign in as the user whose role the client should inherit, mint a key, and send it on x-api-key.

Replace a role with a user: where you would have issued MCP_TOKEN_VIEWER, create a user holding the viewer role and mint that user's key. See Troubleshooting: Runtime for the exact boot messages.

RBAC Is the Ceiling

The role of the user behind the credential bounds everything downstream. A key owned by a viewer can only read and list, even against a table whose aiAccess.operations allows writes — aiAccess widens what is offered, never what is permitted.

Layer Answers
aiAccess on the entity Is this eligible to appear as a tool at all?
MCP_ENABLED Is the server running?
Role permissions May this actor perform this operation?
Field-level permissions Which columns appear in the schema and the result?
Row-level rules Which records are visible?

All five must pass. The practical consequence is that you cannot accidentally over-expose a table by writing aiAccess: true on it — the worst case is that a role sees exactly what it could already have fetched over the API.

Audit

With MCP_AUDIT_ENABLED (default true), every tool call is recorded to system.ai_tool_calls and to the activity stream. Admins can read that table back through MCP itself as {app}_system_ai_tool_calls_list.

Turning auditing off is permitted for compliance edge cases and is a bad default. An AI actor is precisely the one whose calls you will later want to reconstruct.

Rate Limiting

Variable Default Scope
MCP_RATE_LIMIT_PER_MINUTE 60 Per credential.
MCP_RATE_LIMIT_PER_DAY 5000 Same.

Over-limit requests answer 429 with standard rate-limit headers. The per-day ceiling is the one that matters most: a model in a retry loop can burn a minute's budget and keep going, but it cannot quietly run all night against your database.

Admin Internals

MCP_EXPOSE_INTERNALS defaults to true, giving the admin role read-only tools over the auth and system tables — {app}_auth_*_read, {app}_system_*_list and so on — with secret columns denylisted.

This is what makes "which users signed up this week?" answerable without a database client. Set it to false to remove those tools from tools/list entirely, including for admins; do that when the MCP surface is meant for business data only and platform internals are out of scope for whoever is connecting.

Last updated September 1, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium