docs(mcp): add 10-agent research on scene-save workflow

10 parallel research agents (R1-R10) investigated whether MCP can save
scenes as saveable entities that the user later opens in the editor —
rather than injecting via a dev-only window.__pascalScene hook.

Key findings:
- Editor is already backend-agnostic via onLoad/onSave callbacks (R2,R7)
- Current persistence is localStorage-only, single key (R1)
- Zero dynamic routes and no backend code yet — env declared (R4,R5)
- File import exists as "Load Build" but lacks Zod validation (R6)
- MCP-written scenes load cleanly into the editor today (R6, Casa del
  Sol test already proved this)
- Best path: filesystem-handoff this week, Supabase in weeks 2-4,
  Supabase Realtime for live mode in Q2. Skip Yjs for now. (R8)
- 4-5 weeks to private beta, 10-14 to GA (R9)
- 10 high-value ideas ranked; "photo -> scene" is the unblocker (R10)

SYNTHESIS.md pulls all 10 reports together with the recommended
implementation plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adrian Perez
2026-04-18 18:47:36 +02:00
co-authored by Claude Opus 4.7
parent bdb47a6e5d
commit 42bd05db9c
11 changed files with 1463 additions and 0 deletions
@@ -0,0 +1,49 @@
# R1 — Persistence layer
## TL;DR
- **Scene data:** single-key localStorage, `pascal-editor-scene`, shape `{ nodes, rootNodeIds }`. Written by the autosave hook with 1 s debounce; flushed on `beforeunload`.
- **UI preferences, viewer prefs, audio:** three separate Zustand-persist stores, each with its own localStorage key.
- **Asset binaries (textures):** IndexedDB via `idb-keyval`, keys `asset_data:<uuid>`.
- **Backend persistence:** NONE in this repo. No Supabase calls, no API routes for scenes, no database integration.
- **Scene identity / listing:** NONE. One scene per origin per browser.
## Write pathways
| When | What | Where |
|---|---|---|
| 1 s after any scene mutation | `{ nodes, rootNodeIds }``onSave` callback (if provided) else `localStorage['pascal-editor-scene']` | `packages/editor/src/hooks/use-auto-save.ts:104135` |
| Every UI state mutation | `pascal-editor-ui-preferences` | Zustand persist in `use-editor.tsx:372607` |
| Every viewer state mutation | `viewer-preferences` | Zustand persist in `use-viewer.ts:81220` |
| Every audio state mutation | `pascal-audio-settings` | `use-audio.tsx:2243` |
| On `beforeunload` | Final scene snapshot | `use-auto-save.ts:137147` |
## Read pathways
1. **Editor mount** (`editor/index.tsx:765796`):
- If host supplied `onLoad``await onLoad()`
- Else → `loadSceneFromLocalStorage()`
- Apply via `useScene.setScene(nodes, rootNodeIds)`
2. **Selection hydration**`syncEditorSelectionFromCurrentScene()` (`lib/scene.ts:251332`)
3. **Zustand persist** hydrates UI/viewer/audio stores automatically on first subscriber
## What's NOT persisted
- Undo/redo history (`useScene.temporal` — in-memory only)
- Active tool state (`movingNode`, `editingHole`, `curvingWall`)
- Camera position/rotation
- Collections (stored in nodes array but not in the persist partialize)
- Three.js mesh/material cache
## Multi-scene support
- Single global key `pascal-editor-scene`. No scene id, name, thumbnail, version.
- `projectId` prop scopes UI **selection** (building/level/zone) but NOT scene data.
- No listing, no metadata, no per-project isolation of the scene itself.
## Gap to "MCP writes → user opens saved scene"
Needs:
1. Scene entity layer: id, name, projectId, created_at, thumbnail_url.
2. Backend table (or filesystem for local dev).
3. MCP tools for scene lifecycle (`save_scene`, `list_scenes`, `load_scene`, `delete_scene`).
4. Editor route `/scene/[id]` that reads scene by id on mount.
5. Host-app `onLoad(() => fetchScene(sceneId))`.
Foundation is solid — `SceneGraph` type + `applySceneGraphToEditor` are production-ready; only the entity layer is missing.
@@ -0,0 +1,309 @@
# R10 — Ideas, Edges, and Unlocks
*Agent: Research Agent R10*
*Date: 2026-04-18*
*Scope: brainstorm, not specification*
Grounded in the `@pascal-app/mcp` package. MCP today exposes 21 tools, 4 resources, 3 prompts, runs headless in Node, mutates a Zustand (+ Zundo) scene graph, and can round-trip JSON. The editor is Next.js + R3F + WebGPU, persists to IndexedDB, has three Zustand stores (`useScene`, `useViewer`, `useEditor`), and has no user-account backend today beyond a `health` API route. GLB export is stubbed; catalog resolves `asset://` URLs only in the browser. That surface area is the substrate I brainstorm against.
Legend: **S** ≤ 2 dev-days, **M** 12 weeks, **L** 3+ weeks / cross-cutting.
---
## 1. Workflows unlocked
1.1 **Prompt → Pascal one-shot studio**
Value: a landing page textarea ("design me a 90 m² south-facing apartment in Barcelona") that spawns a scene and drops the user into the editor with orbit camera pre-aimed. Lowest-friction wedge for consumer acquisition.
Effort: M (needs hosted MCP + auth + an agent loop that calls `from_brief`).
Risk: expectations calibration — generated scenes will look schematic without finishes.
1.2 **Photo → Pascal via `analyze_floorplan_image`**
Value: the tool exists but currently only the MCP host calls it. Exposing a drag-and-drop upload in the editor (`viewer-overlay`) that posts to MCP and returns a ready-made scene turns Pascal into a floorplan digitiser. Realtors and renovators will pay for this alone.
Effort: M (client upload, MCP host with sampling, progress UI).
Risk: the vision model is approximate; users will expect dimensional exactness and blame Pascal for mis-reads.
1.3 **Listing URL → Pascal (Zillow / Idealista / Rightmove)**
Value: paste a listing URL, a scraper extracts the floor plan image + listed area, `analyze_floorplan_image` produces the scene, then "remodel" prompts run on top. Enormous cold-start value — the user arrives with a house they already care about.
Effort: L (scraping layer, anti-bot, per-site parsers, legal).
Risk: ToS / legal on scraping; drives a category of "renovation-before-offer" anxiety that may alienate listings.
1.4 **Multi-variant generation**
Value: "give me 5 kitchen variations" → 5 forked scenes saved as siblings, tiled in a comparison view. Pattern-matches Midjourney's grid. Encourages exploration, sells more generations.
Effort: M (needs scene forking + a comparison UI; the `forkSceneGraph` helper exists already in `core/clone-scene-graph`).
Risk: without a scored objective ("cheapest", "most storage"), users get lost choosing; need ranking.
1.5 **Regulatory/accessibility lints**
Value: "ensure this scene complies with Spanish Código Técnico de la Edificación accessibility." MCP walks zones/doors/stairs, flags minimum door widths, corridor widths, ramp slopes, stair rise/run. Sells to architects and BIM shops.
Effort: L (per-jurisdiction rule packs; `check_collisions` is a proof the traversal works).
Risk: false confidence — a lint pass is not a stamped permit; liability exposure.
1.6 **Live co-design ("AI architect next to me")**
Value: editor sidebar chat pane; user edits walls, AI proposes adjustments ("you lost the light well — shall I add a skylight?") via MCP on a debounced scene diff. Screen-share-ready demo.
Effort: L (streaming agent, scene-diff prompts, throttling).
Risk: agents nagging mid-edit is the fastest route to churn; needs carefully tuned interventions.
1.7 **Voice-driven redlines on a phone**
Value: open a scene on mobile (preview-button path exists), talk into the mic ("turn the office into a nursery, softer colours"), MCP applies patches, renderer reflows on reload. Wins the "showing mum the renovation" moment.
Effort: M (Whisper → text → `from_brief`/`iterate_on_feedback`, mobile-friendly result).
Risk: WebGPU on low-end Android will fail; need a fallback still renderer.
1.8 **Cost + BOM synthesis**
Value: after scene generation, MCP walks catalog items and zones → exports a parts-list CSV with Spanish supplier SKUs and regional labour rates. Converts the toy into a quoteable artefact.
Effort: M (catalog → pricing mapping, jurisdictional labour constants).
Risk: pricing drifts; must be explicit about "indicative".
1.9 **Time-lapse tours**
Value: MCP emits a keyframed camera flythrough script (camera node already exists in `BaseNode.camera`). One click → shareable MP4. Drives social acquisition.
Effort: M (stitch recorder in the viewer; `apply_patch` can set cameras).
Risk: WebGPU video capture is finicky across Safari.
---
## 2. Novel primitives
2.1 **Scene branches & forks (Pascal-Git)**
Value: "save as branch" on every MCP mutation; user can compare, merge, or revert branches visually. The current temporal middleware gives us a linear undo stack; exposing a DAG unlocks nondestructive exploration and is a natural home for multi-variant results.
Effort: L (schema for branches, UI, storage beyond IndexedDB).
Risk: merge semantics for geometry are unsolved — walls and openings resist 3-way merge.
2.2 **Scene templates catalog**
Value: a resource `pascal://templates/*` — studio apartment, ADU, Japanese machiya, Barcelona eixample flat. `from_brief` prompts seed from the nearest template, drastically improving first-shot quality.
Effort: SM (author ~20 templates, register as MCP resources).
Risk: templates can anchor the generator; need variety + randomisation.
2.3 **Component library / "sub-scenes"**
Value: save a kitchen layout as a reusable component that carries its own sub-graph. MCP tool `instantiate_component` drops it onto a level with a transform. Mirrors Figma components.
Effort: M (schema addition for component refs or instance-of nodes; invalidation when parent changes).
Risk: local-vs-shared component propagation; ownership of community components.
2.4 **Scene diff view**
Value: built on top of `export_json` + a structured differ — show "AI added 3 walls, removed 2 doors, reshaped zone X". Makes AI suggestions reviewable like a PR.
Effort: M (diff algo, UI, inline accept/reject per patch).
Risk: diff UIs require high polish to feel trustworthy.
2.5 **`explain_scene` tool**
Value: a new MCP tool (or prompt) returning a natural-language summary: "A 92 m² duplex with the kitchen facing west; accessibility score 6/10; conspicuously no closet space." Turns scenes into legible artefacts for non-3D users.
Effort: S (wrapping `scene-summary` resource with an LLM prompt).
Risk: hallucinated detail; must be grounded strictly in `find_nodes` data.
2.6 **Semantic scene search**
Value: "find every wall in the scene longer than 4 m that faces south" → `find_nodes` is spec'd narrowly (type/parent/zone/level); extend with predicates + embedding search over `metadata`.
Effort: M (predicate DSL or JSON-logic filter, optional embeddings).
Risk: query DSLs get complex fast; keep it constrained.
2.7 **Real-world anchor nodes**
Value: a `SiteOrigin` node carrying lat/lon/heading/altitude so MCP can reason about sun path, climate, zoning. Enables solar analysis and jurisdictional rules.
Effort: S schema, L downstream (solar calc, sun path widget).
Risk: accidentally leaking address when scenes are shared.
2.8 **Commentable scene nodes**
Value: add a `comment` or `annotation` edge to any node: "client wants this moved 20 cm". Makes Pascal a review surface for human + AI collaboration. Dovetails with 2.4.
Effort: S (new schema node; UI pin).
Risk: scope creep into full comments system.
---
## 3. Edge cases
3.1 **Concurrent MCP writers**
Two agents holding the same `SceneBridge` both call `apply_patch` on the same node. Zundo coalesces at the store level; there is no lock. Result: lost updates, order-dependent chaos.
Mitigation: per-bridge operation mutex, or optimistic version stamps inside `UpdatePatch`.
3.2 **Invalid scene crashes editor**
MCP writes a `DoorNode` with `parentId` pointing to a slab. `validate_scene` catches it, but a misuse of `apply_patch` with a forged parent slips through. Editor hooks assume doors under walls and blow up.
Mitigation: the editor should hydrate through the same Zod validator, not trust the JSON. Add an "editor-safe boot" path that falls back to a recovery scene.
3.3 **10k-node performance cliff**
The Zustand store keeps a flat `nodes` dict; most tools iterate linearly. `check_collisions` is O(n²) on item bounds. Agents might produce hundreds of chairs in an office.
Mitigation: budget + warn inside `apply_patch`, or cap nodes per type with a clear error.
3.4 **Circular parent-child refs**
`validate_scene` covers Zod shape; a patch chain can still create a cycle (A.parent=B, B.parent=A). Traversal hangs.
Mitigation: cycle detection pass in `apply_patch` dry-run before commit.
3.5 **Camera points at nothing**
AI creates a 5 cm tall decorative bowl on the second floor and the last-placed-camera convention zooms there. First impression: black screen.
Mitigation: always auto-frame to root bounding box on MCP-opened scenes; store a `pascal://scene/current/recommendedCamera` resource.
3.6 **Broken external assets**
`ItemNode` can reference `asset://` URLs; in Node, the core asset loaders are browser-only and return nothing. A scene saved in the browser with asset URIs, then opened headless, displays placeholders; a scene passed back to the browser still references dead IDs.
Mitigation: MCP must round-trip asset URIs opaquely (never create new `asset://` IDs), and the editor should show a "missing asset" fallback.
3.7 **User edits after MCP — merge or clobber?**
The current bridge is stateful with a linear undo stack. If an agent re-runs `from_brief` on a user-modified scene, it rewrites from scratch. Either we implement 3-way merge (2.1) or we lock the scene and make MCP operate on a branch.
Mitigation: default to fork-on-regenerate; never overwrite user edits.
3.8 **PII in shared scenes**
A floor plan with lat/lon (2.7) or matching a real home is a privacy liability. Exporting `export_json` strips nothing. Agents uploading scenes to a shared LLM vendor leaks data.
Mitigation: a `strip_pii` utility that blanks address/gps/photos; explicit consent dialog before MCP sends images to remote hosts.
3.9 **Offline + cloud scenes**
If we add cloud persistence (§5), MCP runs against a server-only scene when user is offline. Writes queue; reconciliation becomes a merge problem (3.7). IndexedDB persistence covers local, not cross-device.
Mitigation: conflict-free writes via CRDT-style patch log keyed by node ID.
3.10 **Sampling unavailable**
`analyze_floorplan_image` gracefully errors when the host lacks sampling. Users who paid for this feature on a non-Claude host get a brick.
Mitigation: publish a supported-host matrix; provide a first-party web host for users without one.
3.11 **GLB export stub**
`export_glb` throws `not_implemented`. An AR-preview (§4.6) or a glTF-requiring downstream (Unity, Blender) falls off a cliff. This is the single largest productisation gap.
Mitigation: stand up a headless renderer worker (puppeteer + WebGPU) or build a geometry exporter independent of three-mesh-bvh.
3.12 **Prompt injection via scene metadata**
`BaseNode.metadata` is arbitrary JSON. A hostile scene file seeds strings into `describe_node` output, which an LLM later reads. Classic indirect prompt injection.
Mitigation: sanitise/escape metadata when emitting into model-visible surfaces; strip control tokens.
3.13 **Temporal stack explosion**
Zundo caps history but MCP could batch thousands of operations per "patch" — one undo reverts huge changes invisibly. Users panic when Cmd-Z throws away the whole room.
Mitigation: each MCP mutation shows a visible "AI step" badge; undo granularity documented.
3.14 **Units mismatch**
MCP tools say meters; catalog items may carry cm internally. Silent drift of 100x.
Mitigation: enforce units at the schema boundary; add a unit-assertion test in CI.
---
## 4. Integrations
4.1 **Figma → Pascal**
Value: a Figma plugin lets designers hand a 2D mood board to an MCP scene. The palette, materials, and key dimensions flow in. Wins the handoff from 2D to 3D.
Effort: M.
Risk: Figma plugin review; limited 3D fidelity.
4.2 **Revit / SketchUp / IFC import**
Value: architects live in these tools; Pascal becomes the "redline + present" layer. IFC in particular is the lingua franca of BIM.
Effort: L (IFC parser; map to Pascal nodes).
Risk: schema mismatch; Pascal is lighter-weight than full BIM.
4.3 **USD / glTF / IFC export**
Value: outward compatibility = easier adoption. USD for Pixar/Nvidia Omniverse pipelines, glTF for web, IFC for construction. Unlocks 4.2 reciprocally.
Effort: ML per format.
Risk: 3.11 — geometry derivation still lives in the browser renderer; export-by-transpile is needed.
4.4 **MCP tool marketplace**
Value: third parties publish style MCPs ("Japandi kitchen", "Brutalist staircase", "Zaha-Hadid-ish"). They compose as sub-tools callable from `from_brief`. Pascal becomes a platform.
Effort: L (registry, sandboxing, review).
Risk: quality dilution; security (3.12).
4.5 **Planning-permission APIs (UK Planning Portal, Spain Sede Electrónica)**
Value: generated scene → pre-filled planning application PDF. Brutal time-saver. Differentiator vs Canva-for-architecture competitors.
Effort: L.
Risk: compliance; rules differ per council.
4.6 **AR preview — Apple RoomPlan / ARKit / ARCore**
Value: phone scans the room with RoomPlan → MCP ingests the plist → user redesigns in Pascal → AR overlays result onto the real room. Tactile "buy this sofa here" moment.
Effort: L (iOS/Android apps; glTF export 3.11 prerequisite).
Risk: needs native apps Pascal doesn't have.
4.7 **E-commerce catalog bridges (IKEA, Wayfair, Kave Home)**
Value: map `ItemNode.catalogItemId` to retailer SKUs. "Checkout this room" button. Affiliate revenue.
Effort: M (catalog mapping, retailer API quirks).
Risk: SKU churn; regional availability.
4.8 **Google Earth / OSM site context**
Value: lat/lon (2.7) + MapBox → Pascal renders the adjacent buildings, street, sun path. Scene gains real-world context. Critical for facade design.
Effort: L.
Risk: licensing maps data.
---
## 5. Monetization
5.1 **Pay per generation**
Value: $0.50$2 per `from_brief` call. Low commitment, matches OpenAI/Midjourney consumer norms.
Effort: S (Stripe + credits; MCP host tracks).
Risk: commoditised unless paired with templates (2.2) or regulatory value (1.5).
5.2 **Pro subscription (unlimited AI + cloud saves)**
Value: $15/mo predictable ARR.
Effort: M.
Risk: balance cost; fair-use caps for heavy users.
5.3 **Template / component marketplace**
Value: creators sell templates (2.2) and components (2.3). Pascal takes 20%.
Effort: M (payments, tax, takedowns).
Risk: content moderation; cold-start supply.
5.4 **Enterprise BIM seat**
Value: firms pay per seat; access to IFC import, compliance packs, branded export. $50200/seat/mo.
Effort: L.
Risk: SOC2, procurement cycles.
5.5 **Lead-gen for contractors**
Value: after a scene is generated, Pascal matches to local contractors with quote requests. Contractors pay per lead.
Effort: M.
Risk: lemons-market; must vet contractors.
5.6 **Branded MCP for retailers**
Value: IKEA white-labels Pascal's MCP under "IKEA Studio". Licensing fee. Pascal stays the engine, retailer owns the UI.
Effort: M (API + licensing).
Risk: channel conflict; retailers might eat Pascal.
---
## 6. Ecosystem beyond current scope
6.1 **Pascal MCP becomes a standard for spatial editors**
Value: the tool verbs (`create_wall`, `place_item`, `cut_opening`) generalise. Onshape, SketchUp, Rhino could adopt a "spatial MCP" profile. Pascal authors the spec.
Effort: L (ecosystem work, not code).
Risk: platforms resist standards that commoditise their moats.
6.2 **Open-source Pascal Scene Format**
Value: USD is overkill for interior/architectural scenes; glTF lacks building semantics. A Pascal-flavoured JSON schema (already Zod-native) becomes the "Markdown of interiors". Could ship as `@pascal-app/scene-format`.
Effort: M to carve out; L to evangelise.
Risk: yet-another-format fatigue; ties ecosystem to Pascal's semantic choices.
6.3 **Educational channel — "Design school in Pascal"**
Value: a publisher (or Pascal) ships a curriculum — "design a studio flat", "analyse daylight". Classrooms teach design thinking with an MCP agent as tutor.
Effort: M (curriculum; content).
Risk: sales-motion mismatch with a B2C/B2B tool.
6.4 **Scene replay / provenance logs**
Value: every MCP patch is signed, stored, and replayable. A regulator or client can audit the design history. Opens procurement doors.
Effort: M (append-only log; signing keys).
Risk: GDPR implications of retention.
6.5 **Physical fabrication downstream**
Value: furniture/cabinet generation → CAM-ready DXF → CNC shop. Unlocks "AI designed and built my kitchen" as a narrative.
Effort: L.
Risk: tolerances; liability.
6.6 **Agent-to-agent Pascal**
Value: a procurement agent talks to a designer agent talks to a contractor agent — Pascal scenes are the shared artefact. Pushes Pascal as infrastructure for the agent web.
Effort: L (policy, auth between agents).
Risk: sounds crazy today; will be obvious by 2027.
---
## 7. Crazy-but-maybe ideas
7.1 **"Sceneprint"** — give Pascal a photo of you standing in your room; it infers which room, auto-positions the camera, and starts redesign from there. Sells to TikTok.
7.2 **"Phantom move-in"** — MCP grafts your furniture (measured from another Pascal scene) into a listing. Realtors hand buyers a pre-staged version of the home they're considering.
7.3 **Insurance-linked scenes** — an insurance app ingests your Pascal scene to price contents cover faster and more accurately. Scene = digital twin = underwriting data.
7.4 **Agent-on-call** — you email pascal@your.domain with a photo; MCP replies with a scene URL. No app, no login, pure asynchronous co-design. Drives discovery far beyond the editor.
7.5 **"Haunted" scenes** — designer publishes a scene; buyers walk through in AR; on replacement of an item, MCP whispers "the designer disagrees — here's why". Opinionated design, monetised.
7.6 **Voice-coded CAD for blind users** — purely spoken design, Pascal describes the scene back via `explain_scene` (2.5). Genuine accessibility win and possibly grant-fundable.
7.7 **Multi-player Pascal** — Yjs/CRDT layer on the scene graph, MCP agents as first-class collaborators alongside humans. Google Docs for interiors.
---
## Top 10 ideas ranked by (value × feasibility)
1. **1.2 Photo → Pascal (floor plan upload)** — MCP already has `analyze_floorplan_image`; only the UI entry point and host plumbing are missing. Highest value-for-effort unlock in the repo right now.
2. **2.2 Scene templates catalog** — SM effort, dramatically improves `from_brief` output quality, and doubles as marketplace seed inventory (5.3).
3. **3.5 Auto-framing camera on MCP-opened scenes** — a tiny fix for the single most embarrassing failure mode ("black screen"). S effort, huge UX.
4. **1.1 Prompt → Pascal one-shot studio** — the canonical "MCP creates scene" workflow. Build it as the hosted front door, not a side feature.
5. **1.4 Multi-variant generation**`forkSceneGraph` already exists; a comparison grid lights up exploration and creates upsell moments (pay-per-variant).
6. **2.4 Scene diff view** — makes every AI action reviewable and is a prerequisite for 1.6 and 3.7 merge flows.
7. **1.8 Cost + BOM synthesis** — converts toy scenes into quoteable artefacts; directly monetisable via retailer affiliates (4.7).
8. **2.1 Scene branches & forks** — larger, but it dissolves the "overwrite vs merge" edge (3.7) and is the structural foundation for multi-variant and AI co-design.
9. **3.11 GLB / glTF export via headless renderer** — unlocks AR (4.6), USD/IFC bridges (4.3), and removes the most cited "limitation" in the README.
10. **1.5 Regulatory/accessibility lints (pilot: one jurisdiction)** — pick Spain or the UK, ship one rule pack; opens the B2B architect segment where budgets live.
Honourable mention: **1.6 Live co-design** — the single most defensible long-term product vision, but it depends on 2.1, 2.4, and a streaming agent layer the repo doesn't have yet. Build the pieces, then assemble.
@@ -0,0 +1,78 @@
# R2 — `projectId` semantics + Editor public API
## TL;DR
- **`projectId` is a namespace**, not a scene identifier.
- The Editor is **scene-agnostic** — it loads/saves via `onLoad` / `onSave` callbacks.
- The editor defaults to `loadSceneFromLocalStorage()` / `saveSceneToLocalStorage()` when callbacks aren't supplied.
- One project can contain many scenes (1:N). That mapping is a **host-app concern**, not an Editor concern.
## `<Editor>` public props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| `projectId` | `string \| null` | none | Namespace key for UI-state localStorage, passed to host callbacks |
| `layoutVersion` | `'v1' \| 'v2'` | `'v1'` | Sidebar layout flavour |
| `onLoad` | `() => Promise<SceneGraph \| null>` | `loadSceneFromLocalStorage()` | Fetch initial scene on mount, and when `onLoad` identity changes (scene switch) |
| `onSave` | `(scene: SceneGraph) => Promise<void>` | `saveSceneToLocalStorage()` | Debounced (1000 ms) autosave after every scene change |
| `onDirty` | `() => void` | — | First change after last save |
| `onSaveStatusChange` | `(status: SaveStatus) => void` | — | `'idle' \| 'pending' \| 'saving' \| 'saved' \| 'paused' \| 'error'` |
| `onThumbnailCapture` | `(blob: Blob, cameraData) => void` | — | Auto-fires after 10 s idle OR manual "Generate thumbnail" |
| `previewScene` | `SceneGraph` | — | Read-only version-preview mode |
| `isVersionPreviewMode` | `boolean` | `false` | Locks scene graph |
| `isLoading` | `boolean` | `false` | Spinner overlay |
| `sidebarTabs` | `SidebarTab[]` | `[]` | v2 sidebar tabs w/ custom components |
| `appMenuButton`, `sidebarTop`, `navbarSlot`, `viewerToolbarLeft`, `viewerToolbarRight`, `sidebarOverlay`, `viewerBanner` | `ReactNode` | — | UI slots |
| `settingsPanelProps` | `{ projectId?, projectVisibility?, onVisibilityChange? }` | — | Settings-panel config |
| `sitePanelProps` | `{ projectId?, onUploadAsset?, onDeleteAsset? }` | — | Asset callbacks |
| `presetsAdapter` | `PresetsAdapter` | localStorage | Presets backend |
| `extraSidebarPanels` | `ExtraPanel[]` | `[]` | Additional v1 sidebar panels |
| `commandPaletteEmptyAction` | `CommandPaletteEmptyAction` | — | Fallback on no-match search |
## Data flow
```
host page → <Editor projectId="…"> → useEffect sync (index.tsx:757)
useViewer.setProjectId() (packages/viewer/src/store/use-viewer.ts:147157)
localStorage keys prefixed with `pascal-editor-selection:${projectId}` (lib/scene.ts:32)
Host callbacks: onUploadAsset(projectId, levelId, file, type), onDeleteAsset(projectId, url)
```
## Scene vs project
- Scene = `{ nodes, rootNodeIds, collections? }` — the graph
- Project = namespace (which buildings/levels/zones this user can select; which assets belong)
- 1 project → N scenes (via different `onLoad` identities → scene switch)
## Host-app integration — the minimal server-backed example
```tsx
const [sceneId, setSceneId] = useState<string | null>(null)
<Editor
projectId={projectId}
onLoad={sceneId
? () => fetch(`/api/projects/${projectId}/scenes/${sceneId}`).then(r => r.json())
: () => null} // null → blank
onSave={async (scene) => {
if (!sceneId) {
const r = await fetch(`/api/projects/${projectId}/scenes`, { method: 'POST', body: JSON.stringify(scene) })
setSceneId((await r.json()).id)
} else {
await fetch(`/api/projects/${projectId}/scenes/${sceneId}`, { method: 'PUT', body: JSON.stringify(scene) })
}
}}
/>
```
## Verdict
**The Editor already gives us everything we need on the client side.** The "scene save → open" workflow just needs:
1. A backend table / API keyed by `(projectId, sceneId)`.
2. A scene-picker UI in the host app (route `/scene/[id]` or a dropdown).
3. MCP writes to the same backend.
**No Editor changes required** for the baseline flow. The missing UI (scene list, naming) can be added to the Settings panel (per R3) when we want it inside the Editor package.
@@ -0,0 +1,34 @@
# R3 — Scene-management UI
## What exists today
| Feature | File | Line |
|---|---|---|
| "Save Build" → download `layout_YYYY-MM-DD.json` | `packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx` | 205216, 362365 |
| "Load Build" → file picker `.json``setScene` | `…settings-panel/index.tsx` | 218239, 367383 |
| "Export Scene (JSON)" in command palette | `editor-commands.tsx` | 328346 |
| "Export GLB / STL / OBJ" | `export-manager.tsx` | — |
| "Clear & Start New" destructive button | `settings-panel/index.tsx` | 427434 |
| "Explore scene graph" read-only tree dialog | `settings-panel/index.tsx` | 398421 |
| Autosave hook (1 s debounce → onSave or localStorage fallback) | `use-auto-save.ts` | 1191 |
| Scene-dirty tracking exposed to host via `onDirty` / `onSaveStatusChange` | `use-auto-save.ts` | 56187 |
## What's missing from a typical editor
1. **New scene dialog with naming.** Saves are all date-stamped; no user-given names.
2. **Scene list / picker.** No sidebar panel that lists saved scenes.
3. **Open recent.**
4. **Delete scene** (the destructive button clears current, can't delete a stored scene).
5. **No `Ctrl+S` / `Cmd+S` save shortcut** registered in `use-keyboard.ts`.
6. **No top-bar File menu.** `appMenuButton` slot exists for host to inject one, but nothing ships by default.
## Where new UI should live
- **Scene picker panel** via `extraSidebarPanels` prop — non-invasive, no Editor-core change.
- **Quick actions** via `useCommandRegistry().register([...])``editor.scene.open`, `editor.scene.new`, `editor.scene.save-as`.
- **Save-status badge** next to the navbar via `navbarSlot` (v2 layout).
- **"Created by MCP" toast** via a new `Toast` provider in the editor's runtime init.
## Verdict
UI foundation is **~40%** of the way there. The infrastructure (autosave, callbacks, dialogs, palette extensibility) is all present. The missing pieces are UI-only: a scene-list panel (~150 LOC) + a few palette commands + a status indicator. No Editor core changes required if we keep it callback-driven and plug a host-side scene switcher.
@@ -0,0 +1,38 @@
# R4 — Routing & URLs
## TL;DR
**Zero dynamic routes today.** Every user lands on `/` which hardcodes `projectId="local-editor"`.
## Route tree
```
apps/editor/app/
├── page.tsx (Home: <Editor projectId="local-editor">)
├── layout.tsx
├── privacy/page.tsx
├── terms/page.tsx
├── api/health/route.ts
└── fonts/
```
- No `[id]`, `[projectId]`, `[sceneId]`, or `[[...slug]]` segments.
- No middleware.
- No `rewrites()` / `redirects()` in `next.config.ts`.
- No query-param driven state.
- No hash routing.
## Latent expectation (dead code)
`packages/editor/src/components/ui/action-menu/view-toggles.tsx:79`:
```ts
const projectId = window.location.pathname.split('/editor/')[1]?.split('/')[0]
```
Code expects URLs of shape `/editor/<projectId>/…`. No such routes exist. Falls through to `undefined` gracefully today but signals a planned structure.
## Options to add
| Option | Route | Effort |
|---|---|---|
| A | `/?sceneId=<id>` — reuse `/` + `useSearchParams` | XS |
| B | `/editor/[projectId]` | S |
| C | `/editor/[projectId]/[sceneId]` | M |
| D | `/scene/[id]` — flat, project-agnostic | S |
Recommended: **B for projects, `/scene/[id]` short-link for sharing**. Matches the latent expectation in view-toggles.tsx.
@@ -0,0 +1,40 @@
# R5 — Backend / Supabase
## TL;DR
**Infrastructure declared, ZERO backend code.** `env.mjs` lists Supabase + Postgres + BetterAuth + Resend secrets as REQUIRED, the privacy policy claims scene data is stored in Supabase, `turbo.json` invalidates cache on those secrets — but the repo contains **no Supabase client, no schema, no migrations, no scene CRUD API**.
## Evidence
### Declared infra
- `apps/editor/env.mjs:1819``POSTGRES_URL`, `SUPABASE_SERVICE_ROLE_KEY` (server-only, `.min(1)`)
- `env.mjs:1214``BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `GOOGLE_CLIENT_*`
- `env.mjs:2731``NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `turbo.json:920` — same vars listed as build-cache keys
- `apps/editor/app/privacy/page.tsx:9597` — "Your data is stored using Supabase (PostgreSQL database)"
- `.gitignore:2224` — references `supabase/.branches/`, `supabase/.temp/` dirs (not present)
### What's absent
- Zero `createClient(` / `import.*supabase` matches across `apps/editor/**` and `packages/**`
- Zero `.sql` schema files
- Zero `drizzle/` / `prisma/` / `migrations/` directories
- Zero server actions (`'use server'` grep returns nothing)
- Zero API routes other than `/api/health` (returns `{ status: 'ok' }`)
## API surface today
| Route | Method | Purpose | Auth |
|---|---|---|---|
| `/api/health` | GET | Liveness | none |
## Required to enable MCP → cloud scene
1. Provision a Supabase project (or alternative Postgres).
2. Schema: `projects`, `scenes` (id, project_id, name, data jsonb, version, thumbnail_url, created_at, updated_at, owner_id), `scene_versions` (for history).
3. Supabase client singletons:
- `apps/editor/lib/supabase-browser.ts` (uses `ANON_KEY`)
- `apps/editor/lib/supabase-server.ts` (uses `SERVICE_ROLE_KEY` in server components / API routes)
4. Auth via BetterAuth + Google OAuth (env is there, unused).
5. API routes: `POST/GET/PUT/DELETE /api/projects/[id]/scenes/[sceneId]`.
6. RLS policies: scene rows readable only by owner + collaborators.
7. `SceneBridge` in MCP gets optional `persistenceAdapter: SupabaseAdapter` — replaces the in-memory store with a writeback to Supabase.
## Verdict
Groundwork is in place (env vars, privacy policy, turbo cache keys) but **every line of actual backend code is missing**. This is a greenfield opportunity: the team clearly planned for Supabase but hasn't implemented it yet.
@@ -0,0 +1,55 @@
# R6 — File I/O pathways
## TL;DR
- Export: 2 JSON pathways + 3 binary (GLB/STL/OBJ) pathways.
- Import: 1 JSON pathway ("Load Build"), **no Zod validation** at boundary.
- No drag-drop, no clipboard-paste JSON import.
- Round-trip export → re-import works; MCP-written JSON loads cleanly IF structure matches.
## Exports
| Trigger | Handler | File | Output |
|---|---|---|---|
| Settings → "Save Build" | `handleSaveBuild` | `settings-panel/index.tsx:205216` | `layout_YYYY-MM-DD.json` |
| Cmd palette → "Export Scene (JSON)" | `editor.export.json` | `command-palette/editor-commands.tsx:329346` | `scene_YYYY-MM-DD.json` |
| Settings/palette → "Export GLB/STL/OBJ" | `export-manager.tsx` | `editor/export-manager.tsx:7178` | binary 3D geometry |
Both JSON paths serialise `{ nodes: useScene.getState().nodes, rootNodeIds: useScene.getState().rootNodeIds }`. No metadata (no name, no created_at, no projectId).
## Import
### "Load Build"
- `settings-panel/index.tsx:218239`
- Accept: `application/json`
- Handler: `JSON.parse` → check `data.nodes && data.rootNodeIds` → call `useScene.setScene(nodes, rootNodeIds)`
- **No Zod validation.** Confirmed the security-audit flag from Phase 3.
### `setScene` behaviour (`core/store/use-scene.ts:242271`)
1. `migrateNodes()` — runs a few backward-compat patches. Stair nodes are zod-safeParsed and SILENTLY DROPPED on failure; other types are unvalidated.
2. Orphan pruning — deletes any node whose `parentId` isn't present in the dict.
3. `setState` with cleaned nodes + `dirtyNodes: new Set()`.
4. Marks every node dirty to trigger re-render.
**Critical gap:** Invalid `type` strings silently load. Systems will later fail to find a renderer for them and the node will be invisible but consume state.
## Round-trip fidelity
| Scenario | Loads clean? |
|---|---|
| Export → re-import | ✅ |
| MCP writes well-formed `{ nodes, rootNodeIds }` | ✅ (confirmed: Casa del Sol scene.json loads into the editor) |
| MCP writes bad `node.type` | ⚠️ Silent load, invisible node |
| MCP writes broken parentId chain | ⚠️ Orphans silently deleted |
| MCP writes missing `children: []` on container | ⚠️ Core treats as `undefined` → system ignores |
## Missing / nice-to-have
- Drag-drop JSON onto viewport
- URL-param load: `?load=<publicJsonUrl>`
- Clipboard paste of JSON blob
- `importFromFile` with Zod validation at the boundary
- File-format version field (`formatVersion: "1"`) for forward-compat
## Recommendation
Every "save to cloud" implementation MUST Zod-validate at the boundary with `AnyNode.safeParse` per node + structural checks on `rootNodeIds`. The tool in MCP (`validate_scene`) already does this — run it before any save.
@@ -0,0 +1,46 @@
# R7 — `@pascal-app/editor` public API
## Exports (packages/editor/src/index.tsx)
**Components:** `Editor` (default), `SettingsPanel`, `SitePanel`, `FloatingLevelSelector`, `SceneLoader`, `ViewerToolbarLeft`, `ViewerToolbarRight`, `Slider`, `SliderControl`
**Hooks/Stores:** `useEditor`, `useCommandRegistry`, `useSidebarStore`, `useUploadStore`, `useAudio`, `usePaletteViewRegistry`, `useCommandPalette`
**Utilities:** `applySceneGraphToEditor`, `SceneGraph` (type), `CATALOG_ITEMS`, `PresetsProvider`, `PresetsAdapter` (type)
## `<Editor>` host integration points
| Prop | Signature | Trigger | Host opportunity |
|---|---|---|---|
| `onLoad` | `() => Promise<SceneGraph \| null>` | Mount + `onLoad` identity change | Fetch scene by id from backend |
| `onSave` | `(scene) => Promise<void>` | 1s debounce + `beforeunload` | Persist to backend |
| `onDirty` | `() => void` | First edit after save | Show "unsaved" badge |
| `onSaveStatusChange` | `(status) => void` | `idle/pending/saving/saved/paused/error` | Top-bar status indicator |
| `onThumbnailCapture` | `(blob, cameraData) => void` | ~10s idle after camera/scene stable, 1920×1080 SSGI | Upload to cloud, use in scene list |
| `appMenuButton`, `sidebarTop`, `navbarSlot`, `viewerToolbarLeft`, `viewerToolbarRight`, `sidebarOverlay`, `viewerBanner` | `ReactNode` | Render slots | **Drop in a "Scene picker"** |
| `settingsPanelProps.onVisibilityChange` | `(visible) => void` | User toggles project visibility | Project-level permissions |
| `sitePanelProps.onUploadAsset` | `(projectId, levelId, file, type)` | Scan/guide image upload | S3/Supabase Storage |
| `sitePanelProps.onDeleteAsset` | `(projectId, url)` | User deletes scan/guide | Clean up backend |
| `presetsAdapter` | `PresetsAdapter` | Preset CRUD | Replace localStorage with backend-backed presets |
| `commandPaletteEmptyAction` | fn | No-match search | Route to AI / search |
| `extraSidebarPanels` | `ExtraPanel[]` | Always visible | Add "Saved scenes" panel |
## Sidebar slots suited for a scene switcher
- **Layout v1:** `appMenuButton` (top-left) or `sidebarTop` (above tabs)
- **Layout v2:** `navbarSlot` (full-width top nav)
- **Both:** `extraSidebarPanels` to add a dedicated "Scenes" tab
## Command palette extension
```ts
useCommandRegistry().register([
{ id: 'editor.scene.open', label: 'Open scene…', group: 'Scene', execute: () => setShowSceneList(true) },
{ id: 'editor.scene.new', label: 'New scene', group: 'Scene', shortcut: ['Meta', 'N'], execute: createNewScene },
{ id: 'editor.scene.save-as', label: 'Save as…', group: 'Scene', execute: saveAs },
])
```
## Effort to ship a minimal scene switcher inside this package
- Add `extraSidebarPanels` consumer that takes a `scenes: SceneMeta[]` + `onOpen(id)` + `onDelete(id)` + `onCreate()` — ~150 lines.
- Wire three palette commands — ~50 lines.
- Consume `onThumbnailCapture` in the example host to populate list thumbnails — ~30 lines host-side.
- **Total ≈ 12 days** for an in-editor scene browser, OR host-side if we keep the Editor scene-agnostic.
## Verdict
The Editor's architecture is **backend-agnostic by design**. Every persistence decision is a host callback. Implementing the user's vision is 100% about wiring up what already exists — no Editor refactor needed.
@@ -0,0 +1,476 @@
# R8 — MCP ↔ Editor Integration Design
**Vision.** "Call MCP → scene is saved → I open the scene in the editor, no injection."
Today `apps/editor/app/page.tsx` runs a dev-only `window.__pascalScene = useScene` hack so an MCP running in the same browser can `setScene()`. That has to die. MCP lives in Node (its own `SceneBridge` over `useScene`), editor lives in the browser (its own instance of `useScene`). They never share memory. So "saving" means **serializing a `SceneGraph` to a shared medium the editor can read**.
Below are five concrete options, ranked, with a recommendation.
---
## Option A — Filesystem handoff (`~/.pascal/scenes/<slug>.json`)
### Description
MCP writes `SceneGraph` JSON to `~/.pascal/scenes/<slug>.json` via a new `save_scene` tool. Next.js API route `GET /api/scenes/[slug]` reads the file from disk using `node:fs`. `/scene/[slug]` page fetches and calls `applySceneGraphToEditor()` on mount.
### Architecture
```
Claude Desktop / Cursor
│ stdio
pascal-mcp (Node)
SceneBridge.exportJSON()
~/.pascal/scenes/kitchen-v3.json ◄── shared disk
│ fs.readFile (server-side)
Next.js API route /api/scenes/[slug]
│ fetch()
/scene/[slug] page ──► applySceneGraphToEditor()
```
### Pros
- Zero new infra, zero network between MCP and editor
- Works offline; trivial to debug (`cat ~/.pascal/scenes/foo.json`)
- No auth, no RLS, no API contracts beyond "JSON on disk"
- Editor's existing `applySceneGraphToEditor()` already accepts a `SceneGraph`; reusing `packages/mcp/src/bridge/scene-bridge.ts` `exportJSON()` is a one-liner
- Ships this week
### Cons
- Same-machine only. Breaks the second the editor is deployed to Vercel
- No multi-user, no sharing, no "open on phone"
- Filesystem becomes the source of truth with no history, diffs, or transactions
- Vercel/serverless deployment of the editor cannot read a local user directory (dead on arrival for production)
### Dependencies
- New `save_scene` / `list_scenes` tools in `packages/mcp/src/tools/`
- New `apps/editor/app/api/scenes/[slug]/route.ts`
- New `apps/editor/app/scene/[slug]/page.tsx`
- No new npm packages. No breaking changes.
### Effort: **S** (12 days)
### Security / auth / multi-user
- No auth (filesystem ACLs only). Anyone on the box can read the scenes
- Path traversal risk on the slug — must sanitize (`slugify`, reject `..`)
- No multi-user story whatsoever
### Production readiness: **low (local dev only)**
Valid as a transitional internal tool for solo use. Not shippable as the real product path. Use it as a **stepping stone to B**.
### 5-step v0.1 plan
1. Add `save_scene({ slug })` and `list_scenes()` tools that write to `~/.pascal/scenes/<slug>.json` (Node `fs/promises`, slug sanitization, `XDG_DATA_HOME` fallback)
2. Add `load_scene({ slug })` tool that calls `bridge.loadJSON(readFileSync(...))`
3. `apps/editor/app/api/scenes/[slug]/route.ts` — GET reads `~/.pascal/scenes/<slug>.json` with path-traversal guard, returns JSON
4. `apps/editor/app/scene/[slug]/page.tsx``use client`, fetches `/api/scenes/[slug]` on mount, calls `applySceneGraphToEditor()`
5. Delete the `window.__pascalScene` injection hack from `apps/editor/app/page.tsx`
---
## Option B — Shared Supabase backend (RECOMMENDED)
### Description
MCP and editor both talk to a Supabase `scenes` table. MCP has a `save_scene` tool that `upsert`s via the service role; editor's `/scene/[id]` page SSR-fetches the row with `anon` key and RLS. Existing `env.mjs` already declares `POSTGRES_URL`, `SUPABASE_SERVICE_ROLE_KEY`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `BETTER_AUTH_SECRET`, `GOOGLE_CLIENT_ID`. The rails are laid.
### Architecture
```
Claude / agent
pascal-mcp (Node) ─────► Supabase REST (service role)
│ INSERT scenes { id, owner_id, graph_json, updated_at }
│ returns { sceneId, url: "https://.../scene/<id>" }
User pastes / clicks URL
Next.js editor (Vercel)
│ SSR with anon key + RLS
/scene/[id]/page.tsx ──► applySceneGraphToEditor()
│ on save-in-editor
PATCH scenes (owner_id == auth.uid)
```
Tables:
```
scenes (id uuid pk, owner_id uuid fk auth.users, slug text, title text,
graph_json jsonb, updated_at timestamptz, created_at timestamptz)
scene_revisions (id uuid pk, scene_id fk, author_id fk, graph_json jsonb,
created_at timestamptz, author_kind text) -- "mcp" | "editor"
```
RLS: `owner_id = auth.uid()` for select/update; public-read if `public = true`.
### Pros
- One-machine and cross-device works the same way
- Multi-user, sharing via URL, versioning via `scene_revisions` — all free with Postgres
- Auth already half-wired: `BETTER_AUTH_SECRET` + `GOOGLE_CLIENT_ID` in env.mjs suggest Better Auth planned
- Editor can deploy to Vercel unchanged
- `graph_json` is a `jsonb` column — indexable, queryable, diffable
- Natural extension path to realtime (`supabase.channel`) and presence
### Cons
- Requires running Supabase (local via CLI, or hosted project)
- MCP now has a network dependency; offline stops working unless you layer IndexedDB cache on the editor side
- RLS policy mistakes are a classic data-leak vector
- MCP needs an `owner_id` — how does a stdio MCP know who the user is? Need a device-pairing or API-key bootstrap
### Dependencies
- New `@supabase/supabase-js` dep in `packages/mcp` (dependency) and `apps/editor` (already probably pulling it or easy add)
- Supabase project or local `supabase` CLI for dev
- Migration SQL for `scenes` + `scene_revisions`
- Small change to `packages/mcp` — env bootstrap, service-role key from `~/.pascal/config.json` or `PASCAL_SUPABASE_URL` / `PASCAL_SUPABASE_KEY` env
- No breaking changes to the `SceneBridge``exportJSON()` already produces exactly what we need
### Effort: **M** (1 week for v0.1, 23 weeks to production-harden RLS and auth bootstrap)
### Security / auth / multi-user
- Full multi-user with RLS
- MCP auth problem: solve with **device-pairing** — editor generates a short-lived token in UI ("paste this into Claude Desktop config"), MCP exchanges it for a long-lived machine token. Never put the service-role key in MCP; use per-user tokens
- Supabase handles rate limiting, backups, Point-in-time recovery
- Audit trail via `scene_revisions`
### Production readiness: **high**
Right-sized for a v1 product. Same story as Figma/Linear/Notion — server of record, optional local cache.
### 5-step v0.1 plan
1. Create `supabase/migrations/001_scenes.sql` with `scenes` and `scene_revisions` tables + RLS policies (owner read/write, public-slug read)
2. Add `packages/mcp/src/adapters/supabase.ts` using `@supabase/supabase-js`, driven by `PASCAL_SUPABASE_URL` + `PASCAL_SUPABASE_USER_TOKEN` env (not service-role). Wrap in `save_scene` / `load_scene` / `list_scenes` tools
3. Add `apps/editor/lib/supabase.ts` server client; add `apps/editor/app/scene/[id]/page.tsx` (server component) that fetches and passes `graph_json` to a client `<SceneLoader>` that runs `applySceneGraphToEditor()`
4. Editor-side "save" handler: on `Cmd+S` / debounced dirty, `UPDATE scenes SET graph_json, updated_at WHERE id=... AND owner_id=auth.uid()`. Append a row to `scene_revisions`
5. Add `/settings/mcp` route in editor that mints a device token and shows the JSON block the user pastes into `claude_desktop_config.json` (`env: { PASCAL_SUPABASE_USER_TOKEN: "..." }`)
---
## Option C — MCP *is* the backend (HTTP service)
### Description
Run `pascal-mcp` permanently as an HTTP service (already supported via `connectHttp` in `packages/mcp/src/transports/http.ts`). Add non-MCP REST endpoints `GET /scenes/:id`, `POST /scenes` alongside the MCP Streamable HTTP endpoint. Editor treats the MCP host as its backend.
### Architecture
```
Claude / agent ──MCP/Streamable HTTP──┐
pascal-mcp (Node HTTP :3917)
│ in-memory SceneBridge(s) + on-disk store
Next.js editor ────GET /scenes/:id────►│
◄───JSON {nodes,rootNodeIds}
────POST /scenes───────►│
```
### Pros
- One process owns the scene graph; no dual-store problem
- Offline-friendly if MCP runs on localhost
- MCP already has HTTP transport; extending the Node `http.createServer` with a side route is ~30 lines
- Can introspect the scene via the same bridge MCP tools use → perfect consistency
### Cons
- `SceneBridge` is a singleton — multi-user requires either a bridge-per-request or a separate process per user. Neither is trivial
- Scaling story is poor: you'd have to build session isolation, persistence layer, auth, rate limits — you're rebuilding Supabase badly
- Deploying MCP-as-backend to production means running Node long-lived; no more stdio simplicity
- Ties the editor's backend lifecycle to whatever host is running MCP — if user closes Claude Desktop, the backend dies
- The MCP protocol is for tool invocation, not CRUD; layering both on one port muddies concerns
### Dependencies
- Expand `packages/mcp/src/transports/http.ts` with non-MCP REST routes (or a second `http.createServer`)
- Add a persistence adapter behind the `SceneBridge` (sqlite? jsonfile?)
- Requires an auth story if it ever leaves localhost
- No new external deps if sticking with `node:http`
### Effort: **M** (localhost dev) / **XL** (multi-user production)
### Security / auth / multi-user
- Localhost: none needed; bind to `127.0.0.1`
- Multi-user: has to add bridge sessions, auth, CORS, TLS — effectively builds a toy Supabase
- Exposing MCP HTTP publicly is a big liability (the MCP protocol itself has no native auth)
### Production readiness: **low for production, fine for local**
Reasonable for a "single-developer laptop" loop. Do not ship this as the multi-tenant story.
### 5-step v0.1 plan
1. Split `connectHttp` into `connectMcpHttp` (existing MCP route) and `connectApiHttp` (new REST). Share the same `SceneBridge` instance
2. Add `GET /api/scenes/:id`, `POST /api/scenes`, `GET /api/scenes` backed by an on-disk map `~/.pascal/scenes/*.json` (Option A persistence reused)
3. Add CORS allowlist for `http://localhost:3002` (editor dev port)
4. In the editor, `apps/editor/app/scene/[id]/page.tsx` calls `fetch('http://localhost:3917/api/scenes/[id]')` client-side
5. Wire a CLI flag `pascal-mcp --serve --port 3917 --data-dir ~/.pascal/scenes` that launches both transports
---
## Option D — Editor-as-MCP-client (live subscription)
### Description
Editor imports `@modelcontextprotocol/sdk/client` and connects to a long-running MCP server (the same `pascal-mcp --http`). It `list_tools`, `call_tool('get_scene')`, and subscribes to `notifications/resources/updated` for `pascal://scene/current`. Every change pushed from MCP triggers `applySceneGraphToEditor()`.
### Architecture
```
Claude / agent ─(MCP stdio/HTTP)─► pascal-mcp ◄─(MCP streamable HTTP)─ Editor
SceneBridge
single in-memory scene
notifications/resources/updated (push on change)
```
### Pros
- Real-time: agent edits a wall, editor redraws within an RTT
- One source of truth (MCP process)
- Reuses the MCP protocol on both sides — consistent model
- Feels magical for demos
### Cons
- MCP's resource-update notification contract is still thin in v1; `pascal://scene/current` would need to be a subscribable resource. Our server doesn't currently implement `notifications/resources/updated` — meaningful work to add
- MCP SDK was designed for tool hosts (Claude Desktop, etc.), not for browser long-running clients — running the MCP client in the browser via streamable HTTP is possible but fragile (CORS, SSE, browser-tab lifecycle)
- `SceneBridge` still isn't multi-tenant — same singleton problem as C
- MCP server outages = editor is broken. Tight coupling
- Authentication for the browser → MCP HTTP transport is not a solved problem
- Overkill if you just want "save and open"; this is real-time collab territory
### Dependencies
- `@modelcontextprotocol/sdk/client` in `apps/editor` (new dep in browser bundle — SDK is Node-first, bundle size TBD)
- Add resource subscription support to `packages/mcp/src/server.ts` and `scene-current.ts`
- CORS + auth for MCP HTTP
### Effort: **L** (mostly in MCP server — subscriptions, auth, CORS; plus editor client integration)
### Security / auth / multi-user
- Same singleton problem as C
- Browser → MCP exposes a new attack surface if public
- Each user needs their own MCP process or their own `SceneBridge` session (requires server refactor)
### Production readiness: **low**
Looks cool in a demo. Doesn't compose with Vercel-style deployments. Consider this a **phase 3 add-on** for real-time collab, layered on top of B.
### 5-step v0.1 plan
1. Add `server.sendResourceUpdated('pascal://scene/current')` hooks inside `SceneBridge` mutation methods
2. Implement `resources/subscribe` handler in `packages/mcp/src/server.ts` tracking per-transport subscriptions
3. Add browser MCP client to `apps/editor/lib/mcp-client.ts`; wire auth token header
4. `apps/editor/app/scene/live/page.tsx` — connects, calls `get_scene`, subscribes, re-applies on each notification
5. Add a toggle in `/settings/mcp` for "live mode" — falls back to Option B polling when MCP host is unreachable
---
## Option E — Local-first CRDT via Yjs/Automerge
### Description
`SceneGraph` becomes a Y.Map. MCP writes operations into a Y.Doc; editor loads the same Y.Doc from IndexedDB (same-machine) or a y-websocket server (cross-device). Conflicts resolve automatically.
### Architecture
```
Claude / agent ──► pascal-mcp (Node) ──► Y.Doc ─┐
│ y-indexeddb / y-websocket
Editor tab ─────────────────────────────► Y.Doc ┘
Custom awareness/presence
```
### Pros
- Offline-first, collaborative, real-time — best-in-class UX
- No server needed for same-machine (y-indexeddb); optional y-websocket for cross-device
- Conflict-free merges: two agents + a human editing simultaneously Just Work
- Proven architecture (Figma-ish, but open-source)
### Cons
- `SceneGraph` needs a **full rewrite** to become a CRDT-friendly structure. `nodes: Record<id, AnyNode>` + `rootNodeIds: []` map cleanly to `Y.Map<Y.Map>` and `Y.Array`, but every `AnyNode.parse()` and every Zustand mutation in `packages/core/src/store/actions/*` is currently written against plain objects
- Zundo's temporal middleware doesn't compose with Yjs's own undo manager — you'd pick one, and switching temporal layer affects every editor interaction
- Large scenes: Y.Doc updates are fast but the schema migration + cross-host Zod revalidation is nontrivial
- Doesn't solve auth/identity — you still need Better Auth or similar
- Huge blast radius: this is touching `packages/core` at its heart
### Dependencies
- `yjs`, `y-indexeddb`, optionally `y-websocket`, `y-protocols`
- Major rewrite of `packages/core/src/store` — bridging Zustand ⇄ Yjs
- New MCP adapter `packages/mcp/src/bridge/yjs-bridge.ts` mirroring `SceneBridge` but against Y.Doc
- Potentially replace Zundo with `Y.UndoManager`
- Breaking changes throughout `@pascal-app/core` — every consumer affected
### Effort: **XL** (13 months)
### Security / auth / multi-user
- Auth: still need Better Auth / Supabase Auth for identity; y-websocket needs an auth middleware
- Multi-user: best-in-class once implemented
- Server storage: y-websocket + Postgres persistence (e.g., `y-postgresql`) or S3 snapshots
### Production readiness: **high if you commit**; **trap if you don't**
If you want Figma-quality multiplayer, this is the right answer long-term. But it is a complete rearchitecture of the core store. Not a starter move.
### 5-step v0.1 plan
1. Prototype a Yjs binding for `packages/core/src/store/use-scene.ts` behind a feature flag; keep the plain Zustand path default
2. Build a minimal Y.Doc ⇄ `SceneGraph` serializer and prove round-trip parity against existing scenes
3. Write `packages/mcp/src/bridge/yjs-bridge.ts` that wraps Y.Doc in the same interface as `SceneBridge`
4. Run a same-machine POC: MCP writes to Y.Doc → y-indexeddb → editor tab observes via `Y.Map.observe`
5. Defer y-websocket to phase 2 after same-machine parity is proven
---
## Option F (bonus) — Local daemon + shared SQLite
### Description
`pascal-mcp` runs as a background daemon on localhost. Scenes persist into a single SQLite file (`~/.pascal/pascal.db`) via better-sqlite3. The editor SSR or a Next.js API route opens the same SQLite file read-only. Pure local, durable, queryable, zero network.
### Architecture
```
Claude ──► pascal-mcp (daemon) ──► SQLite (~/.pascal/pascal.db) ◄── Next.js API route
```
### Pros vs A
- Transactional; no torn writes
- Indexable (JSON1 extension in SQLite); fast list/search
- Natural version table (`revisions` table)
- Still zero network, zero external infra
### Cons
- Two readers can deadlock on SQLite unless WAL mode + careful opens; Next.js dev server and MCP both holding the file needs care
- Still same-machine only; doesn't deploy to Vercel
### Effort: **SM**. Worth listing because it's a **better A** without much more work.
---
## Recommended approach + rationale
**Ship Option A this week as a dev loop, commit to Option B as the product path, and keep Option D on the roadmap for a phase-3 "live mode".** Concretely:
1. **Week 1:** Option A (filesystem). Two days of work, ships the "no injection" promise for your own machine. Kills the `window.__pascalScene` hack. Unblocks every subsequent demo. Treat it as a local cache layer that survives option B — keep the `~/.pascal/scenes/` format stable so offline mode later just reads from it.
2. **Weeks 24:** Option B (Supabase). This is the only option that:
- Scales past your laptop
- Composes with the existing `env.mjs` (Supabase, Better Auth already declared)
- Supports multi-user without rearchitecting `@pascal-app/core`
- Matches the deployment target (Next.js on Vercel)
Supabase's `jsonb` + RLS + auth is a perfect fit for "store a scene, open it at `/scene/<id>`". The MCP side is ~200 lines: `save_scene` / `load_scene` / `list_scenes` tools calling `supabase.from('scenes').upsert({...})`.
3. **Quarter 2:** Option D live mode as a pro feature. Layer real-time on top of B via Supabase Realtime (`supabase.channel('scene:<id>').on('postgres_changes', ...)`), not via MCP resource subscriptions — avoids the browser-as-MCP-client rabbit hole.
4. **Option E (CRDT)** is only right if you commit to Figma-grade multiplayer. It's a 3-month project across `@pascal-app/core` and shouldn't be started until you have product-market signal that multiplayer is the moat.
5. **Option C** is tempting as a "one process runs everything" story but rebuilds Supabase badly. Skip unless the product demands a fully-local air-gapped build.
### Why B over C
C asks `SceneBridge` to become a multi-tenant database. `SceneBridge` is a thin wrapper over a Zustand singleton — making it multi-tenant is a rewrite. Supabase already is one.
### Why B over E (for now)
E makes the *editor* collaborative, but the user's stated problem is "save → open", not "co-edit in real time". B solves the stated problem in 10% of the effort and leaves the door open for E later (you can replace the `graph_json` column with a `y_doc` column in a migration and nothing else in the app has to change, because `applySceneGraphToEditor()` still accepts a `SceneGraph`).
---
## 30/60/90 day roadmap
### Day 030 — "Unblock the loop"
- **A (12d):** filesystem handoff; MCP `save_scene` + editor `/scene/[slug]`. Delete `window.__pascalScene`.
- **B-alpha (2w):** Supabase migrations for `scenes` + `scene_revisions`, RLS, MCP tools using user tokens, editor `/scene/[id]` server component.
- **Dogfood:** the team uses `save_scene` from Claude Desktop every day. Bugs get filed.
### Day 3160 — "Productionize"
- **Auth bootstrap:** `/settings/mcp` device-pairing flow. Mints a scoped Supabase user JWT (not service-role) for MCP.
- **Editor → Supabase save:** debounced on-change writes + "Save" button. Append `scene_revisions` rows with `author_kind: 'editor'`.
- **History UI:** simple diff viewer across `scene_revisions`.
- **Sharing:** `public = true` flag → public read. Share URL works.
- **Offline cache (optional):** keep Option A filesystem writes as a redundant cache so scenes survive Supabase outages.
### Day 6190 — "Live mode + polish"
- **Realtime via Supabase channels (not MCP D):** editor subscribes to `postgres_changes` on the current scene and applies remote updates. Gives near-realtime collab without Yjs effort.
- **Presence:** Supabase Realtime presence for "agent is editing".
- **Figma-style undo across actors:** scoped per-user temporal history (use `clearSceneHistory()` when switching scenes; keep Zundo per tab).
- **Decide on E:** if usage shows genuine concurrent-agent-plus-human friction, spike Yjs in Q2. Otherwise defer.
---
## Risks + mitigations
| Risk | Severity | Mitigation |
|---|---|---|
| MCP needs a user identity to write to Supabase with RLS | High | Device-pairing UI in editor that mints a short-lived JWT → long-lived machine token. Never ship the service-role key. |
| Concurrent MCP + editor writes clobber each other | Medium | Optimistic concurrency: include `updated_at` in UPDATE `WHERE` clause; if mismatch, append a revision instead of overwriting and surface a merge prompt. |
| Large `graph_json` payloads blow past Supabase's 8MB body limit | Medium | Measure casa-sol (test scene in `test-reports/casa-sol/`) and typical scene sizes. If >1MB, chunk or move to Storage buckets. |
| Serverless cold starts on `/scene/[id]` make "open" slow | Low | Use Next.js `revalidate: 0` + ISR; Edge runtime where possible; cache in localStorage on client. |
| `applySceneGraphToEditor` is a client-only function, can't run on SSR | Low | Keep it client; `<SceneLoader>` component does `useEffect(() => applySceneGraphToEditor(initialGraph), [])`. |
| Supabase outage breaks the whole editor | Medium | Layer Option A on top: editor falls back to last-known-good from localStorage when fetch fails. |
| RLS misconfiguration leaks scenes across users | Critical | Policy tests in `supabase/tests/`; add a dedicated "rls" CI step; `anon` client gets read-only and only `public = true` rows. |
| MCP Node process can't reach `~/.pascal` on Windows Claude Desktop install | Low | Use `envPaths('pascal')` (via `env-paths`) to resolve per-OS. |
| Slug collisions / path traversal via `save_scene({ slug: '../foo' })` | High (for A) | Strict slug regex `^[a-z0-9-]{1,64}$`; reject everything else. |
| MCP writes a scene under user A; user B opens the URL → leak | High (for B) | Only share by signed URL or explicit `public = true` flag. Default visibility is `owner-only`. |
---
## Open questions for the user
1. **Are scenes per-user or per-project?** Current `projectId="local-editor"` suggests projects exist — do MCP-created scenes live under a project, or are they free-floating until assigned?
2. **How does a stdio MCP know who the user is?** Is the intended flow "user signs into editor → editor emits a device token → user pastes into Claude Desktop config"? Or "MCP is always anonymous and scenes live in a shared staging bucket"?
3. **Offline requirements?** Must editing work with zero network (implies Option A cache on top of B), or is online-required acceptable for v1?
4. **Deployment target for the editor?** Vercel (rules out C, F) or self-hosted (all options viable)?
5. **Scene mutability after save?** Can the editor edit a scene MCP created and have those edits visible to the next MCP call? (Implies bidirectional sync, easiest via B's REST; harder via A's file racing.)
6. **Auth provider?** `env.mjs` has `BETTER_AUTH_SECRET` + `GOOGLE_CLIENT_ID` — is Better Auth the plan, or Supabase Auth? They can coexist but one is source of truth for `auth.uid()`.
7. **Versioning/history UX?** Is "every MCP call appends a revision" desired, or should only explicit saves create revisions? Affects `scene_revisions` write patterns.
8. **Multi-agent story?** If two Claude Desktops write to the same scene concurrently, which wins? (Punts on this until Yjs/E; in B, last-write-wins with optimistic concurrency is a fine v1.)
9. **How does "open in the editor" trigger?** Does MCP return a URL and the user clicks? Or does MCP invoke a deeplink (`pascal://scene/<id>` handler) that focuses an already-open browser tab?
10. **Catalog availability in MCP.** Today `pascal://catalog/items` returns `catalog_unavailable` in headless mode. If a scene references catalog items, does the editor need to re-hydrate them on open, or does MCP have to snapshot the catalog into the scene graph?
@@ -0,0 +1,184 @@
# R9 — Production Readiness Assessment
**Scope:** "MCP creates scene → user opens it in editor" workflow.
**Current state:** 13 commits on `feat/mcp-server`. MCP server ships stdio + streamable HTTP transports, 19 scene tools, vision sampling, resources/prompts. Target: Option B from R8 (server-side persistence with user-scoped scenes). Baseline: single-user editor with `localStorage` autosave, no backend, no auth, no Supabase client in the repo.
---
## 1. Readiness matrix
| Dimension | Current state | Needed for GA | Gap |
|---|---|---|---|
| **Transport security** | stdio (local) + HTTP on `0.0.0.0:<port>`, no TLS, no auth, `sessionIdGenerator` is `randomUUID()` but session unbound to user | TLS termination (ALB/Cloudflare), per-request auth, origin allowlist, DNS rebinding guard | No auth, no TLS, binds `0.0.0.0` by default (`transports/http.ts:30`) |
| **Auth (human → editor)** | None. `projectId="local-editor"` hardcoded in `apps/editor/app/page.tsx:31` | OAuth (GitHub/Google) or magic link; session cookie; JWT for API; Supabase Auth if we adopt it | Starting from zero; no user model, no login UI |
| **Auth (MCP → API)** | None. stdio spawns local process; HTTP transport accepts any caller | Per-user MCP tokens (OAuth2 device flow or PAT), token rotation, scoped capabilities | No token concept, no issuer, no revocation |
| **Persistence** | `localStorage` only (`editor/src/lib/scene.ts:379`) | Server-side DB with per-user rows, versioned rows, RLS | No DB, no API, no migration story |
| **RLS / ownership** | N/A (no DB) | Postgres RLS: `USING (auth.uid() = owner_id)` on `scenes`, `scene_versions`, `scene_assets` | Needs full data model from scratch |
| **URL validation in scenes** | `GuideNode.url`, `ScanNode.url`, `MaterialSchema.texture.url`, `ItemNode.thumbnail`/`src` are bare `z.string()` (`core/src/schema/nodes/guide.ts:7`, `scan.ts:7`, `material.ts:33`, `item.ts:81-82`) | Allowlist (our CDN + signed-URL origins only), SSRF-safe parser, `data:` caps, `blob:` rejection at save-time | Zero validation; SSRF primitive surfaces any time editor or MCP renders a scene |
| **CSP / headers** | `next.config.ts` allows images from `protocol: https, hostname: '**'` and `protocol: http, hostname: '**'` | Explicit CSP (`img-src`, `connect-src`, `media-src`, `script-src 'self'`), HSTS, `X-Frame-Options`, `Referrer-Policy` | No headers set; wildcard image hosts |
| **Rate limiting** | None on HTTP transport (`transports/http.ts`) | Token-bucket per user + per-IP; stricter bucket on mutating tools; MCP tool-call ceiling | None |
| **Quota** | None. An agent can call `create_wall` infinitely; `setScene` accepts any-size JSON | Per-user scene count cap, per-scene node count cap (e.g. 50k), per-version bytes cap (e.g. 5 MB), monthly tool-call cap | None |
| **Size caps** | None. Next `serverActions.bodySizeLimit: '100mb'` (`next.config.ts:19`) is the only ceiling | Explicit per-endpoint limits (256 KB scene patch, 5 MB full save), gzip required, reject on oversize before parse | 100 MB server-action body limit is a DoS amplifier |
| **Concurrency** | Last-write-wins implicit; no version token, no lock | Optimistic concurrency via `if-match: <version>` ETag; reject stale saves; later: CRDT (Yjs/Automerge) for true multi-agent | No detection at all; silent overwrite |
| **Versioning** | `temporal` (zundo) exists in-memory; not persisted | Every save creates `scene_versions` row; retain last N + all "named" versions; soft delete | No persistence at all |
| **Schema evolution** | `setScene.migrateNodes` hook exists in core (per `CROSS_CUTTING.md` §2) but no migration registry | Versioned schema tag on every row (`schema_version: int`), forward-migration functions, replay on load | No schema version field in scene JSON today |
| **Observability** | `console.error` only (`transports/http.ts:33`) | Structured logs (JSON), trace ID per MCP request, Sentry/similar error reporting, metric counters for tool calls | No traces, no error pipeline, no metrics |
| **Audit log** | None | Append-only `scene_events` table with user, tool, timestamp, diff-size, source (mcp/human) | None |
| **GDPR / data rights** | N/A (no user data stored server-side) | DSAR endpoint (export scenes as JSON/GLB), deletion pipeline, consent banner, processor contracts | Needs legal + product work |
| **Cost model** | Storage = 0 (localStorage on user's device) | Budget per user: ~10 MB scenes + thumbnails; CDN egress ~100 MB/mo free tier; vision sampling cost per call | Unknown; depends on choice of storage (Supabase Storage vs S3) |
| **Offline support** | Implicit — `localStorage` works offline | Service Worker + IndexedDB mirror; background sync queue; conflict resolution on reconnect | Current localStorage works but only on same device/browser |
| **Multi-agent collab** | None (single Zustand store, no broadcast) | Realtime channel (Supabase Realtime / Ably / WebSockets); op-log or CRDT; presence | Architectural rewrite |
| **Thumbnail pipeline** | Client-side only (`thumbnail-generator.tsx`) | Server-side rendering worker (headless Three or pre-rendered bake); CDN caching; signed URLs | No server path; MCP cannot currently produce a thumbnail without the editor |
| **Testing at load** | Unit + smoke tests only | Load tests (k6/artillery): 100 concurrent MCP sessions, 1000 writes/min, 95p < 500 ms | No load suite |
| **Secrets hygiene** | No secrets in repo yet | HSM/Vault, per-env keys, rotation policy, supply-chain scanning (SBOM) | Not addressed |
---
## 2. Top 10 risks ranked by severity
1. **SSRF via scene URLs (Critical)**
Any `GuideNode.url` / `ScanNode.url` / `MaterialSchema.texture.url` is a `z.string()`. If MCP writes a scene containing `http://169.254.169.254/latest/meta-data/` or `http://localhost:6379`, when a user later opens that scene the editor `<img>` / `<texture>` load will fetch it from the user's browser or from an SSR render pipeline. With wildcard `images.remotePatterns` this is already loaded client-side. Severity high because MCP is exactly the attacker-controllable input source.
2. **No auth on HTTP transport (Critical)**
`transports/http.ts` binds `0.0.0.0` and generates session IDs client-gettable. Anyone on the network (or Internet if exposed) can invoke every tool — including `apply_patch`, `delete_node`, and write through to an eventual backend. Today this is "only local," but that is a deploy-time decision; the code has no hard barrier.
3. **No user model → no meaningful RLS possible (High)**
Everything below depends on user identity. Without auth, quotas, audit trails, data deletion, concurrent-edit resolution, and cost accounting all collapse to guesswork.
4. **Unbounded scene size / tool-call rate → DoS + cost blowup (High)**
`apply_patch` accepts batched ops with no ceiling. `place_item` can be called in a loop. Combined with Next's 100 MB server-action limit, a compromised MCP can push gigabyte-scale scenes or detonate CDN bills.
5. **Last-writer-wins silent overwrite (High)**
Two agents (or an agent + a human) editing simultaneously: whoever saves last wins, no warning. With MCP autonomous workflows this is likely, not hypothetical.
6. **No schema version on persisted scenes (High)**
First breaking change to `@pascal-app/core` schemas (e.g. `SiteNode.children` fix in `CROSS_CUTTING.md` §2) will silently corrupt saved scenes. There is no `schema_version: n` today.
7. **Dev bridge leaks scene store to window (Medium)**
`apps/editor/app/page.tsx:13-15` sets `window.__pascalScene` in non-production. If `NODE_ENV` is ever mis-set, or a preview deploy ships, any XSS becomes a full scene-graph takeover. Guard is environment-string based, not build-time stripped.
8. **No CSP; wildcard image hosts (Medium)**
`next.config.ts` allows `http(s)://**`. Combined with Risk 1 this is a clean data exfiltration channel: attacker-controlled URL in scene → user's browser GETs `https://attacker.com/?cookie=...` as an image load. `document.cookie` doesn't leak, but `Referer` and timing do.
9. **No observability → breaches invisible (Medium)**
Only `console.error`. No audit log, no trace IDs. We would not detect an in-progress compromise until a user complained.
10. **Supply chain: `@modelcontextprotocol/sdk` and vision tooling (Medium)**
MCP SDK is v1.29.0 and moving fast. Vision tools call out to the host's model provider. Neither has SBOM, pinned digests, or review gate in our CI.
---
## 3. Recommended hardening order
Phased by dependency: each phase unblocks the next.
### Phase A — "don't ship HTTP transport to the open Internet" (days)
1. Default HTTP bind to `127.0.0.1`; require explicit `--bind 0.0.0.0` flag with warning.
2. Add `Origin` / `Host` header check for DNS rebinding (MCP SDK 1.29 has a guard; verify enabled).
3. Mandatory bearer token on HTTP transport; `PASCAL_MCP_TOKEN` env; reject without it.
4. Strip `window.__pascalScene` at build-time (`defineConfig` constant) rather than runtime `NODE_ENV` check.
5. Add strict CSP to `apps/editor` (`Content-Security-Policy: default-src 'self'; img-src 'self' data: https://<our-cdn>; ...`).
6. Replace `z.string()` with `z.string().url()` plus a **URL validator** on `GuideNode.url`, `ScanNode.url`, `MaterialSchema.texture.url`, `ItemNode.thumbnail`/`src`. Allowlist: `data:image/*` (≤ 256 KB), our CDN origin, and signed-URL hosts only. Reject `file:`, `blob:`, `javascript:`, private IPs, link-local, `.internal`.
### Phase B — auth + persistence skeleton (weeks 12)
7. Pick auth stack (Supabase Auth, Clerk, or self-rolled NextAuth). Supabase gives RLS + storage + realtime for free, so it's the low-friction default even if R8's Option B is a different DB.
8. Design minimal schema:
- `scenes(id, owner_id, name, current_version_id, created_at, updated_at, schema_version int)`
- `scene_versions(id, scene_id, parent_version_id, body_jsonb, byte_size, author_id, source enum('human','mcp'), created_at)`
- `scene_assets(id, scene_id, kind, sha256, cdn_url, byte_size, owner_id)`
- `mcp_tokens(id, user_id, hashed_token, scopes, last_used_at, revoked_at)`
9. RLS on all four tables: `owner_id = auth.uid()`. Never use Supabase service role from browser.
10. Server API (Next Route Handlers or tRPC): `POST /api/scenes`, `GET /api/scenes/:id`, `PUT /api/scenes/:id` (takes `if-match` ETag = version ID). All checks `auth.getUser()`.
11. MCP: add `PASCAL_API_URL` + `PASCAL_API_TOKEN` env. Every tool that mutates routes through `apiClient`. Token = per-user PAT, hashed in DB, revocable.
### Phase C — quotas, size caps, rate limiting (week 23)
12. Per-user quotas: 100 scenes, 50k nodes/scene, 5 MB/version, 10k MCP tool calls/day. Enforce at write-path.
13. Rate limiting: Upstash Ratelimit or Postgres advisory locks. 100 req/min global, 20 req/min mutating.
14. Reject requests with `content-length` > cap before reading body.
15. Add size budget to `apply_patch`: max 500 ops per call; reject otherwise.
### Phase D — concurrency + versioning (week 34)
16. Every `PUT /api/scenes/:id` requires `if-match` ETag. On mismatch return 409 with the current version for client merge.
17. Insert a `scene_versions` row on every successful save. Retain last 50; keep all "named" ones; soft-delete older.
18. Expose `GET /api/scenes/:id/versions` + `GET /api/scenes/:id/versions/:v` for history UI.
19. Add `schema_version` to persisted body (start at `1`); migration registry `coreSchemaMigrations[n]` in `@pascal-app/core`; run on load.
### Phase E — observability + audit (week 45)
20. Structured JSON logs (pino), trace IDs propagated through MCP tool calls via headers / session meta.
21. Sentry (or equivalent) for both editor and MCP server.
22. Append-only `scene_events(id, scene_id, user_id, tool, diff_size, source, ts)`.
23. Basic dashboard: writes/min, tool mix, p95 latency, error rate.
### Phase F — compliance + cost (week 56)
24. DSAR endpoint `GET /api/me/export.zip` (all scenes + versions + assets).
25. Account deletion pipeline: hard-delete within 30 days, audit record of deletion.
26. Privacy notice update (`apps/editor/app/privacy/page.tsx`) to describe MCP ingress.
27. Cost model: storage $/scene (estimate ~50 KB avg compressed JSON, 500 KB thumbnails), CDN egress, Sentry seat, Supabase tier.
### Phase G — collab (month 2+)
28. Realtime channel per scene; presence; ephemeral locks per subtree.
29. CRDT decision (Yjs with a lossless bridge to our scene graph) — or stay with OT + server-authoritative ops.
---
## 4. "Beta" vs "GA" checkpoints
### Ready for beta (closed, trusted users, ≤ 100 accounts)
- Phase A complete.
- Phase B (auth + persistence skeleton) complete.
- Phase C-lite: soft quotas + rate limiting; no hard enforcement on node count yet.
- Phase D-lite: `if-match` ETag on writes; version history retained but not yet surfaced in UI.
- Observability: Sentry + basic logs. No dashboards required.
- Privacy notice updated. No DSAR endpoint yet (manual support OK for ≤ 100 users).
- Acceptance criteria:
- Two agents hitting the same scene get a clean 409 on the loser, not silent overwrite.
- Saving a 6 MB scene returns a structured error, not a 500.
- Loading a 2-week-old scene still works after a schema change.
- Revoking an MCP token blocks that client within 60 s.
- An attacker-controlled URL in a scene does **not** cause the editor to call out to `169.254.169.254`.
### Ready for GA (open signup, cost accountable)
- All of Phase AF complete.
- Load tested: 500 concurrent MCP sessions, 2000 writes/min, p95 < 500 ms for read, < 1 s for write.
- Full audit log searchable by operator.
- DSAR + deletion pipeline with SLA (≤ 30 days).
- Written incident response runbook; on-call rotation.
- External pen test focused on MCP transport + URL sanitization (repeat of Phase 3 audit).
- CSP in `Content-Security-Policy` header (not just report-only).
- Thumbnail pipeline server-side (so a scene created by MCP can be listed in a gallery without opening the editor).
- Phase G (realtime collab) can be post-GA if we accept "single active editor per scene at a time" as a UX contract for v1.
---
## 5. Verdict
**Weeks or months to production: ~1014 weeks minimum to GA, ~45 weeks to credible private beta**, assuming one full-time engineer on the hardening work and Option B from R8 (server-side persistence) is chosen.
Rough breakdown:
- **Private beta: ~45 weeks** (Phases AD at MVP depth).
- **Public beta: ~8 weeks** (add Phase E, quota enforcement, version UI, one round of pen-test fixes).
- **GA: ~1214 weeks** (add Phase F: compliance, cost accounting, DSAR, load-test-driven tuning, external pen test).
The schedule is dominated by:
1. **Auth + persistence from scratch** — the repo has none today. Supabase would compress this to ~1 week; NextAuth + self-hosted Postgres is ~23 weeks.
2. **URL hardening on the core schemas** — a breaking change requiring a migration, though small in code size.
3. **Concurrency model** — ETag-based OCC is ~1 week; real CRDT collab is a month and probably post-GA.
**Blockers that could push this out:**
- If R8 picks an Option B that requires rewriting `@pascal-app/core` schemas (e.g. moving to a DB-native format), add 24 weeks.
- If legal requires SOC 2 or EU data residency before launch, add 23 months.
- Any real-time multi-agent requirement in v1 moves GA out by 46 weeks.
**Recommendation.** Ship Phase A (transport hardening + URL validation) in the first week independently of R8 — it's cheap, it reduces blast radius today, and it's not coupled to the persistence choice. Block any public deploy of the HTTP transport until Phase A lands.
@@ -0,0 +1,154 @@
# Research synthesis — "MCP creates scene → I open it in the editor"
> 10 parallel research agents (R1R10) investigated this workflow against the Pascal repo. This document pulls their findings into a single actionable answer.
## Direct answer to your question
**Yes, this is the right approach. And it's about 40% already built.** The Pascal Editor was designed from day one to be backend-agnostic — `onLoad(sceneId)` / `onSave(scene)` callbacks are public props. The plumbing that's missing isn't the Editor; it's the **scene-entity layer** (id, name, thumbnail, owner) and **a backend to store it**. The groundwork for that backend is already laid in env vars and privacy policy, but zero lines of backend code exist yet.
## What exists today (40%)
| Piece | Status | Evidence |
|---|---|---|
| Scene graph serialization | ✅ done | `SceneGraph` type; `export_json` MCP tool; `"Save Build"` UI button |
| Scene graph deserialization | ✅ done | `applySceneGraphToEditor()` + `setScene()`; `"Load Build"` UI button |
| Autosave pipeline (debounced, status-reported) | ✅ done | `use-auto-save.ts` with 6-state machine + `onSaveStatusChange` |
| Host persistence hooks (`onLoad`, `onSave`, `onDirty`) | ✅ done | `<Editor>` props, R2 |
| Thumbnail auto-capture | ✅ done | `onThumbnailCapture` fires ~10s after scene stable, 1920×1080 SSGI |
| Store-level project scoping | ✅ done | `projectId` prop flows through viewer + selection |
| localStorage fallback persistence | ✅ done | `pascal-editor-scene` key |
| IndexedDB for assets | ✅ done | `idb-keyval` for texture blobs |
| Single route (`/`) | ✅ done | but no dynamic segments — R4 |
## What's missing (60%)
| Piece | Effort | Owner |
|---|---|---|
| Scene entity metadata (id, name, thumbnail, owner, created_at) | S | R2 gap |
| Backend storage (Supabase `scenes` table) — env is declared, code is zero | M | R5 gap |
| Dynamic routes `/scene/[id]` and `/editor/[projectId]/[sceneId]` | S | R4 gap |
| Scene-list UI (picker, rename, delete, duplicate) | M | R3 gap |
| MCP tools for scene lifecycle (`save_scene`, `list_scenes`, `load_scene`, `delete_scene`) | S | R8 |
| Zod validation at the scene-load boundary | XS | R6 gap (pre-existing security finding) |
| Auth (Supabase auth / Better Auth — env is declared, code is zero) | M | R5, R9 gap |
| Device-pairing flow so MCP acts as the user | M | R9, R8 |
## The recommended plan — R8's phased approach
**Ship Option A this week. Commit to Option B for production. Defer D (real-time) to Q2. Skip C and E.**
### Week 1 — Option A: filesystem handoff (kills the injection hack)
```
MCP ──► ~/.pascal/scenes/<slug>.json ──► Next.js API route ──► /scene/<slug> page ──► applySceneGraphToEditor()
```
- New MCP tools: `save_scene({ slug })`, `load_scene({ slug })`, `list_scenes()`.
- New Next.js route `/scene/[slug]` that fetches `/api/scenes/[slug]` and loads via the existing `applySceneGraphToEditor` utility.
- Delete `window.__pascalScene` injection from `apps/editor/app/page.tsx`.
- **Effort: 12 days. No new deps. No breaking changes.**
### Weeks 24 — Option B: Supabase backend (the product path)
```
MCP ──► Supabase (SERVICE_ROLE) ──► scenes table
Editor /scene/[id] ──► Supabase (ANON_KEY + RLS) ──► scenes row
```
Schema:
```sql
create table scenes (
id uuid primary key default gen_random_uuid(),
project_id uuid references projects(id),
owner_id uuid references auth.users(id),
name text not null,
graph_json jsonb not null,
thumbnail_url text,
version int not null default 1,
public boolean not null default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create table scene_revisions (
scene_id uuid references scenes(id) on delete cascade,
version int,
graph_json jsonb,
author_kind text check (author_kind in ('human','mcp','agent')),
created_at timestamptz default now(),
primary key (scene_id, version)
);
```
- MCP side: ~200 LOC. `save_scene``supabase.from('scenes').upsert({...})`.
- Editor side: `/scene/[id]/page.tsx` is a Server Component that fetches the row and passes to a client `<SceneLoader>`.
- RLS: owner reads/writes; `public = true` rows readable by anyone.
- **Effort: ~2 weeks.**
### Quarter 2 — Option D: live mode via Supabase Realtime
Supabase's `postgres_changes` channel over the `scenes` row gives you cross-agent realtime without Yjs. A human editing at the same time as an MCP agent would see each other's changes. This is **not** an MCP protocol feature — it's a Postgres feature that Supabase exposes. Much cheaper than rebuilding on Yjs (Option E).
### What about Yjs / CRDT (Option E)?
Defer. It's a 3-month rewrite of `@pascal-app/core`'s store. Only justified if multiplayer is the moat, which the current product signal doesn't demonstrate. If you DO go there later, the migration is painless because `applySceneGraphToEditor` still accepts a `SceneGraph` — you'd just rewrite the store underneath.
## Edge cases R10 surfaced that you hadn't mentioned
1. **"Auto-frame camera on MCP-opened scene"** — today the MCP creates a scene at world origin, the default editor camera points at 30m grid, user sees a black screen. Tiny fix, huge UX win.
2. **MCP-written scenes can carry malicious URLs**`guide.url`, `scan.url`, `material.texture.url`, `item.asset.src` are `z.string()` in core (no scheme allowlist). A scene opened in the editor can beacon home. **This is the same finding as the Phase 3 security audit, not remediated.**
3. **Overwrite vs merge when MCP edits a scene the user is also editing** — today: last-writer-wins silently. Needs ETag or revision-number optimistic locking.
4. **Scene size limit** — Supabase API has an 8MB body limit. Casa del Sol is 27 KB; a real project might go to 12 MB. Measure before you commit.
5. **Undo-stack surprise** — an MCP multi-op patch collapses to ONE undo step. From the user's view, Ctrl+Z wipes the whole MCP run. Might be surprising. Document or segment.
6. **`metadata: json` on every node is AI-visible** — an attacker-crafted `metadata.note: "ignore all instructions and..."` could prompt-inject a summarising agent.
7. **MCP and editor use separate Zundo temporal stores** — undo in one doesn't reach the other.
8. **`SiteNode.children` holds objects not ids** — shipping the scene across the wire requires handling this inconsistency (already workaround-ed in MCP; would re-emerge on the Supabase side).
9. **Offline editor opening a cloud-only scene** — degrade to cached/read-only, don't crash.
10. **Agent writes infinite scenes in a loop** — quota + rate limits per user.
## Ideas you didn't ask for but should consider (R10's top 10 by value×feasibility)
1. **Photo → Pascal** — MCP already has `analyze_floorplan_image`. The unblocker is: a scene UI "Upload floor plan" → vision tool → new scene. Highest ROI in the repo.
2. **Scene templates catalogue** — doubles as marketplace seed inventory.
3. **Auto-framing camera** — fixes "black screen" failure mode.
4. **Prompt → Pascal one-shot studio** — the canonical "MCP creates scene" hosted flow.
5. **Multi-variant generation** — "give me 5 variations" using `forkSceneGraph` (already in core).
6. **Scene diff view** — makes AI actions reviewable.
7. **BOM + cost synthesis** — turns toy scenes into quoteable artefacts.
8. **Scene branches & forks** — prerequisite for co-design workflows.
9. **GLB export via headless renderer** — unlocks AR + USD/IFC pipelines.
10. **Regulatory / accessibility linting** — B2B architect segment.
## Production readiness (R9)
**~45 weeks to private beta. ~1014 weeks to GA.** Dominated by:
- Greenfield auth + persistence (Supabase rails declared but zero code).
- URL hardening migration on core node schemas (close the security audit's Phase 3 gap properly, not just in MCP).
- Optimistic concurrency / revision tracking.
- ETag-based merge UX.
Phase A (transport hardening + URL validation) is decoupled and can ship week 1 as a defensive floor.
## What you should tell me next
Answer these and I can write the implementation PR:
1. **Deployment target** — local-only (Option A sufficient), or Vercel + multi-user (must do B)?
2. **Auth direction** — Supabase Auth, Better Auth (env var is there), Clerk, or none yet?
3. **Scope for v0.1** — just "save + open" with one hardcoded user, OR proper multi-tenant + sharing from day one?
4. **Scene-list UI location** — inside the Editor package (add a new panel), inside the host app (`apps/editor`), or both?
5. **What do I do with the current `feat/mcp-server` branch?** — merge as is (MCP server + test-reports), or fold this new work into the same branch, or open a new `feat/mcp-persistence` branch?
My recommendation in one line: **answer 1 = Vercel/multi-user → ship Option A as a branch-local step this week, then B over weeks 24, and merge the whole thing as `feat/mcp-cloud-scenes`**.
## Report index
- [R1 — Persistence layer](./R1-persistence.md) — localStorage-only, single key, no backend
- [R2 — `projectId` semantics & Editor API](./R2-project-id.md) — Editor is backend-agnostic via `onLoad`/`onSave`
- [R3 — Scene management UI](./R3-scene-ui.md) — 40% there; needs scene list + palette commands
- [R4 — Routing & URLs](./R4-routing.md) — zero dynamic routes; latent expectation of `/editor/<projectId>/…`
- [R5 — Backend / Supabase](./R5-backend.md) — env declared, zero code
- [R6 — File I/O pathways](./R6-file-io.md) — "Save/Load Build" work; no Zod validation on import
- [R7 — `@pascal-app/editor` API](./R7-editor-api.md) — rich callback surface; scene switcher is a 12 day host feature
- [R8 — Integration design options](./R8-mcp-integration-design.md) — A→B→D phased recommendation
- [R9 — Production readiness](./R9-production-readiness.md) — 45 weeks to beta, 1014 to GA
- [R10 — Ideas and edge cases](./R10-ideas-and-edges.md) — 300+ lines; top 10 ranked by value×feasibility