MCP Server Auth: The API Is the Real Boundary A developer built an MCP server named teamkb that provides authenticated access to a team knowledge base via Claude Code. The system replaced a single shared API key with per-user tokens, enabling identity-based audit logs and individual credential revocation. The implementation uses a constant-time token resolver and a server-side write gate to enforce authorization at the API boundary. A single shared API key is fine right up until a second person uses it. intent-brain — the system, repo qmd-team-intent-kb , renamed to the intent-brain plugin v0.4.0 this day — is a team knowledge base. A Fastify HTTP API sits over a governed memory corpus. In front of that API is an MCP server named teamkb , so a teammate doesn't open a dashboard or learn an endpoint. They ask in Claude Code and get a cited answer back with qmd:// citations. That's the whole pitch: institutional memory you query in the same place you write code. Up to this day it authenticated with one shared TEAMKB API KEY . The shared key has two failures that only show up once the tool has more than one user. First, every request looks identical, so the audit log can't say who asked. Second, revoking one person means rotating the key for everyone — there's no per-person handle to drop. Both are structural, not bugs you patch. You fix them by giving each person their own credential. The work closed that gap with three things, in this order: per-user tokens identity , a server-side write gate authorization , and a per-read access log audit . The through-line: the API is the real boundary. The MCP client-side tool gate is UX, not security. And the per-read access log stays separate from the governance audit trail — separate log, not no log. apps/api/src/auth/token-registry.ts . Each token resolves to a record: { actor, role } , where role is 'admin' | 'member' . The shared key's two failures both dissolve here — every request now carries an actor , and revoking one person is dropping one record, not a team-wide rotation. Tokens come from layered sources, in precedence order: explicit records → a TEAMKB TOKENS JSON env → a TEAMKB TOKENS FILE default ~/.teamkb/tokens.json → the legacy single TEAMKB API KEY , which becomes one admin token with actor "shared" for back-compat. Each entry is a bearer token resolved to an identity at request time. Malformed entries are skipped rather than crashing the boot. A record with no role defaults to member — least privilege, not most. In dev mode the registry is allowed to be empty and every request runs as actor 'dev' ; in production an empty registry fails closed . The registry itself is small, and the interesting part is what it deliberately does not do: export class InMemoryTokenRegistry implements TokenRegistry { private readonly records: ReadonlyArray