Foundry Foundry

MCP Hosting Pivot — Self-host doc-search (replacing AgentCore Runtime)

Status: DECISION LOCKED (red-teamed 2026-06-26, session 21). Self-host is the decision; Gateway is the rejected fallback. Build next session, Milestone-0 smoke gated. Forces + closes the long-open "self-host vs Gateway" question in projects/autri/epics/mcp-auth-server (Open questions; Wave-2 "decide in build"). Ladders to the MCP-as-infrastructure wedge (D56) and North Star B3/B6.

The forcing function — direct AgentCore Runtime is blocked for MCP auth

W1-S8 chased one wall all session: every gateway-forwarded MCP request reaches our container header-less, so the container 401s. This session confirmed the root cause is structural, not a config slip:

  • The requestHeaderConfiguration allowlist (["Authorization", "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Token"]) is present and correct on the live runtime — verified via get-agent-runtime. Misconfiguration ruled out.
  • A brand-new freshly-CREATED runtime (autri_mcp_doc_search_v2, created 00:29:59Z) shows the identical strip: [as-diag] hasAuthHeader=false ... headers=[accept,content-length,content-type,host], with zero hasAuthHeader=true lines ever. The gateway authorizer validates the token before forwarding, so every logged line is an already-authenticated request that arrived with the token gone. Kills the CREATE-vs-UPDATE hypothesis.
  • Research verdict (AWS docs): the header-allowlist + context.request_headers mechanism is documented only for HTTP-protocol runtimes. MCP-protocol runtimes have no context object and no documented header forwarding; our symptom matches the unresolved AWS samples issue #1030. GetWorkloadAccessTokenFor* are outbound identity (Token Vault), not the inbound caller's sub.

Conclusion: there is no documented, working way for an MCP-protocol AgentCore Runtime container to read the caller's identity. Our in-container per-user authz design cannot run on direct AgentCore Runtime. Option A is dead.

Decision (LOCKED) — self-host doc-search on Lambda Function URL + CloudFront

Retire the AgentCore Runtime for MCP. Host @autri/mcp-doc-search on a Lambda Function URL (InvokeMode: BUFFERED) behind CloudFront at mcp.autri.ai, cloning the live autri-auth-server stack. Inbound auth still routes through our AS (mcp-oauth.autri.ai); the container reads Authorization directly and runs the existing AutriAsAuth validation. This is option C.

Why C over B (AgentCore Gateway) — Gateway REJECTED

C — Self-host (Lambda+CloudFront) — CHOSENB — AgentCore Gateway + interceptor Lambda — REJECTED
Caller identity to containerAuthorization arrives natively (AllViewerExceptHostHeader, proven by the auth-server).JWT_PASSTHROUGH is HTTP-target-only; an interceptor Lambda must re-inject Authorization. Extra hop.
Existing container codeWorks unchanged (minus the smuggle/diag hacks).Works, but behind Gateway's tool re-cataloging.
Wedge fit (MCP passthrough)Transparent passthrough.Gateway re-catalogs tools/list; not transparent; DYNAMIC mode loses semantic search/3LO.
New infraA clone of the in-prod auth-server stack.Gateway + interceptor Lambda + still needs a host.

B is retained only as a paper fallback if a specific need for AgentCore governance/observability ever emerges.

Grounding corrections from red-team (the draft overstated the risk)

