Design: MCP Server and OAuth Authorization
Context
Cairn's core promise is that agents read, create, comment, and react on the same artifacts humans do — "same auth as your agent." This spec (SPEC-0007) realizes ADR-0004 (MCP as a first-class surface authorized by OAuth 2.1) and depends on ADR-0003 (triple-surface parity: web, CLI, and MCP are thin adapters over one core domain service). It requires SPEC-0002 for the artifact core and share types, and hands provenance/access/expiry rules to SPEC-0009 (ADR-0007).
The design constraints are fixed by the ADRs and the house stack:
- A Go MCP server exposes core operations as MCP tools/resources, in-process over the same core package the web handlers call (ADR-0003). No self-HTTP.
- Authorization is MCP-native OAuth 2.1: authorization-code + PKCE, RFC 8414 metadata discovery, RFC 7591 Dynamic Client Registration, RFC 8707 audience binding, rotating refresh, and RFC 7009 revocation — all served by Cairn's own authorization server inside the Go backend.
- The consent screen is prescriptive: an exact three-line grant the design's canvas specifies, revocable from settings.
- The CLI is just another OAuth client on the identical flow (SPEC-0008 depends on this spec).
Goals / Non-Goals
Goals
- Expose read / create+push / comment+react as MCP tools, and webhook + trajectory streams as read-only MCP resources, at parity with the human surfaces.
- Implement the full MCP OAuth 2.1 authorization flow with exactly three scopes that map one-to-one to the consent screen.
- Bind every token's subject to the human and stamp every action's actor with the
model +
via MCPchannel, so agents inherit — never exceed — the human's reach. - Mint per-connection, independently revocable grants (short access + rotating refresh, audience-bound).
- Let the CLI reuse the identical
/oauth/authorize+/oauth/tokenpath. - Render an accessible, safe consent/login UI.
Non-Goals
- No
sharing:manageorartifacts:deletescope for agents in v1 (ADR-0004, ADR-0007) — changing sharing/expiry and deletion stay explicit human actions. - No agent write path into webhook/trajectory streams in v1 (read-only).
- No per-artifact delegation — the three-scope model is coarse by decision; the capability link (ADR-0005/0007) is the real per-artifact boundary.
- The identity provider for the human-login step is out of scope here (the AS may delegate upstream); this spec only issues Cairn-scoped tokens.
- Provenance/access/retention semantics themselves are specified in SPEC-0009; this spec consumes them.
Decisions
MCP OAuth 2.1 (authorization-code + PKCE), not API keys
Choice: Cairn hosts its own OAuth 2.1 authorization server; clients use the authorization-code grant with mandatory PKCE, discover endpoints via RFC 8414 metadata, and self-register via RFC 7591 DCR.
Rationale: It is the flow MCP clients (Claude Desktop) already implement, it renders the exact three-scope consent the design mandates, and it produces per-connection revocable tokens bound to the human. The CLI collapses onto the same flow (one code path for "same auth as your agent").
Alternatives considered:
- Static API keys / PATs: no interactive per-scope consent, copy-pasted secrets leak, coarse scopes, and not what MCP clients expect. Rejected (ADR-0004 Option B).
- Reuse the web session/cookie: MCP clients are not browsers; no delegation boundary between the human and the agent; not scopable or revocable per-agent. Rejected (ADR-0004 Option C).
- mTLS client certs: heavy issuance/rotation burden, no consent UX, poor fit for consumer clients. Rejected (ADR-0004 Option D).
Exactly three scopes, stream reads folded under artifacts:read
Choice: artifacts:read, artifacts:write, annotations:write — nothing else.
A webhook/trajectory stream is an artifact-shaped resource, so reading it needs no
fourth scope; the consent screen stays at three checkboxes.
Rationale: One-to-one mapping to the consent lines keeps the grant legible and matches the design canvas literally. Withholding delete/sharing scopes enforces least privilege by construction.
Alternatives considered:
- A dedicated
streams:readscope: would push the consent screen to four lines, contradicting the design. Rejected. - A
sharing:managescope for agents: contradicts ADR-0007 (sharing is an explicit human action). Rejected.
Subject = human, actor = model
Choice: The OAuth grant authenticates the human and binds tokens to that
workspace identity (the owner/principal); each action is additionally stamped with
the acting model actor and via MCP.
Rationale: Access control resolves to the human (agents inherit, never exceed); provenance foregrounds the agent for auditability (ADR-0004 subject-vs-actor, recorded per SPEC-0009).
Per-connection grants: short access + rotating refresh, audience-bound
Choice: Each client connection is one grant issuing a ~1h access token (audience-bound via RFC 8707) plus a rotating refresh token; reuse of a rotated-out refresh token revokes the family; RFC 7009 revocation kills one grant without touching siblings or the CLI.
Rationale: Delivers "revoke anytime in settings" literally, limits blast radius of a leaked token, and prevents cross-service replay.
In-process adapter over the core, not a separate service
Choice: The MCP server lives in the same Go binary as the web handlers and calls the core domain package directly (ADR-0003 Option A).
Rationale: Parity becomes a matter of layering — the core owns create/read/ comment/react and all rules (expiry, access, provenance, anchor legality), so no surface can fork them. Rules enforced only at the REST edge would be bypassed by the in-process caller, so size limits/authz that must bind all callers live in/below the core.
Architecture
The MCP server and the OAuth authorization server are two concerns in the same Go backend. The MCP transport authenticates each request with a bearer access token, resolves the human principal + granted scopes, and dispatches to core operations (shared with the web/CLI surfaces). The authorization server handles discovery, DCR, the browser consent flow, and token issuance/rotation/revocation, persisting grants and tokens in PostgreSQL.
flowchart TB
subgraph clients [Clients]
agent[MCP agent e.g. Claude Desktop]
cli[cairn CLI - loopback + PKCE]
browser[Human browser - consent/login]
end
subgraph backend [Cairn Go backend - single binary]
subgraph as [OAuth 2.1 Authorization Server]
meta[/.well-known metadata/]
dcr[/oauth/register - RFC 7591/]
authz[/oauth/authorize - login + consent UI/]
token[/oauth/token - code exchange + refresh rotation/]
revoke[/oauth/revoke - RFC 7009/]
end
mcp[/mcp transport - tools + resource reads/]
core[CORE DOMAIN SERVICE - read/create/comment/react + rules]
end
db[(PostgreSQL - grants, tokens, artifacts)]
blob[(S3-compatible object storage - bodies)]
agent -->|bearer access token| mcp
cli -->|bearer access token| mcp
agent -.->|discover + DCR + authorize + token| as
cli -.->|authorize + token| as
browser -->|login + approve 3 scopes| authz
mcp -->|scope-checked calls| core
authz --> db
token --> db
revoke --> db
dcr --> db
core --> db
core --> blob
The authorization-code + PKCE flow, with the consent screen and the subject/actor mapping, runs as follows:
sequenceDiagram
autonumber
participant C as MCP client / CLI
participant B as Human browser
participant AS as Cairn Auth Server
participant M as MCP transport
participant Core as Core service
C->>AS: GET /.well-known/oauth-authorization-server
AS-->>C: endpoints + metadata
C->>AS: POST /oauth/register (DCR, redirect URI)
AS-->>C: client_id (+ credentials)
C->>B: open /oauth/authorize?client_id&PKCE challenge&scope
B->>AS: authenticate human (session)
AS-->>B: consent screen (3 scope lines, revoke-in-settings)
B->>AS: approve scopes (+ CSRF)
AS-->>C: redirect with single-use authorization code
C->>AS: POST /oauth/token (code + PKCE verifier)
AS-->>C: audience-bound access (~1h) + rotating refresh
C->>M: tool/resource call (Bearer access token)
M->>M: verify audience + scope; resolve subject=human, actor=model
M->>Core: read/create/comment/react (channel via MCP/CLI)
Core-->>M: result (owned by human, provenance stamped)
M-->>C: result
Note over C,AS: later — refresh rotates; revoke kills one grant only
Risks / Trade-offs
- Operating an OAuth 2.1 AS is substantial (DCR, rotation, audience binding, revocation, metadata) versus static keys → mitigate with well-tested libraries, the conformance test suite in ADR-0004, and keeping the surface minimal (three scopes, one grant type).
- Coarse three-scope model can't express per-artifact delegation — an agent with
artifacts:readreads everything the human can → mitigated by the capability model (ADR-0007): the agent only inherits the human's reach and the link is the real boundary. - DCR accepts unregistered clients (open registration) → rate-limit
/oauth/register, validate redirect URIs by exact match, and escape client-supplied strings on the consent page so a hostile registration can't inject content. - Refresh-token theft → rotation + reuse-detection revokes the family; short access-token lifetime limits the window; audience binding blocks cross-service replay.
- In-process adapter could bypass an edge-only rule → keep size limits/authz in or below the core so both the REST edge and the in-process MCP caller enforce them.
- Tracking the evolving MCP authorization spec → isolate protocol specifics behind the AS package and cover them with conformance tests.
Open Questions
- Whether an annotator must belong to the artifact's workspace or may be any
authenticated Cairn user is a policy knob deferred to SPEC-0009/ADR-0007; the MCP
surface only requires that
annotations:writeis present. - Exact access-token lifetime and refresh-token max age (starting point ~1h / multi-day) pending operational tuning.
- Whether to support token introspection (RFC 7662) for internal resource-server validation, or validate audience-bound JWTs locally without a round trip.
- Whether the human-login step delegates to a specific upstream IdP per workspace, and how that maps onto the Cairn workspace identity.