|
| 1 | +# Widget demo data & preview mode |
| 2 | + |
| 3 | +Every MCP App widget in `resources/` has hand-written fixtures so you can look at |
| 4 | +it — including its empty and edge states — with **no database, no seed data and |
| 5 | +no OAuth login**. |
| 6 | + |
| 7 | +## Run it |
| 8 | + |
| 9 | +```bash |
| 10 | +cd mcp-server |
| 11 | +MCP_DEMO_WIDGETS=1 npm run dev |
| 12 | +# → Inspector: http://localhost:3000/inspector?autoConnect=http%3A%2F%2Flocalhost%3A3000%2Fmcp |
| 13 | +``` |
| 14 | + |
| 15 | +Pick any `lms_demo_*` tool, optionally set `variant`, hit **Execute**. The widget |
| 16 | +renders in the response pane (the ⤢ button gives it the full pane). |
| 17 | + |
| 18 | +Headless PNGs of every fixture: |
| 19 | + |
| 20 | +```bash |
| 21 | +./scripts/shoot-demo-widgets.sh dark # → demo-shots/dark/<widget>--<variant>.png |
| 22 | +./scripts/shoot-demo-widgets.sh light |
| 23 | +``` |
| 24 | + |
| 25 | +## How the mode works |
| 26 | + |
| 27 | +| Piece | File | |
| 28 | +| --- | --- | |
| 29 | +| The fixtures | `src/demo-data.ts` (`WIDGET_DEMOS`) | |
| 30 | +| One `lms_demo_<widget>` tool per entry | `src/tools/demo.ts` | |
| 31 | +| The gate: `MCP_DEMO_WIDGETS=1` **and** `NODE_ENV !== production` | `src/env.ts` → `demoWidgetsEnabled()` | |
| 32 | + |
| 33 | +Three deliberate consequences of the flag, all dev-only: |
| 34 | + |
| 35 | +1. **OAuth is not installed** (`index.ts`). Bearer auth on `/mcp` would 401 the |
| 36 | + inspector before any handler ran, and the whole point is rendering widgets |
| 37 | + without Supabase. |
| 38 | +2. **Demo tools register before `installToolGuards()`**, so the role gate does |
| 39 | + not reject them (an inspector session with no login has no tenant role) and |
| 40 | + they never write to `mcp_audit_log`. |
| 41 | +3. **`tools/list` shows only the 18 demo tools.** With no auth there is no role, |
| 42 | + so the existing policy filter hides everything else. Real tools still refuse |
| 43 | + to run — `LmsSession.fromContext` throws "Authentication required." |
| 44 | + |
| 45 | +Widget→host calls (`useCallTool`) still fail in this mode: `lms_grade_review`, |
| 46 | +`lms_complete_lesson` etc. are not reachable without a session. That is expected |
| 47 | +— you are looking at rendering, not round-trips. |
| 48 | + |
| 49 | +## School branding |
| 50 | + |
| 51 | +Widgets pick up the tenant's `primary_color` the way the app does, so a school's |
| 52 | +widgets match its dashboard. |
| 53 | + |
| 54 | +- **Server** — `src/branding.ts` reads `tenants.name / logo_url / primary_color / |
| 55 | + secondary_color` on the caller's RLS-scoped client, cached 5 min per tenant. |
| 56 | + `installToolGuards` (`src/register.ts`) attaches it to the `_meta` of any |
| 57 | + result that has `structuredContent`, so every widget-rendering tool is themed |
| 58 | + without touching its handler. |
| 59 | +- **Widget** — `resources/shared/branding.tsx` reads it from |
| 60 | + `useWidget().metadata` and emits a `:root` ramp (`--brand-50 … --brand-950`) |
| 61 | + derived from the one colour with `color-mix()`. Every widget renders `<Brand />` |
| 62 | + in both its pending and loaded branches. |
| 63 | +- **Styling** — accents are `bg-[var(--brand-600)]`, `text-[var(--brand-400)]`, … |
| 64 | + With no tenant colour the component emits Tailwind's violet ramp, so an |
| 65 | + unbranded school renders exactly as before. |
| 66 | + |
| 67 | +Two constraints worth knowing before you touch this: |
| 68 | + |
| 69 | +- **`resources/styles.css` is not part of the build.** `mcp-use dev/build` |
| 70 | + generates its own Tailwind entry per widget under `.mcp-use/<widget>/styles.css` |
| 71 | + and imports that; there is no CLI hook to add your own. `@theme`, `@utility` |
| 72 | + and plain rules put in `resources/styles.css` silently do nothing — which is |
| 73 | + why the ramp is CSS custom properties + arbitrary-value utilities rather than |
| 74 | + a `brand-600` theme colour. |
| 75 | +- **Status colours stay semantic.** Green/amber/red mastery bars, `at risk` and |
| 76 | + `published` pills are meaning, not brand — they are deliberately not themed. |
| 77 | + |
| 78 | +Preview any widget under a fake school with the `brand` argument on the demo |
| 79 | +tools: `none` (default), `ocean`, `sunset`, `forest`. |
| 80 | + |
| 81 | +## Editing fixtures |
| 82 | + |
| 83 | +Fixtures must satisfy each widget's own `propsSchema`. When you change a widget |
| 84 | +schema, update the fixture in the same commit: `src/demo-data.ts` is the only |
| 85 | +committed example of each widget's payload, and the demo tool will throw on a |
| 86 | +mismatch the moment you execute it. |
| 87 | + |
| 88 | +Keep the awkward values. Null descriptions, comma-string `tags`, `0%`, ungraded |
| 89 | +rows and unnamed students are in there on purpose — they are what the widgets |
| 90 | +actually get, and they are what surfaces layout bugs. |
0 commit comments