MCP Auth Server — Frictionless Remote MCP via a CIMD/DCR Authorization Server
Status: DRAFT for red-team / blue-team (next session). Created 2026-06-24. Ladders to North Star B3/B6 and the MCP-as-infrastructure wedge (decisions D56). Supersedes the archived MCP spike docs (
projects/autri/archive/epics/epic-1-*,archive/functional-mvp"MCP over SSE + OAuth"). Research synthesized 2026-06-24 from a 7-subagent fan-out over the AWS Bedrock AgentCore docs + the MCP auth spec + client support; sources at the end.
Why this epic exists
Autri's strategic wedge is MCP-as-infrastructure — be the MCP server that Claude / Cursor / Copilot consume, not just a web chat. Exposing the retrieval surface as MCP tools is what makes Autri "make any AI better without retraining."
Already built and proven (May 2026 spike, validated end-to-end):
@autri/mcp-doc-search— the MCP tool surface over a KB (5 tools:vector_search,fts_search,lookup_section,list_knowledge_bases,get_document).- JWT auth (
CognitoJwksAuth) + per-connector scope enforcement + audit trail. - Deployed on AWS Bedrock AgentCore Runtime (arm64 container).
- Proven working end-to-end in Claude Desktop (manual 24-hour dev JWT via the
mcp-remoteshim; clean audit rows, no errors).
The one gap (why MCP was cut from beta — D56 / D41): frictionless auth. Users today must paste a hand-minted 24-hour JWT. Real MCP clients expect OAuth with client self-registration — and Cognito (our IdP) supports neither Dynamic Client Registration (DCR, RFC 7591) nor Client ID Metadata Documents (CIMD). That gap stalled the June attempt and pushed MCP to beta+1.
What changed since June (the unlock)
- The MCP auth spec moved (2025-11-25): the client-registration priority order is pre-registration → CIMD → DCR. CIMD is
SHOULDand labeled "most common"; DCR is demoted toMAY"for backwards compatibility" — but not deprecated (grounding correction: the earlier draft of this doc overstated this). Crucially, Claude supports BOTH CIMD and DCR today, so DCR is a fully-sanctioned, production-ready path — which is exactly what lets us lead with it (see Decision). - AgentCore provides the server-side OAuth half for free (the
401+WWW-Authenticate+/.well-known/oauth-protected-resourcediscovery dance) and is IdP-agnostic — it validates JWTs against any OIDC discovery URL, including an authorization server we run. (Grounding: the inbound authorizer requires the…/.well-known/openid-configurationform plus at least one ofallowedAudience/allowedClients;node-oidc-providerserves that discovery form natively, and the authorizer config is mutable post-create viaUpdateAgentRuntime.) - We shipped the consumable API + library-scoped keys (roadmap item 2) — the data + tenancy substrate the MCP tools sit on.
Decision: build our own minimal authorization server
Build a thin OAuth 2.1 authorization server (on node-oidc-provider — OpenID-certified; do not hand-roll the crypto) that:
- Federates user login upstream to Cognito (the live app pool
us-east-1_lSk6wYeDM) — users keep one identity; we never reimplement passwords / MFA / accounts. - Mints our own MCP access tokens (own JWKS; signing keys in Secrets Manager). Required for self-owned client registration — Cognito will not issue tokens for clients it didn't register.
- DCR — the sole registration path (red-teamed 2026-06-24, blue-teamed same day).
node-oidc-provider's Dynamic Client Registration (RFC 7591) is production-solid, certified, on by default, and Claude, Cursor, and Copilot all speak DCR today — so a single DCR path covers every target client at launch. Registrations live in our own store (a DynamoDB adapter with a TTL/cleanup), so the Cognito app-client quota wall doesn't apply. - CIMD — CUT (blue-team 2026-06-24). Out of scope; DCR is the only registration path for now. CIMD would add only spec-alignment (not client coverage — every target client speaks DCR today) and rests on
node-oidc-provider's experimental, draft-01-pinned feature. Revisit on real demand. Seeprojects/autri/requirements/mcp-auth-server. - Serves OAuth discovery + JWKS. Two validation layers: AgentCore's inbound JWT authorizer points at this server's OIDC discovery URL (
…/.well-known/openid-configuration) withallowedAudience= the MCP resource; the in-container verifier re-checks the sameaud. Our AS mints tokens with thataud(RFC 8707 audience binding), and the token carriesconnector_idonly — read/write scope is resolved live per request, never baked into claims (red-team HIGH: the stale-grant fix).
Keep Cognito as the IdP; add this as the AS. DCR-only at launch; write tools arrive in Wave 2 (see Effort + sequencing).
IdP vs Authorization Server (why this split)
- Identity Provider authenticates the user (Cognito: accounts, login, MFA) — fine as-is.
- Authorization Server issues tokens and owns client registration (
/authorize,/token, discovery, DCR). Cognito's AS half lacks DCR. We add an AS that does, delegating user auth back to Cognito.
Alternatives considered (rejected)
- AWS-native AS — none exists. Cognito (no DCR/CIMD), IAM Identity Center (AWS-account-scoped SSO), Verified Permissions (policy engine, issues no tokens), API Gateway/Lambda (token validators). AWS's own documented answer to this gap is "build it yourself."
- WorkOS AuthKit/Connect (our old beta+1 plan): best registration (DCR + CIMD) but cannot keep Cognito as the user source of truth — its model replaces Cognito (migration forces password resets) or treats it only as an org-scoped SSO connection. Fails the "one identity, keep Cognito" requirement. This corrects the D56 "beta+1 = WorkOS Connect" note.
- Ory (Hydra self-host / Ory Network managed): clean "keep Cognito" fit (delegates login), DCR works today, but CIMD not yet (unscheduled). Retained as the low-ops fallback (~$70/mo + a small Claude DCR-compat proxy) if we decide not to run our own AS.
- Cognito-app-client DCR shim (Lambda creates a Cognito app client per registration; keeps Cognito-issued tokens): cannot do CIMD, and hits Cognito's app-client quota (1,000 default / 10,000 hard cap, non-adjustable create rate) — needs a reaper. Rejected as the primary path.
Architecture (draft — refine in red-team)
Red-teamed + decisions locked 2026-06-24.
- Runtime: Lambda + DynamoDB adapter (flips the original Fargate lean). An OAuth AS is stateful (auth codes, grants, sessions), but
node-oidc-providerexternalizes that through a storage adapter, so it runs on Lambda with a DynamoDB adapter and scales to zero (~$0 idle) — right for a pre-launch, ~1-user surface. Risk to smoke at milestone 0:node-oidc-providerhas a heavy init, so confirm cold-start latency on the one-time interactive login is acceptable (this codebase has been bitten by Lambda cold-starts). Fargate/ECS is the fallback if cold-start hurts — and it's net-new infra either way (no ECS/ALB today). - Stack layout: extract the AgentCore Runtime into its own dedicated stack (keeps all MCP work off the drift-sensitive
auth-and-computestack permanently). Low-risk extraction: the Runtime is stateless (pulls from ECR) and MCP is cut from beta (no users depend on it, D56), so stand up a fresh Runtime in the new stack from the same image + the new authorizer, verify, then delete the old one — no fragile CloudFormation resource-import/move needed. - Two-layer token validation: (1) AgentCore inbound authorizer →
discoveryUrl= our AS…/.well-known/openid-configuration,allowedAudience= the MCP resource; (2) in-containerAuthVerifier→ a new impl re-checking thataud. The currentCognitoJwksAuthchecksscope/client_id, notaud— so this is a new verifier impl (the auth interface is cleanly env-selected, so it's config + a small impl, not a refactor). - Tokens: RS256 JWTs with
aud= MCP resource (RFC 8707 audience binding); JWKS published at the AS; signing keys in Secrets Manager (reuse the existing secret pattern — no customer-managed KMS today; the AWS-managed default is fine for beta). - CIMD fetch (fast-follow): GET the client metadata URL (200 only, no redirects), validate
client_id== URL,redirect_uri∈ doc, JSON shape; SSRF guard (block private/loopback, HTTPS-only, optional allow-list). - Login delegation: the AS acts as an OIDC client to Cognito (
us-east-1_lSk6wYeDM); after Cognito authenticates the user, the AS mints the MCP token. - Consent:
node-oidc-providerneeds login + consent interaction views built — and the spec requires per-client user consent before forwarding (confused-deputy MUST). Real scope, not free. - MCP server:
@autri/mcp-doc-searchon AgentCore Runtime — swap its token validation fromCognitoJwksAuthto the new AS verifier (env-selected); keep the tool surface + scope enforcement + audit. - Tenancy: map the authenticated user → library scope in the minted token's claims (reuse the library-scoped model —
library_access→library_kbs).
What's reusable (do not rebuild)
- The MCP tool surface + scope enforcement + audit (built, deployed on AgentCore Runtime). Grounding correction: it's 5 tools, not 4 — 3 shared core (
vector_search,fts_search,lookup_section, defined once in@autri/retrievalso the chat / REST / MCP surfaces can't drift) + 2 MCP-only (list_knowledge_bases,get_document). - The AgentCore Runtime resource. ⚠️ Verify it's still live in
autri-prodbefore relying on it — it's defined in the rarely-deployed auth stack, so source ≠ confirmed-deployed. (It also moves to its own stack — see Architecture.) - The consumable API + library-scoped keys (data + tenancy substrate;
autri_sk_<keyId>.<secret>, library-scoped read / per-KB-allowlist write). - Cognito live pool
us-east-1_lSk6wYeDM(federate to it). Clean up the staleus-east-1_7YgaDlZlBMCP-era pool — it's not in the CDK, so that's a console/CLI delete. - The existing Secrets Manager pattern for the AS signing keys (no new KMS infra needed).
Open questions / red-team targets
Red-teamed 2026-06-24 (fresh-agent grounding against the repo, live CDK, and upstream specs). Resolved items locked into the sections above; genuinely-open items (⬜) carry to build / blue-team.
Resolved:
- ✅ Registration sequencing → DCR-first, CIMD fast-follow.
node-oidc-providerDCR is production-solid + Claude/Cursor/Copilot all speak it; its CIMD is experimental/draft-01. - ✅ AS runtime → Lambda + DynamoDB adapter (scales to zero; Fargate is the cold-start fallback).
- ✅ Auth-stack touch → extract AgentCore to its own stack via fresh-Runtime-then-delete-old (stateless, no CFN import dance).
- ✅ AgentCore discovery URL → mutable on Runtime (
UpdateAgentRuntimeacceptsauthorizerConfiguration); must serve the OIDCopenid-configurationform + setallowedAudience. - ✅ Token-audience binding → mint
aud= MCP resource (RFC 8707), validated at both layers (AgentCore authorizer + in-container verifier). - ✅ Tenancy mapping → reuse library-scope; mint the library scope into the token claims.
Still open (build / blue-team):
- ⬜ Milestone 0 = a real Claude DCR handshake (interactive smoke) BEFORE pouring infra. Claude silently falls back CIMD→DCR unless the AS advertises both
client_id_metadata_document_supportedandnone, so prove the end-to-end flow first. - ⬜ Self-host MCP server vs AgentCore Gateway over
/api/v1— hosting model undecided (inbound auth routes through our AS either way). - ⬜ AgentCore Gateway discovery-URL mutability — confirmed for Runtime; Gateway unverified.
- ⬜
node-oidc-providerCIMD draft reconciliation (draft-01 vs the spec's referenced draft) — for the fast-follow. - ⬜ Lambda cold-start of
node-oidc-provideron the interactive flow — smoke before committing to Lambda. - ⬜ Consent-screen UX + confused-deputy per-client consent (spec MUST).
- ⬜ Security hardening: SSRF on the CIMD metadata fetch, signing-key custody, redirect allow-listing.
Effort + sequencing
Blue-teamed 2026-06-24 → scope contract: projects/autri/requirements/mcp-auth-server. One epic, two waves, DCR-only (CIMD + static-bearer cut — see the requirements doc's Out-of-scope).
- Milestone 0 (do first): a real Claude DCR handshake against a minimal AS — interactive smoke, the recurring lesson. Everything else is sized only after this proves out.
- Wave 1 — AS + frictionless DCR auth + read surface. The wedge-unblocking core; carries the risky unknowns (real Claude handshake, node-oidc-provider, AgentCore JWT acceptance). Includes the token-
connector_id-only design, two-layeraudvalidation, the AgentCore stack extraction, the read-path tenancy-leak fix, andfilter_then_rankon the read surface. Read-side dogfood = recall over MCP. - Wave 2 — write surface (dev-memory dogfood).
connector_write_kbs+ hardened resolver,publish_document(replace-by-key on the immutable session id), Option-B direct-S3 integration (s3:PutObject IAM on the AgentCore role, route through the shared org-checked executor), audit extension, and wiring/stopto publish via MCP. v1 write = create/publish + supersede-own-by-key only; arbitrary replace/delete deferred. - The earlier ~1–1.5wk estimate is superseded — the write surface alone carries four HIGH red-team findings' worth of hardening. Size against the requirements doc's per-wave acceptance criteria, not a single number.
- MCP is Autri-product leverage, not on the Brehob critical path — but it compounds into Brehob (the same hardened API + auth substrate powers dev-memory recall and QuoteAI's consumption of Autri). Being ahead on the Brehob substrate makes this the right window.
Stories — dependency-ordered (Wave 1 + Wave 2)
Refined 2026-06-24 from the requirements contract (projects/autri/requirements/mcp-auth-server). IDs are stable. → = depends on. Each story names its check: 🤖 mechanized (loop closes before review) / 🧑 human-smoke. Ship via /hl:ship wave-by-wave in the topo order at the end.
Wave 1 — AS + frictionless DCR + read surface
- W1-S0 — Milestone-0 vertical-slice spike (THE GATE). ✅ GATE PASSED 2026-06-24 — real Claude Desktop completed DCR end-to-end against a throwaway
node-oidc-providerAS + minimal MCP server and called a read tool; AgentCore Runtime confirmed LIVE inautri-prod(autri_mcp_doc_search-F2ukq3HHqR, READY, authorizer currently → Cognito, so mutability for S7 is confirmed). Throwaway spike on branchw1-s0-milestone-spike(draft PR #73, do-not-merge). → nothing. Unblocked S1+. 🧑 interactive handshake. - W1-S5 — Read-path tenancy fix. ✅ Shipped (
9c934fb, PR #71). Joinedlibrary_accesson(library_id, user_id)intogetKbScopeForConnector(retrieval/src/scope.ts) so a revoked member's connector loses read access live; reads NOT gated oncan_admin(that's the W2 write resolver's concern). Verified by scope tests (leak + non-admin cases) + ci. → nothing. 🤖 isolation test (scripts/probe-tenant-isolation.sh). - W1-S6 —
filter_then_rankon the MCP surface. ✅ Shipped (3bfb2e8, PR #72). (The epic's original "~1 line, add toCORE_RETRIEVAL_TOOLS" premise was wrong —filter_then_rankis a per-KB factorybuildFilterThenRankTool, deliberately excluded from the core set; it's registered per-connector inmcp-servers/doc-search/src/tools.ts, loading each in-scope KB's attribute schema, mirroring the v1/RESTrunFilterThenRankpattern. Now the 6th MCP tool.) → nothing. 🤖 unit + live retrieval call. - W1-S1 — node-oidc-provider AS (production form). DCR, Cognito federation, RS256 + own JWKS, signing keys in Secrets Manager, OIDC discovery at
…/.well-known/openid-configuration, DynamoDB adapter, token carriesconnector_idonly — resolved from the user-owned connector being authorized (selected via the connector-scoped MCP URL; ownership verified at consent), withaudkept to a single canonical MCP resource (so S7's AgentCoreallowedAudiencestays fixed). → S0. 🤖 conformance/integration tests. - W1-S3 — Login + consent interaction screens. node-oidc-provider interaction views: login delegation to Cognito + per-client consent (confused-deputy MUST), confirming the connector → library being granted and verifying the logged-in user owns that connector. → S1. 🧑.
- W1-S4 — Token lifecycle. Short-lived access + refresh + revocation; scope resolved live per request, never from claims. → S1. 🤖 test: revoke → next request denied.
- W1-S2 — AS runtime: Lambda + DynamoDB, deploy to
autri-prod. Package for Lambda + the DynamoDB adapter; cold-start smoke of node-oidc init on the interactive flow; deploy. → S1. 🤖 cold-start measure · 🧑. - W1-S7 — AgentCore extraction + two-layer
audvalidation. Extract the AgentCore Runtime to its own stack (fresh-Runtime-then-delete-old;autri-infra/lib/auth-and-compute/agentcore.ts); set the inbound authorizer (discoveryUrl= our AS,allowedAudience= the MCP resource) viaUpdateAgentRuntime; swap the in-container verifierCognitoJwksAuth→ AS verifier checkingaud(mcp-servers/doc-search/src/auth.tsbuildAuthVerifier). → S2. 🤖 + 🧑. - W1-S9 — Connector management UI (Settings). Rebuild
app/app/settings/connectorson the new model: create / list / rename / repoint-library / revoke a connector, and surface its unique MCP URL to paste into Claude. A connector is a user-owned, library-scoped, editable object; scope resolves live (W1-S5) so an edit hits an already-connected client with no client-side change. Retires the client-credentials/Cognito-app-client connector model (D34/D35, never wired) for the DCR path. → S1. 🧑. (Added 2026-06-24 — blue-team amendment: pre-create-in-Settings connector model.) - W1-S8 — Wave-1 integration + acceptance. E2E: create a connector in Settings (S9) → Claude → DCR → token → extracted MCP stack → all read tools (incl.
filter_then_rank) scope-enforced; revoke connector /library_access(or repoint the connector) → next request reflects it; zero hand-minted JWTs. → S1, S2, S3, S4, S5, S6, S7, S9. 🧑 (the Wave-1 acceptance contract).
Wave 2 — write surface (dev-memory dogfood)
- W2-S1 —
connector_write_kbs+ resolver write scope. New table (mirrorapi_key_write_kbs,app/lib/db/schema.ts:440); extend the resolver →{ readKbs, writeKbs }with the CA-2 re-intersect against the live, revoke- andlibrary_access-checked read set (builds on W1-S5). → W1-S5. 🤖 test: a stale allowlist row never grants write. - W2-S2 — Write-allowlist issuance (admin-gated). The action to grant a connector write to a KB, gated on
requireLibraryAdmin. → W2-S1. 🤖. - W2-S3 — AgentCore S3 write access (Option B infra). Add
s3:PutObjectIAM +UPLOADS_BUCKETto the AgentCore Runtime role (its own stack from W1-S7). → W1-S7. 🤖 + 🧑. - W2-S4 —
publish_documenttool (replace-by-key on session id). MCP write tool: check write scope → route through the shared org-checkedcreateUploadInScope(app/lib/documents/create-upload.ts) → PUT to S3 directly → replace-by-key on the immutable session id (portreplaceByName,api-client/src/client.ts:181); size cap. → W2-S1, W2-S3. 🤖 test: two sessions → two docs; double-publish of one session → one doc. - W2-S5 — Audit extension.
writeToolCallEvent(mcp-servers/doc-search/src/audit.ts) recordstargetType=document,targetId,document.create. → W2-S4. 🤖. - W2-S6 — Wire
/stopto MCP + Wave-2 acceptance. Switch the dev-memory/stoppublish from the CLI to the MCPpublish_documenttool; E2E:/stop→ episode lands → recall-able viafilter_then_rank; double-fire → one doc; empty-allowlist (Brehob) connector = read-only; isolation probe extended to the write path. → W2-S4, W2-S5. 🧑 dogfood E2E.
Open (decide in build): self-host MCP vs AgentCore Gateway (affects S7); COGS attribution for connector writes (api_key_usage FK to api_keys — affects S4/S5).
Ship order (topo): W1-{S5, S6} land immediately · S0 gates · S1 → {S2, S3, S4, S9} → S7 → S8 · then W2: S1 → {S2, S3} → S4 → S5 → S6.
Sources (key)
- MCP Authorization spec 2025-11-25 (priority: pre-registration → CIMD → DCR; DCR optional/backwards-compat, not deprecated): https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
- AgentCore inbound JWT authorizer (IdP-agnostic; needs
…/.well-known/openid-configuration+allowedAudience): https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html - AgentCore
UpdateAgentRuntime(authorizer is mutable post-create): https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_UpdateAgentRuntime.html - AgentCore Runtime — hosting MCP servers: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-mcp.html
- Cognito lacks DCR/CIMD (runtime-oauth note): https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html
- CIMD (IETF draft + explainer): https://oauth.net/2/client-id-metadata-document/ · https://workos.com/blog/client-id-metadata-documents-cimd-oauth-client-registration-mcp
- node-oidc-provider (OpenID-certified; DCR core, CIMD experimental/draft-01): https://github.com/panva/node-oidc-provider
- Claude connector authentication (supports BOTH CIMD + DCR today; CIMD gated on AS advertising
client_id_metadata_document_supported+none): https://claude.com/docs/connectors/building/authentication - WorkOS migrate-from-Cognito (replaces Cognito; forced password resets): https://workos.com/docs/migrate/aws-cognito
- Ory Hydra delegated login (keep-Cognito fit; DCR yes, CIMD not yet): https://www.ory.com/docs/hydra/guides/oauth2-clients
- Community DCR-shim references: https://github.com/stache-ai/agentcore-dcr · https://jonathanpenny.com/blog/oauth-dcr-aws-bedrock-agentcore