MCP Auth Server — Requirements (Scope Contract)
Status: Blue-teamed 2026-06-24; Wave 1 IN BUILD (session 16). The scope contract for the
mcp-auth-serverinitiative; grounds the epicprojects/autri/epics/mcp-auth-server. Produced via/hl:red-team(two passes — the AS design + a narrow write-surface pass, both fresh-agent-grounded against the repo/CDK/specs) →/hl:blue-team. Ladders to North Star B3/B6. Per methodology: themustitems ARE the contract; the epic reconciles to this doc; scope changes are logged in Amendments, never silent.
Shape: one epic, two waves
Blue-team decision: keep the full read and write MCP substrate in one epic, sequenced in two waves (not split into separate epics). DCR-only — CIMD and the static-bearer path are cut (see Out of scope).
- Wave 1 ships the wedge-unblocking core: frictionless DCR auth + the read surface. The risky unknowns (real Claude handshake, node-oidc-provider, AgentCore JWT acceptance) all live here.
- Wave 2 adds the write surface — the dev-memory
/stopdogfood, proving the write substrate end-to-end over MCP.
Wave 1's read-side dogfood (recall over MCP) already validates the AS; the /stop publish stays on the CLI until Wave 2.
Wave 1 — Authorization server + frictionless DCR + read surface
Goal: a real MCP client (Claude) connects to Autri's MCP server over OAuth with zero hand-minted tokens, and can read — including the structured-attribute filter_then_rank operator.
Each criterion is tagged 🤖 (AI-mechanized — the loop closes before review) or 🧑 (human-judgment smoke).
MUST
- Thin OAuth 2.1 AS on
node-oidc-provider, federating login to Cognito (us-east-1_lSk6wYeDM), minting own RS256 tokens (own JWKS; signing keys in Secrets Manager). 🤖 unit/integration · 🧑 - DCR (RFC 7591) registration — the sole launch path; covers Claude + Cursor + Copilot. 🧑 real-client handshake
- OAuth/OIDC discovery served at
…/.well-known/openid-configuration+ a JWKS endpoint. 🤖 - Login + consent interaction screens; per-client user consent before forwarding (confused-deputy MUST). 🧑
- Token lifecycle: short-lived access + refresh + revocation. Token carries
connector_idonly; scope resolved live per request, never baked into claims (red-team HIGH — the stale-grant fix). 🤖 test: revoke → next request denied - Runtime: Lambda + DynamoDB adapter, deployed in
autri-prod. 🤖 node-oidc cold-start smoke - Extract the AgentCore Runtime into its own stack (fresh-Runtime-then-delete-old; stateless, no CFN import). 🧑 verify live
- Two-layer
audvalidation: AgentCore inbound authorizer (discoveryUrl= our AS,allowedAudience= the MCP resource) + in-container verifier swap (CognitoJwksAuth→ AS verifier checkingaud, RFC 8707). 🤖 - Fix the read-path tenancy leak: join
library_access(+canAdmin) intogetKbScopeForConnector, so a revoked member's connector loses access live. (Red-team found this is a live bug today; also pre-hardens Wave 2 write.) 🤖 isolation test - Add
filter_then_rankto the MCP read surface (HTTP-only today; reuses the shared tool-def). 🤖 - Milestone 0 (do first): a real Claude DCR handshake against a minimal AS — interactive smoke before pouring infra. 🧑
- Verify the AgentCore Runtime is still live in
autri-prodbefore building. 🧑 - Connector management UI (W1-S9, amendment 2026-06-24): Settings flow to create / list / rename / repoint-library / revoke a connector and surface its MCP URL; the connector-binding model is pre-create-in-Settings (see Amendments). 🧑
SHOULD
- Redirect allow-listing (security hardening).
- Confirm Cursor/Copilot over DCR (free with DCR — just verify).
- Clean up the stale Cognito pool
us-east-1_7YgaDlZlB(console/CLI; not in CDK).
Wave 1 acceptance (the contract): Claude Desktop completes DCR against our AS, obtains a token, and calls every read tool (incl. filter_then_rank) with scope correctly enforced; revoking a connector — or a member's library_access — denies the next request; no hand-minted JWT anywhere in the flow.
Wave 2 — Write surface (dev-memory dogfood)
Goal: the dev-memory /stop loop publishes its session episode through MCP (not the CLI), proving the write substrate end-to-end — and dogfooding the thing Autri sells.
MUST
connector_write_kbstable (mirror ofapi_key_write_kbs) + extend the resolver →{ readKbs, writeKbs }with the CA-2 re-intersect against the live, revoke-checked read set (never a standalone write query). 🤖 test: a stale allowlist row never grants write- Write-allowlist issuance gated on
requireLibraryAdmin— only a library admin can grant a connector write. 🤖 publish_documenttool with replace-by-key on the immutable session id: every session gets its own permanent doc; only a re-fired/stopof the same session supersedes that session's own prior partial publish. Key = session id, never date. 🤖 test: two sessions → two docs; double-publish of one session → one doc- Integration = Option B (MCP server writes S3 + DB directly): add
s3:PutObjectIAM +UPLOADS_BUCKETto the AgentCore role; route writes through the shared org-checked create executor (createUploadInScope) to inherit the org backstop — no parallel write codepath. 🤖 · 🧑 - Audit extension:
writeToolCallEventrecordstargetType=document,targetId, adocument.createevent. 🤖 - Size cap in the
publish_documentschema (direct-S3 path; avoid any web-Lambda 6 MB hop). 🤖 - Wire the dev-memory
/stoploop to publish via the MCPpublish_documenttool. 🧑 E2E - v1 write = create/publish + supersede-own-by-key only. No arbitrary user-facing replace/delete.
Wave 2 acceptance: /stop publishes a session episode through MCP into the dev-memory KB; the episode is recall-able via filter_then_rank; a double-fired /stop yields exactly one doc for that session; a connector with an empty write-allowlist (every Brehob connector) is read-only; cross-tenant write is impossible (verified via the isolation probe extended to the write path).
Out of scope (rejected, not deferred)
- CIMD registration (+ its SSRF guard, draft-01 reconciliation) — DCR covers Claude/Cursor/Copilot today; CIMD adds only spec-alignment, not client coverage, and rests on an experimental node-oidc feature. Revisit only on real demand.
- Static-bearer / API-key interim MCP path — DCR covers the GUI clients (Claude has no static-token field anyway); scripted/programmatic access already has the consumable HTTP API +
autriCLI. A static-bearer MCP path duplicates that surface. - Arbitrary user-facing
replace/deletetools — until a real memory-pruning need (thepublishtool's supersede-own-by-key covers the dogfood). - WorkOS / Ory migration; Cognito-app-client DCR shim — killed in red-team.
recallas a bespoke MCP tool — deferred (composes fromfilter_then_rank); revisit purely for discoverability.
Open decisions (resolve during build)
- Self-host MCP server vs AgentCore Gateway over
/api/v1— the Wave 1 hosting model (inbound auth routes through our AS either way). - COGS / usage attribution for connector-driven writes —
api_key_usageis FK toapi_keys; in Wave 2, either give connectors a usage table or relax the FK + addconnector_id. - AgentCore Gateway discovery-URL mutability — confirmed mutable for Runtime; verify Gateway only if that hosting model is chosen.
Verification mechanisms (build the feedback loop, not just the feature)
- Milestone-0 Claude DCR handshake (🧑) — the gate before any infra is poured. ✅ PASSED 2026-06-24 (real Claude Desktop DCR end-to-end against the throwaway AS; AgentCore Runtime confirmed live).
- node-oidc cold-start smoke on Lambda (🤖/🧑) — confirms the runtime choice before committing. (Spike measured ~90–190 ms boot with plain node; Lambda risk is tsx transpile → esbuild pre-bundle.)
- Tenant-isolation probe (
autri/scripts/probe-tenant-isolation.sh) extended to the connector write path (🤖) — Wave 2. - Wave-2 dogfood E2E:
/stoppublish →filter_then_rankrecall round-trip (🧑).
Amendments
| Date | Change | Why |
|---|---|---|
| 2026-06-24 | Connector-binding model = pre-create-in-Settings (not pick-at-consent). A connector is a user-owned, library-scoped, editable object with its own MCP URL; the token's connector_id is resolved from the connector being authorized (ownership verified at consent); scope resolves live (W1-S5) so Settings edits apply to a connected client with no client-side change. Adds W1-S9 — Connector management UI to Wave 1. Retires the client-credentials / Cognito-app-client connector model (D34/D35, never wired) for the DCR path. | Editability: users manage / repoint / revoke connections in Autri (live), not by fiddling with Claude Desktop. Also yields "private by default" libraries — a KB is AI-reachable only once its owner mints a connector. Dan's call at build-time (session 16). |
| 2026-06-26 | Connector write resolver re-checks can_admin LIVE per request (stricter than this contract's "CA-2 re-intersect against the live read set"). getWriteScopeForConnector (W2-S1) gates writeKbs on the owner currently holding can_admin on the bound library, on top of the read-set re-intersect — so a demoted admin's connector loses write immediately, not just when the grant row is removed. Issuance (W2-S2) is admin-gated too (defense in depth). Hosting re-grounding: the self-host MCP server open decision is closed (self-host won the W1 pivot; AgentCore destroyed), so W2-S3's s3:PutObject IAM lands on the autri-mcp-server Lambda role, not an AgentCore role. | Write is an admin-level capability (only an admin can grant it), so losing admin should revoke it live — consistent with how W1-S5 already drops read on access loss. The cost over the contract-literal version is one JOIN condition. Dan's ELI5 call (session 22). |
Provenance
- Epic:
projects/autri/epics/mcp-auth-server - Red-team: 2026-06-24 — AS design pass + narrow write-surface pass (fresh-agent grounding against repo / live CDK / upstream specs)
- Decisions:
decisions.md2026-06-24 (session 15) - North Star: B3 (consumable substrate) / B6 (MCP-as-infrastructure wedge)