22
33## Problem Statement
44
5+ ** Supersedes PRD-044 MCP auth approach.** PRD-044 proposed tenant-scoped Dex redirects
6+ (Option A). This PRD bypasses Dex entirely for MCP - a cleaner solution. PRD-044's BFF SSO
7+ tenant context fix and ` HandlerOptionalTenant ` middleware for Dex endpoints remain needed
8+ independently and are out of scope here.
9+
510The MCP (Model Context Protocol) OAuth flow bypasses the Meridian UI entirely and redirects
611users to the embedded Dex OIDC login page directly. This creates three problems:
712
@@ -50,15 +55,15 @@ remove the old Dex-direct flow entirely - no feature flags, no fallback.
5055 -> MCP validates client_id, PKCE, redirect_uri (unchanged)
5156 -> MCP stores OIDCFlowState {PKCE challenge, client_id, redirect_uri,
5257 state, tenant, scopes}
53- -> MCP 302 -> https://{tenant}.{baseDomain}/oauth/ consent?mcp_state=
58+ -> MCP 302 -> https://{tenant}.{baseDomain}/auth/mcp- consent?mcp_state=
5459 {key}&client_id={id}
5560 [CHANGED: was Dex redirect, now UI redirect]
5661
57623. Browser loads SPA consent page
5863 -> SPA checks sessionStorage for JWT
5964 -> If no JWT: redirect to /login with return_url
6065 -> After login (or if already logged in):
61- SPA fetches GET /oauth /consent-info?client_id=...&mcp_state=...
66+ SPA fetches GET /mcp /consent-info?client_id=...&mcp_state=...
6267 -> MCP server validates state exists, returns trusted client metadata
6368 -> SPA renders consent card with client name, redirect URI, scopes,
6469 tenant, approve/deny
@@ -118,7 +123,7 @@ remove the old Dex-direct flow entirely - no feature flags, no fallback.
118123that redirects to the UI consent page URL with ` mcp_state ` and ` client_id ` query params.
119124Store ` requested_scopes ` from the authorize request in ` OIDCFlowState ` .
120125
121- ** New endpoint ` GET /oauth /consent-info ` ** : Returns trusted client metadata (client_name,
126+ ** New endpoint ` GET /mcp /consent-info ` ** : Returns trusted client metadata (client_name,
122127redirect_uri, scopes) after validating the ` mcp_state ` exists in the state store.
123128Unauthenticated endpoint - returns display data only. Cross-checks ` client_id ` in URL
124129matches client_id in state. For dynamically registered clients, include ` is_dynamic: true `
@@ -152,10 +157,13 @@ consumption, 2-minute TTL, capped at 10,000 entries, background eviction.
152157
153158### 3. Frontend (` frontend/src/ ` )
154159
155- ** New route** : ` /oauth/consent ` in ` App.tsx ` .
160+ ** New route** : ` /auth/mcp-consent ` in ` App.tsx ` . This path avoids the Caddy
161+ ` @mcp_transport ` matcher which intercepts all ` /oauth/* ` paths and routes them to
162+ the MCP server. Using ` /auth/mcp-consent ` ensures the request falls through to the
163+ SPA catch-all.
156164
157165** New page component** : ` OAuthConsentPage ` - checks auth state, fetches client metadata
158- from ` /oauth /consent-info ` , renders consent card, handles approve/deny.
166+ from ` /mcp /consent-info ` , renders consent card, handles approve/deny.
159167
160168** New display component** : ` ConsentCard ` - shows application name, tenant context, scope
161169description, redirect URI, approve/deny buttons. For dynamically registered clients (where
@@ -166,10 +174,21 @@ login page.
166174
167175### 4. Wiring (` cmd/meridian/ ` )
168176
177+ The unified binary (` cmd/meridian ` ) runs both the BFF (api-gateway) and MCP server in
178+ the same Go process. This enables shared in-memory stores between them.
179+
169180Shared ` ConsentCodeStore ` created once and passed to both BFF's ` MCPConsentHandler ` and
170181MCP's ` OIDCHandler ` . Shared ` OIDCStateStore ` also passed to BFF handler (needed for deny
171182flow and redirect_uri lookup).
172183
184+ ** Deployment note** : The demo docker-compose runs a separate ` mcp-server ` container
185+ alongside the unified ` meridian ` container. The consent flow requires both BFF and MCP
186+ to share in-memory stores, so the consent flow runs within the unified binary only.
187+ The separate ` mcp-server ` container's OAuth endpoints are not used for the consent
188+ flow - Caddy routes ` /mcp/* ` to the MCP handler within the unified binary. If the
189+ MCP server is ever deployed as a fully separate service, the in-memory stores must
190+ be replaced with HTTP-based inter-service calls (BFF calls MCP to exchange codes).
191+
173192### 5. Cleanup
174193
175194** Remove from docker-compose env vars** : ` MCP_DEX_ISSUER_URL ` , ` MCP_DEX_CLIENT_ID ` ,
@@ -258,7 +277,7 @@ The "Unverified application" badge only appears for dynamically registered clien
258277
259278### API Contracts (Frontend View)
260279
261- ** GET ` /oauth /consent-info?client_id=...&mcp_state=... ` ** (MCP server, unauthenticated)
280+ ** GET ` /mcp /consent-info?client_id=...&mcp_state=... ` ** (MCP server, unauthenticated)
262281
263282- 200: ` { client_id, client_name, redirect_uri, scopes, is_dynamic } `
264283- 400: invalid/expired state or client_id mismatch
@@ -276,7 +295,7 @@ The "Unverified application" badge only appears for dynamically registered clien
276295### Backend
277296
2782971 . MCP ` /oauth/authorize ` redirects to UI consent page (not Dex)
279- 2 . MCP ` /oauth /consent-info ` returns trusted client metadata including ` redirect_uri ` ,
298+ 2 . MCP ` /mcp /consent-info ` returns trusted client metadata including ` redirect_uri ` ,
280299 ` scopes ` , and ` is_dynamic ` after validating state
2813003 . BFF ` POST /api/auth/mcp-consent ` requires valid JWT, issues one-time consent code
2823014 . MCP ` /oauth/callback ` accepts consent codes and cross-validates against flow state
0 commit comments