Summary
The OAuth Server consent screen rendered by generateAuthorizeHtml (src/controllers/oauthServerController.ts) is a self-contained inline-HTML page with its own hardcoded styles, completely separate from the React dashboard's design system. Side by side with /login (Better Auth, dashboard-styled) or any of the Dashboard pages, it looks like a different product. For a screen whose entire purpose is to be a trust signal — "yes, this is the same MCPHub you've been administering, this consent prompt is legitimate" — the visual jump is actively counter-productive and gives a phishing replica a meaningfully easier bar to clear.
This is filed as a separate concern from the consent-screen content additions tracked in #821, because the two are orthogonal: the content fix doesn't depend on visual unification and vice versa.
What the consent screen currently uses
In generateAuthorizeHtml:
- An inline
<style> block with hardcoded colors (#eef5ff container background, #23408f title, #2563eb approve button, etc.).
- A separate font stack (
-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) declared inline; no relation to the SPA's font setup.
- No reference to any shared design tokens, no Tailwind utilities, no link to the bundled SPA CSS.
- A custom button shape, custom spacing, custom border radius — all duplicated rather than reused.
The React dashboard (Tailwind-based) has its own design tokens for the same things. The result is that /login (LoginPage.tsx) renders within the dashboard's visual language but /oauth/authorize does not, even though they're surfaces of the same authentication flow.
Why it matters
-
Trust signal. The consent screen is the most security-critical surface in the entire product: the moment where a user grants long-lived access to their MCP servers. A user who has spent any time in the dashboard learns its visual language. When the consent screen looks unrelated to that language, two things happen:
- The screen reads as "tossed-off" or "third-party," reducing the credibility of the approval ask.
- A phishing replica only has to imitate the inline page, which is a much smaller target than imitating the entire dashboard. The trust gain of "this looks like the dashboard I know" disappears.
-
Inconsistency with adjacent auth surfaces. Better Auth's /login page already uses the dashboard's design system (it's part of the SPA). The OAuth Server consent flow is the only auth-related surface that drops out of the design system, which makes the inconsistency even more jarring than it would be for a one-off page.
-
Maintenance drift. Every dashboard theme/branding change will leave the consent screen behind unless someone remembers to also touch this template. The two surfaces will diverge over time absent active intervention.
Implementation directions
Three sketches in increasing-invasiveness order; happy to be told which the project prefers:
-
(α) Same inline-render, dashboard tokens. Pull the dashboard's design tokens (the same color / spacing / font variables Tailwind compiles from) into a small shared CSS string that both generateAuthorizeHtml and the React app reference. Lowest disruption; gets ~80% of the visual parity. The page stays server-rendered, so the auth flow's coupling to the SPA build doesn't change.
-
(β) Same inline-render, link to bundled dashboard CSS. generateAuthorizeHtml emits a <link rel="stylesheet" href="${assetsPath}/dashboard.css"> (or whatever name the build produces) and styles its container with dashboard utility classes. The consent page is still server-rendered, but visually identical to the SPA. Introduces an implicit dependency on the SPA bundle being available at render time, which is already true in any deployment that serves the dashboard.
-
(γ) Move consent into the React SPA. GET /oauth/authorize serves a minimal HTML shell that boots the SPA at a route like /oauth/consent?.... The React page renders the consent UI using existing components and POSTs back to /oauth/authorize. Most invasive but the long-term right answer — same path Better Auth already takes for /login. Has the side benefit of letting the consent UI use the same i18n, accessibility, and component primitives as everything else.
(α) is the obvious "small PR, big visual win" option to land first. (β) is a midpoint if the dashboard's Tailwind output isn't easy to extract as discrete tokens. (γ) probably warrants its own RFC if anyone wants to take it on.
Out of scope (filed / will file separately)
Filing for visibility — realistically can't commit to a PR on this one, but happy to bikeshed any of the three directions if that's useful.
🤖 Generated with Claude Code
Summary
The OAuth Server consent screen rendered by
generateAuthorizeHtml(src/controllers/oauthServerController.ts) is a self-contained inline-HTML page with its own hardcoded styles, completely separate from the React dashboard's design system. Side by side with/login(Better Auth, dashboard-styled) or any of the Dashboard pages, it looks like a different product. For a screen whose entire purpose is to be a trust signal — "yes, this is the same MCPHub you've been administering, this consent prompt is legitimate" — the visual jump is actively counter-productive and gives a phishing replica a meaningfully easier bar to clear.This is filed as a separate concern from the consent-screen content additions tracked in #821, because the two are orthogonal: the content fix doesn't depend on visual unification and vice versa.
What the consent screen currently uses
In
generateAuthorizeHtml:<style>block with hardcoded colors (#eef5ffcontainer background,#23408ftitle,#2563ebapprove button, etc.).-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) declared inline; no relation to the SPA's font setup.The React dashboard (Tailwind-based) has its own design tokens for the same things. The result is that
/login(LoginPage.tsx) renders within the dashboard's visual language but/oauth/authorizedoes not, even though they're surfaces of the same authentication flow.Why it matters
Trust signal. The consent screen is the most security-critical surface in the entire product: the moment where a user grants long-lived access to their MCP servers. A user who has spent any time in the dashboard learns its visual language. When the consent screen looks unrelated to that language, two things happen:
Inconsistency with adjacent auth surfaces. Better Auth's
/loginpage already uses the dashboard's design system (it's part of the SPA). The OAuth Server consent flow is the only auth-related surface that drops out of the design system, which makes the inconsistency even more jarring than it would be for a one-off page.Maintenance drift. Every dashboard theme/branding change will leave the consent screen behind unless someone remembers to also touch this template. The two surfaces will diverge over time absent active intervention.
Implementation directions
Three sketches in increasing-invasiveness order; happy to be told which the project prefers:
(α) Same inline-render, dashboard tokens. Pull the dashboard's design tokens (the same color / spacing / font variables Tailwind compiles from) into a small shared CSS string that both
generateAuthorizeHtmland the React app reference. Lowest disruption; gets ~80% of the visual parity. The page stays server-rendered, so the auth flow's coupling to the SPA build doesn't change.(β) Same inline-render, link to bundled dashboard CSS.
generateAuthorizeHtmlemits a<link rel="stylesheet" href="${assetsPath}/dashboard.css">(or whatever name the build produces) and styles its container with dashboard utility classes. The consent page is still server-rendered, but visually identical to the SPA. Introduces an implicit dependency on the SPA bundle being available at render time, which is already true in any deployment that serves the dashboard.(γ) Move consent into the React SPA.
GET /oauth/authorizeserves a minimal HTML shell that boots the SPA at a route like/oauth/consent?.... The React page renders the consent UI using existing components and POSTs back to/oauth/authorize. Most invasive but the long-term right answer — same path Better Auth already takes for/login. Has the side benefit of letting the consent UI use the same i18n, accessibility, and component primitives as everything else.(α) is the obvious "small PR, big visual win" option to land first. (β) is a midpoint if the dashboard's Tailwind output isn't easy to extract as discrete tokens. (γ) probably warrants its own RFC if anyone wants to take it on.
Out of scope (filed / will file separately)
resourceparameter, RFC 7591 metadata, clientId disambiguation) — tracked in UX: OAuth Server consent screen omits the target MCP server (resource), client identity, and RFC 7591 metadata #821. The visual unification can land first or after; the content additions can ship in the current inline template and then be carried into whichever visual approach is chosen.Filing for visibility — realistically can't commit to a PR on this one, but happy to bikeshed any of the three directions if that's useful.
🤖 Generated with Claude Code