Goal
Add per-user API key authentication to the MCP HTTP transport. Each authenticated user generates personal API keys through the UI. MCP requests authenticated with a user key are attributed to that user in revisions, audit events, and MCP activity logs.
Current State
mcp_server/server.py — 1596 lines, FastMCP with 31 tools, zero auth on HTTP transport
- MCP HTTP endpoint:
POST /mcp/ (streamable-http)
- Stdio transport: local subprocess (no auth needed)
- All MCP writes currently record
user_id = NULL / created_by = NULL
- Attribution columns:
section_revisions.created_by, section_comments.created_by, chat_messages.created_by, mcp_activity.user_id, audit_events.user_id
Critical Constraints
- Do NOT rewrite or refactor the 31 MCP tools. Auth added at transport/middleware layer only.
- Stdio transport remains unauthenticated (local subprocess, OS-authenticated).
- Backward compatible.
MCP_AUTH_REQUIRED=false (default) preserves current behavior.
- Idempotent migrations —
IF NOT EXISTS, DO $$ BEGIN ... END $$.
Part 1: Database Schema
Create db/14_mcp_api_keys.sql:
CREATE TABLE IF NOT EXISTS mcp_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
key_hash TEXT NOT NULL, -- SHA-256 hex digest
key_prefix TEXT NOT NULL, -- first 8 chars: "sk-prd-a1b2c3d4..."
name TEXT NOT NULL DEFAULT 'Default',
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ -- soft delete
);
CREATE INDEX IF NOT EXISTS idx_mcp_api_keys_hash ON mcp_api_keys(key_hash) WHERE revoked_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_mcp_api_keys_user ON mcp_api_keys(user_id);
Key format: sk-prd-{32 random hex chars} (e.g., sk-prd-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6). Only SHA-256 hash stored in DB. Full key returned once at creation.
Part 2: MCP Server Auth Middleware
Env vars: MCP_AUTH_REQUIRED (bool, default false), MCP_API_KEY (optional master key fallback)
Auth logic: Starlette middleware or FastMCP handler wrapper on /mcp/ POST:
- Extract
Authorization: Bearer <key> header
- If missing + auth required -> 401 JSON-RPC error
- Check master key (
MCP_API_KEY env) -> authenticated, no user attribution
- Hash key with SHA-256, look up in
mcp_api_keys where revoked_at IS NULL
- If found -> authenticated with user_id; update
last_used_at
- If not found -> 401
User propagation via contextvars:
mcp_request_user_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
"mcp_request_user_id", default=None
)
Set in middleware after auth. In tool INSERT statements, replace hardcoded NULL with mcp_request_user_id.get(None) for: section_revisions.created_by, section_comments.created_by, comment_replies author, mcp_activity.user_id, audit_events.user_id.
Stdio transport: contextvar stays at default None — current behavior preserved.
Part 3: API Endpoints for Key Management
| Method |
Path |
Auth |
Description |
| POST |
/api/mcp-keys |
user |
Generate key (returns full key once). Max 5 active per user |
| GET |
/api/mcp-keys |
user |
List current user keys (prefix only) |
| DELETE |
/api/mcp-keys/{key_id} |
user |
Revoke own key (soft delete) |
| GET |
/api/admin/mcp-keys |
admin |
List all org keys with user info |
| DELETE |
/api/admin/mcp-keys/{key_id} |
admin |
Revoke any key |
Part 4: Frontend UI
frontend/src/components/mcp-key-manager.tsx:
- "MCP API Keys" section in user settings (or new
/app/settings page)
- Key list table: Name, Key prefix, Last Used, Created, Status, Revoke button
- "+ Generate New Key" dialog: name input -> generate -> show full key with copy button + warning "Copy now, shown once"
- Revoke confirmation dialog (destructive)
- Admin view: additional "All Organization Keys" section with User column
Types in frontend/src/lib/types.ts:
export interface McpApiKey {
id: string;
key_prefix: string;
name: string;
last_used_at: string | null;
created_at: string;
is_active: boolean;
}
export interface McpApiKeyCreateResponse {
id: string;
key: string;
key_prefix: string;
name: string;
created_at: string;
}
Part 5: Configuration and Docs
- Update README: MCP Authentication section with Claude Code config example
- Update
docker-compose.yml: add MCP_AUTH_REQUIRED, MCP_API_KEY to mcp-server env
- Update
.env.example
Part 6: Tests
API tests (tests/test_mcp_keys.py): generate key, list keys, revoke key, max 5 limit, revoked key frees slot
MCP auth tests (tests/test_mcp_auth.py): no auth when required -> 401, valid key passes, revoked key rejected, invalid key rejected, user attribution with key, no-auth mode allows anonymous, last_used_at updated
Validation Criteria
Goal
Add per-user API key authentication to the MCP HTTP transport. Each authenticated user generates personal API keys through the UI. MCP requests authenticated with a user key are attributed to that user in revisions, audit events, and MCP activity logs.
Current State
mcp_server/server.py— 1596 lines, FastMCP with 31 tools, zero auth on HTTP transportPOST /mcp/(streamable-http)user_id = NULL/created_by = NULLsection_revisions.created_by,section_comments.created_by,chat_messages.created_by,mcp_activity.user_id,audit_events.user_idCritical Constraints
MCP_AUTH_REQUIRED=false(default) preserves current behavior.IF NOT EXISTS,DO $$ BEGIN ... END $$.Part 1: Database Schema
Create
db/14_mcp_api_keys.sql:Key format:
sk-prd-{32 random hex chars}(e.g.,sk-prd-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6). Only SHA-256 hash stored in DB. Full key returned once at creation.Part 2: MCP Server Auth Middleware
Env vars:
MCP_AUTH_REQUIRED(bool, default false),MCP_API_KEY(optional master key fallback)Auth logic: Starlette middleware or FastMCP handler wrapper on
/mcp/POST:Authorization: Bearer <key>headerMCP_API_KEYenv) -> authenticated, no user attributionmcp_api_keyswhererevoked_at IS NULLlast_used_atUser propagation via
contextvars:Set in middleware after auth. In tool INSERT statements, replace hardcoded NULL with
mcp_request_user_id.get(None)for:section_revisions.created_by,section_comments.created_by,comment_repliesauthor,mcp_activity.user_id,audit_events.user_id.Stdio transport: contextvar stays at default None — current behavior preserved.
Part 3: API Endpoints for Key Management
/api/mcp-keys/api/mcp-keys/api/mcp-keys/{key_id}/api/admin/mcp-keys/api/admin/mcp-keys/{key_id}Part 4: Frontend UI
frontend/src/components/mcp-key-manager.tsx:/app/settingspage)Types in
frontend/src/lib/types.ts:Part 5: Configuration and Docs
docker-compose.yml: addMCP_AUTH_REQUIRED,MCP_API_KEYto mcp-server env.env.examplePart 6: Tests
API tests (
tests/test_mcp_keys.py): generate key, list keys, revoke key, max 5 limit, revoked key frees slotMCP auth tests (
tests/test_mcp_auth.py): no auth when required -> 401, valid key passes, revoked key rejected, invalid key rejected, user attribution with key, no-auth mode allows anonymous, last_used_at updatedValidation Criteria