Foundry Foundry

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-server initiative; grounds the epic projects/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: the must items 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 /stop dogfood, 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_id only; 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 aud validation: AgentCore inbound authorizer (discoveryUrl = our AS, allowedAudience = the MCP resource) + in-container verifier swap (CognitoJwksAuth → AS verifier checking aud, RFC 8707). 🤖
  • Fix the read-path tenancy leak: join library_access (+ canAdmin) into getKbScopeForConnector, 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_rank to 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-prod before 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_kbs table (mirror of api_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_document tool with replace-by-key on the immutable session id: every session gets its own permanent doc; only a re-fired /stop of 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:PutObject IAM + UPLOADS_BUCKET to the AgentCore role; route writes through the shared org-checked create executor (createUploadInScope) to inherit the org backstop — no parallel write codepath. 🤖 · 🧑
  • Audit extension: writeToolCallEvent records targetType=document, targetId, a document.create event. 🤖
  • Size cap in the publish_document schema (direct-S3 path; avoid any web-Lambda 6 MB hop). 🤖
  • Wire the dev-memory /stop loop to publish via the MCP publish_document tool. 🧑 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 + autri CLI. A static-bearer MCP path duplicates that surface.
  • Arbitrary user-facing replace/delete tools — until a real memory-pruning need (the publish tool's supersede-own-by-key covers the dogfood).
  • WorkOS / Ory migration; Cognito-app-client DCR shim — killed in red-team.
  • recall as a bespoke MCP tool — deferred (composes from filter_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_usage is FK to api_keys; in Wave 2, either give connectors a usage table or relax the FK + add connector_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: /stop publish → filter_then_rank recall round-trip (🧑).

Amendments

DateChangeWhy
2026-06-24Connector-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-26Connector 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.md 2026-06-24 (session 15)
  • North Star: B3 (consumable substrate) / B6 (MCP-as-infrastructure wedge)

Review

🔒

Enter your access token to view annotations