SPEC-0007: MCP Server and OAuth Authorization
Overview
This capability defines Cairn's MCP server surface — the agent interface — and the OAuth 2.1 authorization that lets an agent (or the CLI) act on a human's behalf. It realizes ADR-0004 (MCP as a first-class surface with OAuth) and ADR-0003 (triple-surface parity: the MCP tools are thin adapters over the same core service the web and CLI use). It builds on SPEC-0002 (artifact core and share types).
The MCP surface exposes exactly the operations humans have — read an artifact or bundle file, create & push a new artifact, comment, and react — plus read access to live webhook and trajectory streams. Authorization is the MCP-native OAuth 2.1 flow (authorization-code + PKCE, dynamic client registration, audience-bound access tokens, rotating refresh tokens, RFC 7009 revocation). The human approves an exact three-line consent screen — "Read artifacts you can access", "Create & push new artifacts", "Comment & react on your behalf" — and every grant is independently revocable from settings. The subject of every token is the human; the acting model is recorded as the provenance actor. Agents never exceed the human's reach and are granted no delete or sharing scopes. The CLI is another OAuth client on the identical flow, delivering "same auth as your agent."
Requirements
Requirement: MCP Tool Surface — Artifact & Bundle Read
The MCP server MUST expose an artifact-read tool that returns any artifact — or a
named file within a bundle — that the authorizing human can reach, calling the same
core read operation the web and CLI surfaces call (ADR-0003). The tool MUST require
the an agent holding the server MUST return the artifact body and metadata via the core read operation an agent invokes the read tool with an id the human principal cannot reach (or that is expired/unknown) the server MUST return a uniform not-found result and disclose nothing about existenceartifacts:read scope. It MUST resolve a public id (or mcp://cairn/<id>
handle, ADR-0005) and return the body plus metadata (share type, provenance, expiry)
exactly as the core produces them. Reads MUST NOT return artifacts the human
principal could not reach, and MUST return a uniform not-found result for unknown,
unauthorized, or expired ids (per SPEC-0009 / ADR-0007).Scenario: Read a reachable artifact
artifacts:read invokes the read tool with a valid id its human can reachScenario: Read an unreachable id
Requirement: MCP Tool Surface — Create & Push
The MCP server MUST expose a create-and-push tool that creates a new artifact (or
bundle) owned by the human principal, calling the same core create operation as the
other surfaces. It MUST require the artifacts:write scope. New artifacts MUST be
created only with the default access policy (you + anyone with link) and default
TTL; the tool MUST NOT accept parameters that broaden sharing, disable expiry beyond
workspace policy, or otherwise exceed the human's own create permissions. The
returned artifact MUST carry provenance stamping the model actor and the via MCP
channel (SPEC-0009).
The create tools MUST accept an optional an agent calls the server MUST create the artifact with the default policy and TTL, stamped an agent holding the server MUST create an artifact owned by the human, with the default policy and TTL, stamped a create call includes a request to make the artifact owner-only, no-expiry, or otherwise non-default the server MUST reject the non-default policy request and either create with defaults or return an error, never broadening accesstags array (SPEC-0002 REQ "Artifact
Tags"). Tags never broaden access and never alter provenance. The tool descriptions
MUST publish the bounds, the handoff tag convention, and the semi-trusted stance a
receiver takes toward a handoff (ADR-0018), so an agent learns how to request a
handoff at the point where it creates the artifact.Scenario: Agent creates a handoff
artifact_create or bundle_create with tags handoff and lane:autovia MCP and carrying exactly those tags, and its artifact.created event MUST carry the tags together with on_behalf_of, the client name/version the server recorded from the session's initialize (SPEC-0012)Scenario: Agent creates an artifact
artifacts:write pushes a new bodyvia MCP, and return its id and linkScenario: Create attempts a non-default policy
Requirement: MCP Tool Surface — Comment & React
The MCP server MUST expose comment and react tools that post annotations as the
human's agent, calling the same core annotation operations as the web and CLI. Both
MUST require the an agent holding the server MUST persist the reaction with provenance an agent whose token lacks the server MUST refuse the call and make no annotationannotations:write scope. Anchors MUST be validated by the core
(ADR-0006); the annotation's provenance MUST record the model actor and via MCP.Scenario: Agent reacts to a span
annotations:write reacts to a trajectory turn with a valid anchorvia MCP and the model actorScenario: Comment without the annotation scope
annotations:write invokes the comment tool
Requirement: MCP Resource Surface — Stream Reads
The MCP server MUST expose webhook and trajectory streams as readable resources
addressed by their agent handles ( an agent with the server MUST stream captured requests incrementally with no additional scope required an agent attempts to push data into a webhook or trajectory stream over MCP the server MUST reject it — streams are read-only to agents in v1mcp://cairn/hook/<id>, mcp://cairn/run/<id>;
ADR-0005). Reading a stream MUST require only artifacts:read — a stream is an
artifact-shaped resource and MUST NOT require a fourth scope. Streaming reads MUST
deliver incremental data (webhook requests as they arrive, trajectory spans as they
land). The MCP surface MUST provide no write path into a stream in v1.Scenario: Agent tails a webhook stream
artifacts:read opens mcp://cairn/hook/<id> for a reachable webhookScenario: Agent attempts to write a stream
Requirement: Agent-Shaped Tool Schemas
MCP tool schemas MUST be shaped for the agent that reads them, even where that diverges from the REST encoding of the same field. ADR-0003's triple-surface parity is about CAPABILITY — every surface can do the same things — not about byte-identical wire encodings. REST serves humans and deterministic clients and keeps its own representations; MCP is the surface agents write to.
Concretely, a field an agent supplies MUST NOT surface as a schema that is
impractical to produce from an agent's own data. A text field MUST be declared
as a string rather than as an array of byte values, and a structured field as an
object rather than as an encoded scalar. Where a Go type would infer a schema
that violates this, the MCP input type MUST diverge from the REST type and
convert at the boundary. an agent inspects the a span's the schema MUST NOT require an array of byte values, which costs several times the bytes for text the agent already holds as a string a span's output is not valid UTF-8 the REST ingest remains the surface that carries it; the MCP text field is not required to represent arbitrary bytesScenario: Span output is plain text over MCP
run_create input schemaoutput MUST be declared as a string, and plain UTF-8 text MUST be accepted and stored verbatimScenario: A binary span output
Requirement: MCP Prompt Surface
Every write tool's input schema MUST describe each field, not merely type it: an agent sees the schema and nothing else, so a bare field is one it must guess at. Where a field has a recommended vocabulary, the schema MUST name that vocabulary; where a field drives what a human ultimately sees, the schema MUST say so.
Beyond per-field description, the MCP server MUST expose at least one prompt carrying
the workflow-level guidance a schema structurally cannot: how to decompose work into
spans, which of several vocabularies to commit to, and which fields make the resulting
artifact legible rather than merely valid. The trajectory surface MUST expose an MCP client lists prompts an agent fetches the guidance MUST direct it to locate and parse its harness's own local session record — which holds per-turn timestamps, tool calls, arguments, results and token usage — and to derive span timings from it rather than estimating MUST instruct it to tell the human, and label the run as approximate, when no such record can be found an agent inspects the the a category is added to or removed from the recommended set the advertised vocabulary MUST change with it, rather than silently continuing to advertise a stale listrun_capture
for this purpose. Prompt guidance MUST be derived from the same source of truth the
server enforces, so it cannot drift from the implementation it describes.Scenario: Agent discovers capture guidance
run_capture MUST be present with a description, and fetching it MUST return guidance naming both category vocabularies and stating that a span's output is what a reader sees on expandScenario: Guidance directs the agent to its own transcript
run_capture before recording a runScenario: Span schema names the category vocabulary
run_create input schemacategory field's description MUST enumerate every recommended category, so an agent never has to invent a vocabulary to fill the fieldScenario: Guidance cannot drift from the palette
Requirement: OAuth 2.1 Authorization-Code + PKCE
Authorization MUST use the OAuth 2.1 authorization-code grant with PKCE
mandatory for every client (public and confidential). The token endpoint MUST
reject a code exchange that lacks a valid a client exchanges an authorization code without a valid PKCE the token endpoint MUST reject the exchange and issue no tokens a previously redeemed authorization code is presented a second time the token endpoint MUST reject it and SHOULD revoke any tokens already issued for that codecode_verifier matching the registered
code_challenge. Authorization codes MUST be single-use, short-lived, and bound to
the client, redirect URI, and PKCE challenge.Scenario: Code exchange without a verifier
code_verifierScenario: Replayed authorization code
Requirement: Metadata Discovery & Dynamic Client Registration
Cairn MUST publish OAuth 2.1 authorization-server metadata (RFC 8414) and
protected-resource metadata so MCP clients can discover endpoints without bespoke
configuration. It MUST support Dynamic Client Registration (RFC 7591) so an
arbitrary MCP client (e.g. Claude Desktop) can self-register a redirect URI and
receive client credentials. The registration endpoint MUST be rate-limited and MUST
validate redirect URIs (exact match, loopback allowance for native/CLI clients). a new MCP client fetches the AS metadata and posts a valid dynamic registration the server MUST return client metadata (client id, registered redirect URI) usable for the authorization-code flow a registration request supplies a malformed or non-allowed redirect URI the server MUST reject the registrationScenario: Client discovers and registers
Scenario: Registration with an invalid redirect URI
Requirement: Exactly Three Consent Scopes
The authorization server MUST recognize exactly three scopes, mapping one-to-one to
the design's consent lines: a human approves all three lines of the consent screen the issued token MUST carry exactly a client requests a scope outside the three (e.g. the server MUST reject the request or issue only the recognized subset, never the elevated scopeartifacts:read ("Read artifacts you can access"),
artifacts:write ("Create & push new artifacts"), and annotations:write
("Comment & react on your behalf"). Approving the consent screen MUST grant exactly
the approved subset and nothing more. The server MUST NOT define or issue a
sharing:manage or artifacts:delete scope to agents in v1.Scenario: Full consent grants exactly three scopes
artifacts:read artifacts:write annotations:write and no other scopeScenario: Unknown or elevated scope requested
artifacts:delete)
Requirement: Token Model — Short Audience-Bound Access + Rotating Refresh
Each authorization (one client connection) MUST issue a token family: a short-lived
access token (~1 hour) and a longer-lived refresh token. Access tokens MUST be
audience-bound to the Cairn resource server (RFC 8707 resource indicators) so a
token cannot be replayed against another service. Refresh tokens MUST rotate on
use — redeeming a refresh token issues a new one and invalidates the old;
detection of a reused (already-rotated) refresh token MUST revoke the token family. an access token issued for Cairn is presented to a different audience/resource it MUST be rejected as not audience-valid a refresh token is redeemed the server MUST issue a new refresh token, invalidate the prior one, and revoke the family if the prior (rotated-out) token is ever presented againScenario: Access token bound to audience
Scenario: Refresh rotation and reuse detection
Requirement: Token Revocation (RFC 7009)
Each grant MUST be an independently revocable connection the human sees in settings
(client name, scopes, last-used). The server MUST implement an RFC 7009 revocation
endpoint. Revoking a grant MUST invalidate that grant's access and refresh tokens
without affecting the human's other connections or the CLI. a human revokes a specific MCP connection from settings that grant's access and refresh tokens MUST stop working while sibling grants and the CLI continue to work a revoked access token is presented to the MCP endpoint the server MUST respond 401 and perform no operationScenario: Revoke one connection
Scenario: Revoked token used
Requirement: Subject/Actor Identity Mapping & Least Privilege
The OAuth grant MUST authenticate the human and bind tokens to that workspace
identity; the human is the owner/principal for every action the agent takes. Every
agent action MUST additionally be stamped with the acting model actor and the
an agent reads or creates over MCP access MUST resolve to the human principal, ownership MUST be the human, and provenance MUST foreground the model actor with channel an agent attempts to read an artifact outside the human's reach or to change sharing/delete the server MUST refuse — agents inherit, never exceed, the human's permissionsvia MCP channel for provenance (SPEC-0009 / ADR-0007). An agent MUST NOT reach any
artifact its human could not, MUST NOT create anything the human does not own, and
MUST NOT change sharing/expiry or delete another party's artifact (no such scope
exists).Scenario: On-behalf-of resolution
via MCPScenario: Agent attempts to exceed the human
Requirement: CLI on the Identical OAuth Flow
The CLI MUST be a public OAuth client using the same authorization-code + PKCE flow
against the same the CLI runs its login flow it MUST use the identical the CLI creates an artifact with its token the recorded channel MUST be /oauth/authorize and /oauth/token endpoints, with a loopback
redirect and PKCE mandatory. The only difference from an MCP agent MUST be the
server-assigned channel (via CLI vs via MCP, per SPEC-0009). No separate auth
mechanism may exist for the CLI.Scenario: CLI authorizes
/oauth/authorize + /oauth/token endpoints via loopback redirect + PKCE and display authorized as <email> · via MCP OAuthScenario: Channel distinguishes CLI from MCP
via CLI, assigned server-side, not via MCP
Requirement: Consent Screen Content
The authorization endpoint MUST render a consent screen naming the requesting client
("<Client> wants to connect to your Cairn workspace over MCP") and listing exactly
the three scope lines under a "THIS WILL ALLOW <CLIENT> TO:" heading, each with an
approve affordance. It MUST state that the connection is revocable ("Connected over
MCP · revoke anytime in settings"). The human MUST be authenticated before the
consent screen is shown; approval MUST create a grant limited to the checked scopes. an authenticated human reaches the consent screen for a client requesting all three scopes it MUST display exactly the three lines ("Read artifacts you can access", "Create & push new artifacts", "Comment & react on your behalf") and a revoke-in-settings notice the human denies consent no grant or token is created and the client receives an Scenario: Consent screen mirrors the three scopes
Scenario: Consent denied
access_denied authorization error
Requirement: Error Handling Standards
Errors across the MCP transport, OAuth endpoints, and the core-call boundary MUST be
wrapped with context at each layer boundary. Domain and protocol failures callers
must distinguish (invalid_grant, invalid_scope, insufficient_scope, expired token,
unknown id) MUST be represented as sentinel/typed errors and mapped to the correct
OAuth/MCP error codes; failures MUST NOT be silently swallowed. All errors MUST be
recorded with structured (key-value) logging that never logs token secrets or PKCE
verifiers. a tool call is refused for lacking a scope the server MUST return a distinct a core operation returns a domain error to an MCP tool adapter the adapter MUST wrap it with context and map it to the correct MCP/OAuth error rather than leaking or dropping itScenario: Insufficient scope surfaces distinctly
insufficient_scope error (not a generic failure) and log it structurally without secretsScenario: Layer boundary wrapping
Requirement: Database Operation Standards
Multi-step OAuth state changes (issuing a grant with its token family; refresh
rotation; revocation cascade) MUST execute in transactions so a grant is never left
half-written. All database access MUST use parameterized queries only (no string
interpolation) and explicit connection lifecycle with timeouts. Token and code
lookups MUST be constant-time / hashed-at-rest where they represent secrets. a refresh token is rotated invalidating the old token and issuing the new one MUST occur in a single transaction so a crash cannot leave two valid or zero valid refresh tokens the server looks up a grant or token by identifier it MUST use a parameterized query and never string-interpolate client inputScenario: Atomic refresh rotation
Scenario: Parameterized token lookup
Endpoint Table
Auth-by-default: every endpoint is Auth: Required unless explicitly justified
otherwise. Discovery, registration, and token/revoke endpoints are unavoidably
reachable without a Cairn bearer token because they bootstrap authorization; each
is justified below and MUST be rate-limited (see Security Requirements).
| Endpoint | Method | Purpose | Auth |
|---|---|---|---|
/.well-known/oauth-authorization-server | GET | AS metadata (RFC 8414) discovery | Public — clients must read metadata before they can authenticate; contains no secrets |
/.well-known/oauth-protected-resource | GET | Protected-resource metadata | Public — same discovery bootstrap; no secrets |
/oauth/register | POST | Dynamic Client Registration (RFC 7591) | Public — DCR must accept unregistered clients per the MCP auth spec; rate-limited, redirect-URI validated |
/oauth/authorize | GET | Render login + consent screen | Required — human session; unauthenticated visitors are redirected to login |
/oauth/authorize | POST | Submit consent (approve/deny) | Required — human session + CSRF token |
/oauth/token | POST | Code exchange, refresh rotation | Client-authenticated (PKCE) — no Cairn session; authenticated by PKCE code_verifier / client credentials |
/oauth/revoke | POST | Revoke a grant's tokens (RFC 7009) | Client-authenticated — presented token + client credentials authenticate the call |
/mcp | POST / GET (SSE) | MCP transport: tools + resource reads | Required — OAuth 2.1 bearer access token, audience-bound to Cairn |
Security Requirements
This capability is web-facing (OAuth endpoints, the consent UI, and the MCP transport). The following are MANDATORY.
Requirement: Authentication & Authorization
Mutating and workspace-scoped endpoints MUST require authentication: a human session
for the consent UI, and an OAuth 2.1 bearer access token (audience-bound to Cairn)
for the MCP transport. Every MCP tool/resource MUST enforce its required scope
( an unauthenticated client calls a mutating MCP tool (create/comment/react) the server MUST respond 401 and make no change a bearer token lacks the scope a tool requires the server MUST refuse with artifacts:read, artifacts:write, or annotations:write) before acting. Agents
MUST NOT exceed the human's permissions and MUST hold no delete/sharing scope.Scenario: Unauthenticated mutation
Scenario: Missing required scope
insufficient_scope and perform no operation
Requirement: Rate Limiting
All public and bootstrap endpoints (metadata discovery, a client exceeds the configured rate on the server MUST respond 429 with Retry-After without processing the request/oauth/register,
/oauth/token, /oauth/revoke, /oauth/authorize) and the /mcp transport MUST
be rate-limited per-identity/per-IP; exceeding a limit MUST return 429 with
Retry-After. Registration and token endpoints MUST additionally throttle to blunt
client-spraying and code/refresh brute force.Scenario: Burst on the token endpoint
/oauth/token or /oauth/register
Requirement: Security Headers
Responses from the OAuth/consent web pages MUST set a strict Content-Security-Policy,
X-Content-Type-Options: nosniff, Referrer-Policy, and (over HTTPS) HSTS. The consent
page MUST NOT reflect untrusted client-supplied strings (client name, scopes) without
contextual escaping, so a malicious registration cannot inject active content into
the consent origin. a dynamically registered client supplies a name containing HTML/script the consent page MUST escape/sanitize it so it cannot execute in Cairn's originScenario: Untrusted client name on the consent screen
Requirement: Request Body Size Limits
Every endpoint MUST enforce a maximum request size; oversize requests MUST be
rejected with 413 before buffering the full body. This applies to a create-and-push call exceeds the configured body limit the server MUST reject it with a size error and not persist a partial blob/oauth/register
payloads, token requests, and MCP create-and-push tool bodies (delegating the
artifact-body ceiling to the core, SPEC-0002).Scenario: Oversize create over MCP
Requirement: CSRF Protection
Cookie/session-authenticated state-changing requests — specifically the consent
approval POST on a consent-approval POST arrives without a valid CSRF token on the session-auth route the server MUST reject it and create no grant/oauth/authorize — MUST be CSRF-protected (token and/or SameSite
strategy). Token-authenticated MCP requests are exempt (bearer tokens are not ambient
credentials).Scenario: Cross-site consent post
Requirement: Redirect & SSRF Validation
The authorization endpoint MUST redirect only to a client's pre-registered
redirect URI (exact match; loopback allowed for native/CLI clients); no
request-supplied absolute redirect URI outside the registered set is honored. Any
delegation to an upstream identity provider MUST validate its callback and MUST NOT
follow user-supplied URLs server-side (SSRF guard). an authorization request supplies a redirect URI not matching the client's registration the server MUST reject the request and redirect nowhereScenario: Redirect to an unregistered URI
Accessibility Requirements
This capability renders a user-facing consent/login page. WCAG 2.1 AA is the minimum target.
Requirement: WCAG 2.1 AA & Semantics
The consent and login pages MUST meet WCAG 2.1 AA. Structure MUST use ARIA landmarks
(banner, main, contentinfo). The distinction between granted and denied, and between
the three scope lines, MUST NOT be conveyed by color alone — each scope MUST carry
its text label and an explicit control state. a scope's approved/denied state is shown with color an equivalent text or shape cue (checkbox state, label) MUST also be presentScenario: Color-only scope state
Requirement: Icon-Only Controls
Any icon-only control on the consent/login pages (e.g. a client-logo, an info
disclosure, a close/back affordance) MUST expose an aria-label describing its action. a control on the consent page has no visible text label it MUST expose an aria-label describing its actionScenario: Icon button on consent
Requirement: Dynamic Content Regions
Any live/validation region on the login or consent flow (e.g. an inline error such as
"consent expired", "invalid request", or a post-approval status) MUST use an
aria-live region (polite for normal updates, assertive for errors). the consent request is invalid or expired and an inline message replaces the form it MUST be announced via an aria-live regionScenario: Inline consent error
Requirement: Keyboard Navigation & Focus Management
All interactive elements on the consent/login pages MUST be keyboard-operable
(logical tab order; Enter/Space activate the approve/deny controls; Escape dismisses
any popover). If consent is presented in a modal/dialog, focus MUST move into it on
open, be trapped while open, and return to the triggering context on close. a keyboard user tabs through the consent screen every scope control and the approve/deny buttons MUST be reachable and operable via keyboard in a logical orderScenario: Keyboard-only approval