diff --git a/packages/mcp/test-reports/research/R1-persistence.md b/packages/mcp/test-reports/research/R1-persistence.md new file mode 100644 index 00000000..1cbc0a4b --- /dev/null +++ b/packages/mcp/test-reports/research/R1-persistence.md @@ -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:`. +- **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. diff --git a/packages/mcp/test-reports/research/R10-ideas-and-edges.md b/packages/mcp/test-reports/research/R10-ideas-and-edges.md new file mode 100644 index 00000000..5e00ff48 --- /dev/null +++ b/packages/mcp/test-reports/research/R10-ideas-and-edges.md @@ -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. diff --git a/packages/mcp/test-reports/research/R2-project-id.md b/packages/mcp/test-reports/research/R2-project-id.md new file mode 100644 index 00000000..4390d125 --- /dev/null +++ b/packages/mcp/test-reports/research/R2-project-id.md @@ -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. + +## `` 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` | `loadSceneFromLocalStorage()` | Fetch initial scene on mount, and when `onLoad` identity changes (scene switch) | +| `onSave` | `(scene: SceneGraph) => Promise` | `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 → → 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(null) + + 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. diff --git a/packages/mcp/test-reports/research/R3-scene-ui.md b/packages/mcp/test-reports/research/R3-scene-ui.md new file mode 100644 index 00000000..f0294188 --- /dev/null +++ b/packages/mcp/test-reports/research/R3-scene-ui.md @@ -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. diff --git a/packages/mcp/test-reports/research/R4-routing.md b/packages/mcp/test-reports/research/R4-routing.md new file mode 100644 index 00000000..e2209fae --- /dev/null +++ b/packages/mcp/test-reports/research/R4-routing.md @@ -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: ) +├── 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//…`. No such routes exist. Falls through to `undefined` gracefully today but signals a planned structure. + +## Options to add +| Option | Route | Effort | +|---|---|---| +| A | `/?sceneId=` — 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. diff --git a/packages/mcp/test-reports/research/R5-backend.md b/packages/mcp/test-reports/research/R5-backend.md new file mode 100644 index 00000000..4c784f6b --- /dev/null +++ b/packages/mcp/test-reports/research/R5-backend.md @@ -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. diff --git a/packages/mcp/test-reports/research/R6-file-io.md b/packages/mcp/test-reports/research/R6-file-io.md new file mode 100644 index 00000000..bdc0a801 --- /dev/null +++ b/packages/mcp/test-reports/research/R6-file-io.md @@ -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=` +- 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. diff --git a/packages/mcp/test-reports/research/R7-editor-api.md b/packages/mcp/test-reports/research/R7-editor-api.md new file mode 100644 index 00000000..393f011f --- /dev/null +++ b/packages/mcp/test-reports/research/R7-editor-api.md @@ -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) + +## `` host integration points + +| Prop | Signature | Trigger | Host opportunity | +|---|---|---|---| +| `onLoad` | `() => Promise` | Mount + `onLoad` identity change | Fetch scene by id from backend | +| `onSave` | `(scene) => Promise` | 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. diff --git a/packages/mcp/test-reports/research/R8-mcp-integration-design.md b/packages/mcp/test-reports/research/R8-mcp-integration-design.md new file mode 100644 index 00000000..8d1160a1 --- /dev/null +++ b/packages/mcp/test-reports/research/R8-mcp-integration-design.md @@ -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/.json`) + +### Description + +MCP writes `SceneGraph` JSON to `~/.pascal/scenes/.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/.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/.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/" } + ▼ + 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 `` 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` + `rootNodeIds: []` map cleanly to `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/`". 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:').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; `` 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/` 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? diff --git a/packages/mcp/test-reports/research/R9-production-readiness.md b/packages/mcp/test-reports/research/R9-production-readiness.md new file mode 100644 index 00000000..5c8ec41a --- /dev/null +++ b/packages/mcp/test-reports/research/R9-production-readiness.md @@ -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:`, 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: ` 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 `` / `` 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://; ...`). +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. diff --git a/packages/mcp/test-reports/research/SYNTHESIS.md b/packages/mcp/test-reports/research/SYNTHESIS.md new file mode 100644 index 00000000..237c6edc --- /dev/null +++ b/packages/mcp/test-reports/research/SYNTHESIS.md @@ -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 | `` 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/.json ──► Next.js API route ──► /scene/ 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 ``. +- 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//…` +- [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