The original draft listed VPC/RDS/NAT/streaming as new unknowns. Grounding against auth-server-stack.ts shows they are already solved in prod:

  • C1 — VPC + RDS is NOT a new delta. The auth-server is already VPC-attached (PRIVATE_WITH_EGRESS, shared appSecurityGroup carrying the RDS :5432 ingress, NAT for egress) and already queries RDS (the consent-gate). doc-search needs the identical setup — a clone, not new infra.
  • C2 — no streaming needed. doc-search sets enableJsonResponse: true (server.ts:186) → buffered JSON, not SSE; the auth-server uses InvokeMode.BUFFERED. We use BUFFERED. The streaming-throughput-cap risk is gone; the only real constraint is the 6 MB Function-URL buffered-response cap (revisit with RESPONSE_STREAM only if get_document hits it — model-token streaming is the client's concern, unaffected by our invoke mode).

What's preserved (do NOT rebuild)

Only the hosting layer changes. Untouched:

  • The AS (mcp-oauth.autri.ai, autri-auth-server stack) and the entire OAuth/DCR handshake — proven end-to-end this session.
  • The connector model + Settings UI (W1-S9) + the consumable substrate.
  • The container code: mcp-servers/doc-search/src/auth.ts (AutriAsAuth), server.ts, tools.ts, the 6 tools, scope enforcement, audit.
  • The branded resource/aud design (https://mcp.autri.ai/mcp, RFC 8707) + the AS's canonical-aud minting.

Architecture (red-teamed)

  • New stack autri-mcp-server = a clone of auth-server-stack.ts: VPC-attached Lambda (PRIVATE_WITH_EGRESS, shared appSecurityGroup, NAT egress), Function URL (authType NONE, InvokeMode.BUFFERED) → CloudFront → mcp.autri.ai alias + cert. NodejsFunction/esbuild CJS bundle, @aws-sdk/* externalized. X-Forwarded-Host/Proto pinned + AllViewerExceptHostHeader + CACHING_DISABLED (the proven combo).
  • H1 — Lambda-ify the Hono app. doc-search currently runs as a long-running server (serve({fetch: app.fetch}), server.ts:273). Add a Lambda handler (hono/aws-lambda handle, or mirror the auth-server's lambdaHandler). Top-level await buildPoolConfig() means cold-start pays the Secrets fetch + pool connect (same cold-start the auth-server measures).
  • H2 — wire OPENAI_API_KEY. vector_search/hybrid_search call api.openai.com (vector-search.ts:41-50) and need the key + NAT egress. It was absent from the AgentCore runtime env, so those tools were already broken in prod — fix as part of the pivot (inject from Secrets Manager).
  • Auth, simplified: container reads Authorization directly → AutriAsAuth checks sig/iss/aud/scope → sub + connector_id → live ownership assertion (existing server.ts logic). Drop the custom-header smuggle, the discovery shim's token-copy branch, and both diagnostics ([as-trace], [as-diag]).
  • M2 — discovery / 401, now owned cleanly: we own the origin, so the MCP server serves the correct PRM (resource = branded) and the 401 WWW-Authenticate with the right resource_metadata directly from the app — eliminating the CloudFront discovery-shim Function (which couldn't even emit the 401 on a 4xx).
  • M1 — defense-in-depth: no gateway authorizer means every request (incl. bad tokens, probes) executes the Lambda before the container rejects. A cost/DoS surface, not an authz hole (the container fully validates). Fine at ~1-user beta; add a CloudFront throttle/WAF later.

Migration plan — Milestone-0 GATED (H3)

  1. Build autri-mcp-server (clone the auth-server stack); deploy on the raw *.cloudfront.net (no alias).
  2. GATE: real Claude handshake → self-hosted Lambda → a tool call returns, against the raw domain. Do NOT proceed until this is green ([[feedback_interactive_flows_need_interactive_smoke]]). AgentCore (RuntimeV2) stays live as the rollback until this passes.
  3. Repoint mcp.autri.ai CNAME → new distribution; redeploy with alias.
  4. Verify end-to-end (DCR → token → tool call, scope-enforced).
  5. cdk destroy AgentCore; remove diagnostics; delete the agentcore stack + its now-unused cert; revert the uncommitted RuntimeV2 rename.

Queued (apply on build / at /stop)

  • L2 — monitoring: the autri-monitoring stack is name-keyed to autri_mcp_doc_search (now deleted; we deployed _v2). Reconcile — drop the AgentCore alarms as part of the teardown.
  • L3 — epic re-scope: S7 (AgentCore extraction) is obsoleted; S8 acceptance reframes around the self-hosted stack; flip the epic's two "self-host vs Gateway — decide in build" open items to "decided: self-host." Apply to projects/autri/epics/mcp-auth-server next session.

Sources (key)

Review

🔒

Enter your access token to view annotations