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:
co-authored by
Claude Opus 4.7
parent
bdb47a6e5d
commit
42bd05db9c
@@ -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:104–135` |
|
||||
| Every UI state mutation | `pascal-editor-ui-preferences` | Zustand persist in `use-editor.tsx:372–607` |
|
||||
| Every viewer state mutation | `viewer-preferences` | Zustand persist in `use-viewer.ts:81–220` |
|
||||
| Every audio state mutation | `pascal-audio-settings` | `use-audio.tsx:22–43` |
|
||||
| On `beforeunload` | Final scene snapshot | `use-auto-save.ts:137–147` |
|
||||
|
||||
## Read pathways
|
||||
|
||||
1. **Editor mount** (`editor/index.tsx:765–796`):
|
||||
- If host supplied `onLoad` → `await onLoad()`
|
||||
- Else → `loadSceneFromLocalStorage()`
|
||||
- Apply via `useScene.setScene(nodes, rootNodeIds)`
|
||||
2. **Selection hydration** — `syncEditorSelectionFromCurrentScene()` (`lib/scene.ts:251–332`)
|
||||
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** 1–2 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: S–M (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: M–L 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. $50–200/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** — S–M 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:147–157)
|
||||
↓
|
||||
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` | 205–216, 362–365 |
|
||||
| "Load Build" → file picker `.json` → `setScene` | `…settings-panel/index.tsx` | 218–239, 367–383 |
|
||||
| "Export Scene (JSON)" in command palette | `editor-commands.tsx` | 328–346 |
|
||||
| "Export GLB / STL / OBJ" | `export-manager.tsx` | — |
|
||||
| "Clear & Start New" destructive button | `settings-panel/index.tsx` | 427–434 |
|
||||
| "Explore scene graph" read-only tree dialog | `settings-panel/index.tsx` | 398–421 |
|
||||
| Autosave hook (1 s debounce → onSave or localStorage fallback) | `use-auto-save.ts` | 1–191 |
|
||||
| Scene-dirty tracking exposed to host via `onDirty` / `onSaveStatusChange` | `use-auto-save.ts` | 56–187 |
|
||||
|
||||
## 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:18–19` — `POSTGRES_URL`, `SUPABASE_SERVICE_ROLE_KEY` (server-only, `.min(1)`)
|
||||
- `env.mjs:12–14` — `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `GOOGLE_CLIENT_*`
|
||||
- `env.mjs:27–31` — `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||
- `turbo.json:9–20` — same vars listed as build-cache keys
|
||||
- `apps/editor/app/privacy/page.tsx:95–97` — "Your data is stored using Supabase (PostgreSQL database)"
|
||||
- `.gitignore:22–24` — 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:205–216` | `layout_YYYY-MM-DD.json` |
|
||||
| Cmd palette → "Export Scene (JSON)" | `editor.export.json` | `command-palette/editor-commands.tsx:329–346` | `scene_YYYY-MM-DD.json` |
|
||||
| Settings/palette → "Export GLB/STL/OBJ" | `export-manager.tsx` | `editor/export-manager.tsx:71–78` | 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:218–239`
|
||||
- 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:242–271`)
|
||||
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 ≈ 1–2 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** (1–2 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, 2–3 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** (1–3 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: **S–M**. 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 2–4:** 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 0–30 — "Unblock the loop"
|
||||
|
||||
- **A (1–2d):** 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 31–60 — "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 61–90 — "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 1–2)
|
||||
|
||||
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 2–3)
|
||||
|
||||
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 3–4)
|
||||
|
||||
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 4–5)
|
||||
|
||||
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 5–6)
|
||||
|
||||
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 A–F 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: ~10–14 weeks minimum to GA, ~4–5 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: ~4–5 weeks** (Phases A–D at MVP depth).
|
||||
- **Public beta: ~8 weeks** (add Phase E, quota enforcement, version UI, one round of pen-test fixes).
|
||||
- **GA: ~12–14 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 ~2–3 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 2–4 weeks.
|
||||
- If legal requires SOC 2 or EU data residency before launch, add 2–3 months.
|
||||
- Any real-time multi-agent requirement in v1 moves GA out by 4–6 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 (R1–R10) 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: 1–2 days. No new deps. No breaking changes.**
|
||||
|
||||
### Weeks 2–4 — 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 1–2 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)
|
||||
|
||||
**~4–5 weeks to private beta. ~10–14 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 2–4, 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 1–2 day host feature
|
||||
- [R8 — Integration design options](./R8-mcp-integration-design.md) — A→B→D phased recommendation
|
||||
- [R9 — Production readiness](./R9-production-readiness.md) — 4–5 weeks to beta, 10–14 to GA
|
||||
- [R10 — Ideas and edge cases](./R10-ideas-and-edges.md) — 300+ lines; top 10 ranked by value×feasibility
|
||||
Reference in New Issue
Block a user