Skip to content

feat(mcp): widget preview fixtures + tenant branding for widgets - #572

Merged
guillermoscript merged 2 commits into
masterfrom
feat/mcp-widget-fixtures-branding-571
Jul 27, 2026
Merged

feat(mcp): widget preview fixtures + tenant branding for widgets#572
guillermoscript merged 2 commits into
masterfrom
feat/mcp-widget-fixtures-branding-571

Conversation

@guillermoscript

@guillermoscript guillermoscript commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Two things for the MCP App widgets:

  1. A way to look at them. MCP_DEMO_WIDGETS=1 registers one lms_demo_<widget> tool per widget, serving hand-written fixtures — all 18 widgets, 44 variants covering empty lists, null names, ungraded rows, locked lessons, unpublished pages. No database, no seed, no OAuth.
  2. School branding. Widgets now theme off tenants.primary_color, the same source the app uses, instead of hardcoding violet.

Enables #571 (the findings it produced are filed as #566#570). Does not close it.

Why

Widget props came only from live Supabase reads, so seeing a widget meant a seeded DB, an OAuth login, and finding a row that happened to hit the branch you were working on. Edge states were near-impossible to produce on demand — which is why several of the bugs in #566#570 had been sitting in the widgets unnoticed. They all showed up within minutes of rendering against deliberately awkward fixtures.

Branding was the obvious gap once the widgets were visible side by side with the app: the app injects the tenant's colours as CSS custom properties (components/tenant/tenant-css-vars-server.tsx), and widgets — rendering in a host iframe that never sees that markup — stayed platform violet.

How it works

Fixturesmcp-server/src/demo-data.ts (data only, never imported by a production tool) and src/tools/demo.ts. The flag is gated on MCP_DEMO_WIDGETS=1 and NODE_ENV !== production (src/env.ts, demoWidgetsEnabled()).

Three deliberate dev-only consequences of the flag, all documented in mcp-server/docs/WIDGET_DEMO_DATA.md:

  • OAuth is not installed — bearer auth on /mcp would 401 the inspector before any handler ran.
  • Demo tools register before installToolGuards(), so the role gate doesn't reject them (a session with no login has no tenant role) and they never write to mcp_audit_log.
  • tools/list exposes only the 18 demo tools; every real tool still refuses to run (LmsSession.fromContext throws).

Brandingsrc/branding.ts reads name / logo_url / primary_color / secondary_color on the caller's RLS-scoped client, cached 5 min per tenant (misses cached too). installToolGuards attaches it to _meta on any result carrying structuredContent, so all 18 widget tools are themed without touching a single handler. resources/shared/branding.tsx reads useWidget().metadata and emits a --brand-50..950 ramp derived from the one colour with color-mix() in oklab. Tenant-controlled colour is allow-listed before it reaches a <style> tag.

With no tenant colour it emits Tailwind's violet ramp verbatim, so unbranded schools are pixel-identical to before.

Two things a reviewer should know

  • mcp-server/resources/styles.css has never been part of the widget build. mcp-use dev/build generates its own Tailwind entry per widget at .mcp-use/<widget>/styles.css and imports that; the CLI exposes no hook to add your own. Everything in that file — the body/h1/a base rules, the carousel classes — has been dead. That is why the ramp uses arbitrary-value utilities (bg-[var(--brand-600)]) rather than a brand-600 theme colour. Left in place with a warning comment rather than deleted in this PR; happy to remove it if you'd rather.
  • Status colours are deliberately not themed. The red/amber/green mastery bars and the at risk / published pills encode meaning, not brand. Under a forest-green brand the buttons theme and the mastery bars stay red/amber/green — that's intended, not a miss (QA step 3).

How to QA

No db:reset needed — that's the point of the change.

cd mcp-server
MCP_DEMO_WIDGETS=1 npm run dev
# → http://localhost:3000/inspector?autoConnect=http%3A%2F%2Flocalhost%3A3000%2Fmcp
  1. Tool list shows exactly 18 lms_demo_* tools and nothing else.
  2. Run any of them; the widget renders in the response pane (⤢ for full width). Try variant=empty / locked / broken-mdx for the edge states.
  3. Set brand=ocean (or sunset / forest) and re-run — accents follow the colour, mastery bars stay red/amber/green.
  4. Set brand=none and confirm it's the original violet.
  5. Render everything to PNG, both themes:
    ./scripts/shoot-demo-widgets.sh dark
    ./scripts/shoot-demo-widgets.sh light
    42 files each, demo-shots/<theme>/ (gitignored).

Confirm the flag is genuinely off by default — the part worth checking hardest:

cd mcp-server && npm run dev      # no flag

Expect: no [demo] Registered … line, [OAuth] Bearer authentication enabled present, and

curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:3000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}'

401.

Screenshots

