All docs

MCP authentication & rate limiting

MCP authentication & rate limiting

Module: apps/mcp/src/auth.ts Token storage: agent_tokens (task 11). Hash is SHA-256 keyed by AGENT_TOKEN_PEPPER; the plaintext format is drk_at_<base64url>.

Request lifecycle

Authorization: Bearer drk_at_…
        │
        ▼
  cachedVerify (60s LRU)  ──miss──▶  verifyAgentToken (Postgres)
        │
        │ revoked / expired / agent.status='disabled' → null → 401
        ▼
  bumpRateLimit (Redis INCR rl:{tokenId}:{minute})
        │
        │ > limit → 429 + Retry-After
        ▼
  setAuthContext({ agentId, orgId, scopes, tokenId })
        │
        ▼
  maybeUpdateLastUsed (fire-and-forget; 10% sample, forced after 5 min)

Limits

The Redis key is rl:{token_id}:{floor(now/60)} with EXPIRE 60. The window rolls forward every wall-clock minute; we don't bother with sliding windows — the goal is to stop runaway agent loops, not to be precise.

Scope catalogue

Authoritative list: AGENT_SCOPES in packages/db/src/schema/agents.ts.

Tools call assertScope(ctx, 'tasks:write') at the top of their handler; the error code on miss is auth.insufficient_scope.

Cache invalidation

The middleware subscribes to the Redis channel agent_token.revoked. When a revoke happens (UI or /agents/tokens/{id} API later), the publisher posts a no-op message and every MCP process flushes its token LRU. Subsequent verifications hit Postgres and observe the revoked_at column.

Dev/test bypass

  • NODE_ENV=test or DEV_AUTH_BYPASS=true — missing Authorization returns

200 with an empty AuthContext. Tools that need a real orgId will reject on their own; this only short-circuits the middleware.

  • The env validator (packages/shared/src/env.ts) refuses to start a

production process with DEV_AUTH_BYPASS=true.

Audit

The auth middleware itself does not write to agent_audit — every tool handler does, with recordAudit({ action: tool_name, … }). This keeps the audit row attached to the business write inside the same transaction.