Skip to content
Mocky/Docs v0.2
Architecture

ADR 001 — Muse: MCP-powered design intelligence

28 min read
Why it works this way

A decision that has already shipped survives in the code, but the reasoning behind it does not: the alternatives that were weighed and refused leave no trace anywhere. An Architecture Decision Record (ADR) — a dated, append-only note covering one choice, its context and its consequences — exists so a later reader can tell a deliberate constraint from an accident. This one is numbered and scoped to a single subject because a record that tries to cover everything gets edited until it no longer describes any particular moment.

  • Status: Accepted — implemented on main (Phases 1–5; some Phase 4 UI refinements + the memory-MCP taste profile remain, see §8/§9)
  • Date: 2026-07-26
  • Supersedes / relates to: the existing capability registry, Planner, DESIGN.md bridge, and provider proxy.
  • Driver: MOCKY_MUSE_PROMPT.md ("Prompt G", authored by Claude Fable 5).

This ADR is the deliverable of Phase 0. It records what the current codebase actually is, where the Muse plan's assumptions diverge from it, the concrete decisions we will make, and how every existing invariant plus the new M-series invariants are respected. No implementation code is written in this phase. Implementation begins at Phase 1 only after this ADR is approved.


1. Context — what Mocky actually is today#

Why it works this way

Every decision further down rests on where the code actually runs, so the record establishes that first: the Muse plan assumed a server-side pipeline, while Mocky in fact builds screens inside the browser tab (src/lib/generate.ts, src/lib/plan.ts, src/lib/capabilities/select.ts) and keeps the server thin. Stating the divergence before deciding anything is what makes the rest auditable — a reader can check the premise rather than only the conclusion, and a wrong premise here would quietly invalidate all ten decisions.

The Muse prompt's architecture diagram (§2) shows a backend-centric pipeline: MCP Host → Inspiration Engine → Dossier → Planner → Generation, all inside a "Mocky Backend (Node/Express)". That is not how Mocky is built. The single most important audit finding is:

Mocky's generation pipeline runs in the browser, not the backend.

Concretely:

ConcernWhere it lives todayFile(s)
Capability selection (deterministic)Browsersrc/lib/capabilities/select.ts
Planner (optional, structured-output LLM)Browsersrc/lib/plan.ts
Generation / edit / fix (streamed)Browsersrc/lib/generate.ts
Pipeline orchestration + stage phasesBrowser (React)src/components/ProjectView.tsx
DESIGN.md bridge (preamble, tokens, export)Browsersrc/lib/design.ts, designTokens.ts, export/theme.ts, export/project.ts
Sandbox render (null-origin iframe, vendored Babel)Browsersrc/components/Preview.tsx, lib/capabilities/prelude.ts
PersistenceBrowser localStorage (mocky.projects.v1, mocky.design.v1); settings incl. API key are browser-onlysrc/lib/project.ts, sync.ts
Backend roleThin: static file serving, accounts/SSO, per-user JSON sync, and the /__provider SSRF-guarded reverse proxyserver/index.js, server/provider-proxy.js

The backend is deliberately minimal: plain JSON files under server/data/, no database, no native dependencies (express + cookie-parser only). Writes are atomic (temp + rename). This "no DB, no native deps" posture is a de-facto project invariant and the Docker image (node:20-slim) depends on it staying small.

Implication for Muse. The parts of Muse that cannot run in a browser — spawning local MCP servers over stdio, running Playwright/Chromium, fetching arbitrary web pages, downloading and storing image files — must live in the Node backend. Muse therefore introduces, for the first time, a real server-side pipeline and a meaningful set of new backend dependencies. This is the central tension this ADR resolves (see Decision D3).

A second consequence: today the app is fully usable frontend-only (npm run dev, no backend). Muse requires the backend to be running. When the backend is absent (pure localStorage mode), the Muse toggle must be hidden or disabled with a clear notice — it can never appear to work and silently no-op.


2. The eight existing invariants, restated and checked#

Why it works this way

An invariant is a rule the code must never break, and Mocky's were referenced by number inside scattered comments (generate.ts, plan.ts and capabilities/registry.test.ts all say "invariant N") without any file listing them, so nobody could check new work against the full set. Collecting them here turns "Muse breaks nothing" from an assertion into a table a reviewer can walk row by row, which is why the compliance column sits beside the rule instead of living in a separate note.

The invariants are referenced by number in code comments (invariant 1/2/3/5/8) but were never collected in one place. This ADR codifies all eight (reconstructed from the code and the Muse prompt's own parenthetical list) so Phase 1+ can be checked against them. Part of this ADR's value is writing them down.

#InvariantEvidenceMuse compliance
I1Never regex-parse generated or vendored source to "discover names" or decide what's used — use a real (Babel) scope walk. (Parsing Markdown prose is explicitly exempt.)generate.ts:381, export/rewrite.ts:6, export/theme.ts:11Muse parses Markdown/JSON (dossier, DESIGN.md) and model JSON output — prose/data, not source. The Imagery Plan injects images by slot id, never by regex-editing generated JSX. ✅
I2The preview iframe is null-origin (sandbox="allow-scripts", no allow-same-origin); blob URLs are same-origin to null so no CORS is needed. Never add crossorigin attributes.Preview.tsx:62-64,149Generated images are served from Mocky's origin and referenced as absolute URLs with no crossorigin attribute ( display is not CORS-gated). See D5. ✅ (new M6)
I3No CDN