Not embedded — the renders are reproducible in one command (step 5 above) and demo-shots/ is gitignored. Say the word and I'll attach the before/after branding pair.

The visual result: identical violet unbranded → ocean #0369a1 / sunset #e11d48 / forest #15803d applied to accents across light and dark, with status colours unchanged.

Live path verified

The real path — genuine Supabase session → RLS read of tenants → branding in _meta — has now been exercised against local Supabase.

Seeded code-academy with a deliberately un-violet colour so the result couldn't be ambiguous (primary_color = #0f766e, secondary_color = #f59e0b; it ships as #7c3aed, which is indistinguishable from the platform default).

Case Result
Admin (creator@codeacademy.com), lms_list_courses _meta['lms/branding'] present with the seeded teal; the widget's filter chip renders teal, published pills stay green
Student (alice@student.com), lms_my_learning branding present — the tenants RLS policy admits any tenant member, so student widgets are themed too
Same tenant with primary_color/secondary_color/logo_url all NULL _meta is [], branding absent, widget falls back to the platform violet

Run with OAuth on and MCP_DEMO_WIDGETS unset, so this is the production code path, not the demo one.

Checklist

  • npm run typecheck and npm run test:unit pass — root suite 594/594 (48 files), mcp-server 39/39 (4 new in src/branding.test.ts)
  • npm run build passes — mcp-server production build green, 18 widgets built; the brand utilities compile into the built CSS (10 distinct var(--brand-*)) and the ramp ships in 42 built JS bundles. Root Next build not run: no files outside mcp-server/ and tests/unit/ changed
  • Every new tenant-scoped query filters by tenant_id — the one new query (branding.ts) filters tenants.id on the caller's RLS client
  • Tested with every relevant role — branding verified live as admin and as student (see above); demo tools are role-less by design
  • Loading and error states handled — <Brand /> renders in every widget's pending and loaded branch; branding failures degrade to the default palette
  • New UI strings added to messages/en.json / es.json — N/A here, and tracked as a real gap: widgets have no i18n at all (MCP widget UX polish: i18n, colour semantics, CTAs, unused payload data, card alignment #570)
  • Migration — none

🤖 Generated with Claude Code

https://claude.ai/code/session_017sLtN12VfnLziCpst3Pfo2

Two things the MCP widgets were missing: a way to look at them, and the
school's own colours.

**Preview mode.** Widget props came only from live Supabase reads, so seeing
a widget needed a seeded DB, an OAuth login, and a row that happened to hit
the branch you cared about — and edge states (empty lists, locked lessons,
null scores, unpublished pages) were near-impossible to produce on demand.
`MCP_DEMO_WIDGETS=1` registers one `lms_demo_<widget>` tool per widget
serving hand-written fixtures for all 18, 44 variants covering the awkward
cases. The flag is hard-gated on a non-production NODE_ENV; with it unset
nothing registers, OAuth is installed as before and unauthenticated /mcp
still 401s. Fixtures are also now the only committed example of each
widget's payload, so a propsSchema change has to update them.

**Tenant branding.** Widgets hardcoded violet while the app themes off
`tenants.primary_color` (components/tenant/tenant-css-vars-server.tsx).
The server now attaches branding to the tool result's `_meta` centrally in
installToolGuards — no handler changes — and the widget derives a
`--brand-50..950` ramp from that one colour with color-mix(). With no tenant
colour it emits Tailwind's violet ramp verbatim, so unbranded schools are
pixel-identical to before.

The ramp is CSS custom properties plus arbitrary-value utilities
(`bg-[var(--brand-600)]`) rather than a Tailwind theme colour because
mcp-use generates its own per-widget Tailwind entry under .mcp-use/ and
offers no hook to add one: resources/styles.css has never been part of the
widget build. Noted in the file.

Status colours (red/amber/green mastery bars, at-risk and published pills)
are deliberately left unthemed — they carry meaning, not brand.

Findings from the review pass this enabled are tracked in #571.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sLtN12VfnLziCpst3Pfo2
@guillermoscript guillermoscript added enhancement New feature or request mcp-server MCP server (mcp-server/) tools, widgets, auth labels Jul 27, 2026
`numbersIn` located step markers by grepping the rendered markup for
`border-violet-600`. Widgets now paint accents with the tenant brand ramp,
so that class no longer exists and the matcher returned [] — the assertion
failed even though step numbering (the #561 regression this guards) is
unchanged.

Anchor on `border-[var(--brand-600)]` instead, which follows the accent
rather than a fixed hue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sLtN12VfnLziCpst3Pfo2
@guillermoscript
guillermoscript merged commit f93a762 into master Jul 27, 2026
2 checks passed
@guillermoscript
guillermoscript deleted the feat/mcp-widget-fixtures-branding-571 branch July 27, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request mcp-server MCP server (mcp-server/) tools, widgets, auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant