Files
editor/packages/mcp/test-reports/research/R8-mcp-integration-design.md
T
Adrian PerezandClaude Opus 4.7 42bd05db9c docs(mcp): add 10-agent research on scene-save workflow
10 parallel research agents (R1-R10) investigated whether MCP can save
scenes as saveable entities that the user later opens in the editor —
rather than injecting via a dev-only window.__pascalScene hook.

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:47:36 +02:00

25 KiB
Raw Blame History

R8 — MCP ↔ Editor Integration Design

Vision. "Call MCP → scene is saved → I open the scene in the editor, no injection."

Today apps/editor/app/page.tsx runs a dev-only window.__pascalScene = useScene hack so an MCP running in the same browser can setScene(). That has to die. MCP lives in Node (its own SceneBridge over useScene), editor lives in the browser (its own instance of useScene). They never share memory. So "saving" means serializing a SceneGraph to a shared medium the editor can read.

Below are five concrete options, ranked, with a recommendation.


Option A — Filesystem handoff (~/.pascal/scenes/<slug>.json)

Description

MCP writes SceneGraph JSON to ~/.pascal/scenes/<slug>.json via a new save_scene tool. Next.js API route GET /api/scenes/[slug] reads the file from disk using node:fs. /scene/[slug] page fetches and calls applySceneGraphToEditor() on mount.

Architecture

Claude Desktop / Cursor
        │  stdio
        ▼
  pascal-mcp (Node)
        │
  SceneBridge.exportJSON()
        │
        ▼
  ~/.pascal/scenes/kitchen-v3.json     ◄── shared disk
        ▲
        │  fs.readFile (server-side)
  Next.js API route /api/scenes/[slug]
        ▲
        │  fetch()
  /scene/[slug] page  ──► applySceneGraphToEditor()

Pros

  • Zero new infra, zero network between MCP and editor
  • Works offline; trivial to debug (cat ~/.pascal/scenes/foo.json)
  • No auth, no RLS, no API contracts beyond "JSON on disk"
  • Editor's existing applySceneGraphToEditor() already accepts a SceneGraph; reusing packages/mcp/src/bridge/scene-bridge.ts exportJSON() is a one-liner
  • Ships this week

Cons

  • Same-machine only. Breaks the second the editor is deployed to Vercel
  • No multi-user, no sharing, no "open on phone"
  • Filesystem becomes the source of truth with no history, diffs, or transactions
  • Vercel/serverless deployment of the editor cannot read a local user directory (dead on arrival for production)

Dependencies

  • New save_scene / list_scenes tools in packages/mcp/src/tools/
  • New apps/editor/app/api/scenes/[slug]/route.ts
  • New apps/editor/app/scene/[slug]/page.tsx
  • No new npm packages. No breaking changes.

Effort: S (12 days)

Security / auth / multi-user

  • No auth (filesystem ACLs only). Anyone on the box can read the scenes
  • Path traversal risk on the slug — must sanitize (slugify, reject ..)
  • No multi-user story whatsoever

Production readiness: low (local dev only)

Valid as a transitional internal tool for solo use. Not shippable as the real product path. Use it as a stepping stone to B.

5-step v0.1 plan

  1. Add save_scene({ slug }) and list_scenes() tools that write to ~/.pascal/scenes/<slug>.json (Node fs/promises, slug sanitization, XDG_DATA_HOME fallback)
  2. Add load_scene({ slug }) tool that calls bridge.loadJSON(readFileSync(...))
  3. apps/editor/app/api/scenes/[slug]/route.ts — GET reads ~/.pascal/scenes/<slug>.json with path-traversal guard, returns JSON
  4. apps/editor/app/scene/[slug]/page.tsxuse client, fetches /api/scenes/[slug] on mount, calls applySceneGraphToEditor()
  5. Delete the window.__pascalScene injection hack from apps/editor/app/page.tsx

Description

MCP and editor both talk to a Supabase scenes table. MCP has a save_scene tool that upserts via the service role; editor's /scene/[id] page SSR-fetches the row with anon key and RLS. Existing env.mjs already declares POSTGRES_URL, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID. The rails are laid.

Architecture

  Claude / agent
       │
       ▼
  pascal-mcp (Node) ─────► Supabase REST (service role)
       │                   INSERT scenes { id, owner_id, graph_json, updated_at }
       │
       │ returns { sceneId, url: "https://.../scene/<id>" }
       ▼
  User pastes / clicks URL
       │
       ▼
  Next.js editor (Vercel)
       │  SSR with anon key + RLS
       ▼
  /scene/[id]/page.tsx  ──► applySceneGraphToEditor()
       │
       │  on save-in-editor
       ▼
  PATCH scenes  (owner_id == auth.uid)

Tables:

scenes (id uuid pk, owner_id uuid fk auth.users, slug text, title text,
        graph_json jsonb, updated_at timestamptz, created_at timestamptz)
scene_revisions (id uuid pk, scene_id fk, author_id fk, graph_json jsonb,
                 created_at timestamptz, author_kind text)  -- "mcp" | "editor"

RLS: owner_id = auth.uid() for select/update; public-read if public = true.

Pros

  • One-machine and cross-device works the same way
  • Multi-user, sharing via URL, versioning via scene_revisions — all free with Postgres
  • Auth already half-wired: BETTER_AUTH_SECRET + GOOGLE_CLIENT_ID in env.mjs suggest Better Auth planned
  • Editor can deploy to Vercel unchanged
  • graph_json is a jsonb column — indexable, queryable, diffable
  • Natural extension path to realtime (supabase.channel) and presence

Cons

  • Requires running Supabase (local via CLI, or hosted project)
  • MCP now has a network dependency; offline stops working unless you layer IndexedDB cache on the editor side
  • RLS policy mistakes are a classic data-leak vector
  • MCP needs an owner_id — how does a stdio MCP know who the user is? Need a device-pairing or API-key bootstrap

Dependencies

  • New @supabase/supabase-js dep in packages/mcp (dependency) and apps/editor (already probably pulling it or easy add)
  • Supabase project or local supabase CLI for dev
  • Migration SQL for scenes + scene_revisions
  • Small change to packages/mcp — env bootstrap, service-role key from ~/.pascal/config.json or PASCAL_SUPABASE_URL / PASCAL_SUPABASE_KEY env
  • No breaking changes to the SceneBridgeexportJSON() already produces exactly what we need

Effort: M (1 week for v0.1, 23 weeks to production-harden RLS and auth bootstrap)

Security / auth / multi-user

  • Full multi-user with RLS
  • MCP auth problem: solve with device-pairing — editor generates a short-lived token in UI ("paste this into Claude Desktop config"), MCP exchanges it for a long-lived machine token. Never put the service-role key in MCP; use per-user tokens
  • Supabase handles rate limiting, backups, Point-in-time recovery
  • Audit trail via scene_revisions

Production readiness: high

Right-sized for a v1 product. Same story as Figma/Linear/Notion — server of record, optional local cache.

5-step v0.1 plan

  1. Create supabase/migrations/001_scenes.sql with scenes and scene_revisions tables + RLS policies (owner read/write, public-slug read)
  2. Add packages/mcp/src/adapters/supabase.ts using @supabase/supabase-js, driven by PASCAL_SUPABASE_URL + PASCAL_SUPABASE_USER_TOKEN env (not service-role). Wrap in save_scene / load_scene / list_scenes tools
  3. Add apps/editor/lib/supabase.ts server client; add apps/editor/app/scene/[id]/page.tsx (server component) that fetches and passes graph_json to a client <SceneLoader> that runs applySceneGraphToEditor()
  4. Editor-side "save" handler: on Cmd+S / debounced dirty, UPDATE scenes SET graph_json, updated_at WHERE id=... AND owner_id=auth.uid(). Append a row to scene_revisions
  5. Add /settings/mcp route in editor that mints a device token and shows the JSON block the user pastes into claude_desktop_config.json (env: { PASCAL_SUPABASE_USER_TOKEN: "..." })

Option C — MCP is the backend (HTTP service)

Description

Run pascal-mcp permanently as an HTTP service (already supported via connectHttp in packages/mcp/src/transports/http.ts). Add non-MCP REST endpoints GET /scenes/:id, POST /scenes alongside the MCP Streamable HTTP endpoint. Editor treats the MCP host as its backend.

Architecture

  Claude / agent  ──MCP/Streamable HTTP──┐
                                          ▼
                                    pascal-mcp (Node HTTP :3917)
                                          │
                                          │  in-memory SceneBridge(s) + on-disk store
                                          │
  Next.js editor  ────GET /scenes/:id────►│
                  ◄───JSON {nodes,rootNodeIds}
                  ────POST /scenes───────►│

Pros

  • One process owns the scene graph; no dual-store problem
  • Offline-friendly if MCP runs on localhost
  • MCP already has HTTP transport; extending the Node http.createServer with a side route is ~30 lines
  • Can introspect the scene via the same bridge MCP tools use → perfect consistency

Cons

  • SceneBridge is a singleton — multi-user requires either a bridge-per-request or a separate process per user. Neither is trivial
  • Scaling story is poor: you'd have to build session isolation, persistence layer, auth, rate limits — you're rebuilding Supabase badly
  • Deploying MCP-as-backend to production means running Node long-lived; no more stdio simplicity
  • Ties the editor's backend lifecycle to whatever host is running MCP — if user closes Claude Desktop, the backend dies
  • The MCP protocol is for tool invocation, not CRUD; layering both on one port muddies concerns

Dependencies

  • Expand packages/mcp/src/transports/http.ts with non-MCP REST routes (or a second http.createServer)
  • Add a persistence adapter behind the SceneBridge (sqlite? jsonfile?)
  • Requires an auth story if it ever leaves localhost
  • No new external deps if sticking with node:http

Effort: M (localhost dev) / XL (multi-user production)

Security / auth / multi-user

  • Localhost: none needed; bind to 127.0.0.1
  • Multi-user: has to add bridge sessions, auth, CORS, TLS — effectively builds a toy Supabase
  • Exposing MCP HTTP publicly is a big liability (the MCP protocol itself has no native auth)

Production readiness: low for production, fine for local

Reasonable for a "single-developer laptop" loop. Do not ship this as the multi-tenant story.

5-step v0.1 plan

  1. Split connectHttp into connectMcpHttp (existing MCP route) and connectApiHttp (new REST). Share the same SceneBridge instance
  2. Add GET /api/scenes/:id, POST /api/scenes, GET /api/scenes backed by an on-disk map ~/.pascal/scenes/*.json (Option A persistence reused)
  3. Add CORS allowlist for http://localhost:3002 (editor dev port)
  4. In the editor, apps/editor/app/scene/[id]/page.tsx calls fetch('http://localhost:3917/api/scenes/[id]') client-side
  5. Wire a CLI flag pascal-mcp --serve --port 3917 --data-dir ~/.pascal/scenes that launches both transports

Option D — Editor-as-MCP-client (live subscription)

Description

Editor imports @modelcontextprotocol/sdk/client and connects to a long-running MCP server (the same pascal-mcp --http). It list_tools, call_tool('get_scene'), and subscribes to notifications/resources/updated for pascal://scene/current. Every change pushed from MCP triggers applySceneGraphToEditor().

Architecture

  Claude / agent ─(MCP stdio/HTTP)─► pascal-mcp  ◄─(MCP streamable HTTP)─ Editor
                                         │
                                      SceneBridge
                                         │
                                    single in-memory scene
                                         ▲
                    notifications/resources/updated  (push on change)

Pros

  • Real-time: agent edits a wall, editor redraws within an RTT
  • One source of truth (MCP process)
  • Reuses the MCP protocol on both sides — consistent model
  • Feels magical for demos

Cons

  • MCP's resource-update notification contract is still thin in v1; pascal://scene/current would need to be a subscribable resource. Our server doesn't currently implement notifications/resources/updated — meaningful work to add
  • MCP SDK was designed for tool hosts (Claude Desktop, etc.), not for browser long-running clients — running the MCP client in the browser via streamable HTTP is possible but fragile (CORS, SSE, browser-tab lifecycle)
  • SceneBridge still isn't multi-tenant — same singleton problem as C
  • MCP server outages = editor is broken. Tight coupling
  • Authentication for the browser → MCP HTTP transport is not a solved problem
  • Overkill if you just want "save and open"; this is real-time collab territory

Dependencies

  • @modelcontextprotocol/sdk/client in apps/editor (new dep in browser bundle — SDK is Node-first, bundle size TBD)
  • Add resource subscription support to packages/mcp/src/server.ts and scene-current.ts
  • CORS + auth for MCP HTTP

Effort: L (mostly in MCP server — subscriptions, auth, CORS; plus editor client integration)

Security / auth / multi-user

  • Same singleton problem as C
  • Browser → MCP exposes a new attack surface if public
  • Each user needs their own MCP process or their own SceneBridge session (requires server refactor)

Production readiness: low

Looks cool in a demo. Doesn't compose with Vercel-style deployments. Consider this a phase 3 add-on for real-time collab, layered on top of B.

5-step v0.1 plan

  1. Add server.sendResourceUpdated('pascal://scene/current') hooks inside SceneBridge mutation methods
  2. Implement resources/subscribe handler in packages/mcp/src/server.ts tracking per-transport subscriptions
  3. Add browser MCP client to apps/editor/lib/mcp-client.ts; wire auth token header
  4. apps/editor/app/scene/live/page.tsx — connects, calls get_scene, subscribes, re-applies on each notification
  5. Add a toggle in /settings/mcp for "live mode" — falls back to Option B polling when MCP host is unreachable

Option E — Local-first CRDT via Yjs/Automerge

Description

SceneGraph becomes a Y.Map. MCP writes operations into a Y.Doc; editor loads the same Y.Doc from IndexedDB (same-machine) or a y-websocket server (cross-device). Conflicts resolve automatically.

Architecture

  Claude / agent ──► pascal-mcp (Node) ──► Y.Doc ─┐
                                                  │  y-indexeddb / y-websocket
  Editor tab ─────────────────────────────► Y.Doc ┘
                                                  │
                                             Custom awareness/presence

Pros

  • Offline-first, collaborative, real-time — best-in-class UX
  • No server needed for same-machine (y-indexeddb); optional y-websocket for cross-device
  • Conflict-free merges: two agents + a human editing simultaneously Just Work
  • Proven architecture (Figma-ish, but open-source)

Cons

  • SceneGraph needs a full rewrite to become a CRDT-friendly structure. nodes: Record<id, AnyNode> + rootNodeIds: [] map cleanly to Y.Map<Y.Map> and Y.Array, but every AnyNode.parse() and every Zustand mutation in packages/core/src/store/actions/* is currently written against plain objects
  • Zundo's temporal middleware doesn't compose with Yjs's own undo manager — you'd pick one, and switching temporal layer affects every editor interaction
  • Large scenes: Y.Doc updates are fast but the schema migration + cross-host Zod revalidation is nontrivial
  • Doesn't solve auth/identity — you still need Better Auth or similar
  • Huge blast radius: this is touching packages/core at its heart

Dependencies

  • yjs, y-indexeddb, optionally y-websocket, y-protocols
  • Major rewrite of packages/core/src/store — bridging Zustand ⇄ Yjs
  • New MCP adapter packages/mcp/src/bridge/yjs-bridge.ts mirroring SceneBridge but against Y.Doc
  • Potentially replace Zundo with Y.UndoManager
  • Breaking changes throughout @pascal-app/core — every consumer affected

Effort: XL (13 months)

Security / auth / multi-user

  • Auth: still need Better Auth / Supabase Auth for identity; y-websocket needs an auth middleware
  • Multi-user: best-in-class once implemented
  • Server storage: y-websocket + Postgres persistence (e.g., y-postgresql) or S3 snapshots

Production readiness: high if you commit; trap if you don't

If you want Figma-quality multiplayer, this is the right answer long-term. But it is a complete rearchitecture of the core store. Not a starter move.

5-step v0.1 plan

  1. Prototype a Yjs binding for packages/core/src/store/use-scene.ts behind a feature flag; keep the plain Zustand path default
  2. Build a minimal Y.Doc ⇄ SceneGraph serializer and prove round-trip parity against existing scenes
  3. Write packages/mcp/src/bridge/yjs-bridge.ts that wraps Y.Doc in the same interface as SceneBridge
  4. Run a same-machine POC: MCP writes to Y.Doc → y-indexeddb → editor tab observes via Y.Map.observe
  5. Defer y-websocket to phase 2 after same-machine parity is proven

Option F (bonus) — Local daemon + shared SQLite

Description

pascal-mcp runs as a background daemon on localhost. Scenes persist into a single SQLite file (~/.pascal/pascal.db) via better-sqlite3. The editor SSR or a Next.js API route opens the same SQLite file read-only. Pure local, durable, queryable, zero network.

Architecture

  Claude ──► pascal-mcp (daemon) ──► SQLite (~/.pascal/pascal.db) ◄── Next.js API route

Pros vs A

  • Transactional; no torn writes
  • Indexable (JSON1 extension in SQLite); fast list/search
  • Natural version table (revisions table)
  • Still zero network, zero external infra

Cons

  • Two readers can deadlock on SQLite unless WAL mode + careful opens; Next.js dev server and MCP both holding the file needs care
  • Still same-machine only; doesn't deploy to Vercel

Effort: SM. Worth listing because it's a better A without much more work.


Ship Option A this week as a dev loop, commit to Option B as the product path, and keep Option D on the roadmap for a phase-3 "live mode". Concretely:

  1. Week 1: Option A (filesystem). Two days of work, ships the "no injection" promise for your own machine. Kills the window.__pascalScene hack. Unblocks every subsequent demo. Treat it as a local cache layer that survives option B — keep the ~/.pascal/scenes/ format stable so offline mode later just reads from it.

  2. Weeks 24: Option B (Supabase). This is the only option that:

    • Scales past your laptop
    • Composes with the existing env.mjs (Supabase, Better Auth already declared)
    • Supports multi-user without rearchitecting @pascal-app/core
    • Matches the deployment target (Next.js on Vercel)

    Supabase's jsonb + RLS + auth is a perfect fit for "store a scene, open it at /scene/<id>". The MCP side is ~200 lines: save_scene / load_scene / list_scenes tools calling supabase.from('scenes').upsert({...}).

  3. Quarter 2: Option D live mode as a pro feature. Layer real-time on top of B via Supabase Realtime (supabase.channel('scene:<id>').on('postgres_changes', ...)), not via MCP resource subscriptions — avoids the browser-as-MCP-client rabbit hole.

  4. Option E (CRDT) is only right if you commit to Figma-grade multiplayer. It's a 3-month project across @pascal-app/core and shouldn't be started until you have product-market signal that multiplayer is the moat.

  5. Option C is tempting as a "one process runs everything" story but rebuilds Supabase badly. Skip unless the product demands a fully-local air-gapped build.

Why B over C

C asks SceneBridge to become a multi-tenant database. SceneBridge is a thin wrapper over a Zustand singleton — making it multi-tenant is a rewrite. Supabase already is one.

Why B over E (for now)

E makes the editor collaborative, but the user's stated problem is "save → open", not "co-edit in real time". B solves the stated problem in 10% of the effort and leaves the door open for E later (you can replace the graph_json column with a y_doc column in a migration and nothing else in the app has to change, because applySceneGraphToEditor() still accepts a SceneGraph).


30/60/90 day roadmap

Day 030 — "Unblock the loop"

  • A (12d): filesystem handoff; MCP save_scene + editor /scene/[slug]. Delete window.__pascalScene.
  • B-alpha (2w): Supabase migrations for scenes + scene_revisions, RLS, MCP tools using user tokens, editor /scene/[id] server component.
  • Dogfood: the team uses save_scene from Claude Desktop every day. Bugs get filed.

Day 3160 — "Productionize"

  • Auth bootstrap: /settings/mcp device-pairing flow. Mints a scoped Supabase user JWT (not service-role) for MCP.
  • Editor → Supabase save: debounced on-change writes + "Save" button. Append scene_revisions rows with author_kind: 'editor'.
  • History UI: simple diff viewer across scene_revisions.
  • Sharing: public = true flag → public read. Share URL works.
  • Offline cache (optional): keep Option A filesystem writes as a redundant cache so scenes survive Supabase outages.

Day 6190 — "Live mode + polish"

  • Realtime via Supabase channels (not MCP D): editor subscribes to postgres_changes on the current scene and applies remote updates. Gives near-realtime collab without Yjs effort.
  • Presence: Supabase Realtime presence for "agent is editing".
  • Figma-style undo across actors: scoped per-user temporal history (use clearSceneHistory() when switching scenes; keep Zundo per tab).
  • Decide on E: if usage shows genuine concurrent-agent-plus-human friction, spike Yjs in Q2. Otherwise defer.

Risks + mitigations

Risk Severity Mitigation
MCP needs a user identity to write to Supabase with RLS High Device-pairing UI in editor that mints a short-lived JWT → long-lived machine token. Never ship the service-role key.
Concurrent MCP + editor writes clobber each other Medium Optimistic concurrency: include updated_at in UPDATE WHERE clause; if mismatch, append a revision instead of overwriting and surface a merge prompt.
Large graph_json payloads blow past Supabase's 8MB body limit Medium Measure casa-sol (test scene in test-reports/casa-sol/) and typical scene sizes. If >1MB, chunk or move to Storage buckets.
Serverless cold starts on /scene/[id] make "open" slow Low Use Next.js revalidate: 0 + ISR; Edge runtime where possible; cache in localStorage on client.
applySceneGraphToEditor is a client-only function, can't run on SSR Low Keep it client; <SceneLoader> component does useEffect(() => applySceneGraphToEditor(initialGraph), []).
Supabase outage breaks the whole editor Medium Layer Option A on top: editor falls back to last-known-good from localStorage when fetch fails.
RLS misconfiguration leaks scenes across users Critical Policy tests in supabase/tests/; add a dedicated "rls" CI step; anon client gets read-only and only public = true rows.
MCP Node process can't reach ~/.pascal on Windows Claude Desktop install Low Use envPaths('pascal') (via env-paths) to resolve per-OS.
Slug collisions / path traversal via save_scene({ slug: '../foo' }) High (for A) Strict slug regex ^[a-z0-9-]{1,64}$; reject everything else.
MCP writes a scene under user A; user B opens the URL → leak High (for B) Only share by signed URL or explicit public = true flag. Default visibility is owner-only.

Open questions for the user

  1. Are scenes per-user or per-project? Current projectId="local-editor" suggests projects exist — do MCP-created scenes live under a project, or are they free-floating until assigned?

  2. How does a stdio MCP know who the user is? Is the intended flow "user signs into editor → editor emits a device token → user pastes into Claude Desktop config"? Or "MCP is always anonymous and scenes live in a shared staging bucket"?

  3. Offline requirements? Must editing work with zero network (implies Option A cache on top of B), or is online-required acceptable for v1?

  4. Deployment target for the editor? Vercel (rules out C, F) or self-hosted (all options viable)?

  5. Scene mutability after save? Can the editor edit a scene MCP created and have those edits visible to the next MCP call? (Implies bidirectional sync, easiest via B's REST; harder via A's file racing.)

  6. Auth provider? env.mjs has BETTER_AUTH_SECRET + GOOGLE_CLIENT_ID — is Better Auth the plan, or Supabase Auth? They can coexist but one is source of truth for auth.uid().

  7. Versioning/history UX? Is "every MCP call appends a revision" desired, or should only explicit saves create revisions? Affects scene_revisions write patterns.

  8. Multi-agent story? If two Claude Desktops write to the same scene concurrently, which wins? (Punts on this until Yjs/E; in B, last-write-wins with optimistic concurrency is a fine v1.)

  9. How does "open in the editor" trigger? Does MCP return a URL and the user clicks? Or does MCP invoke a deeplink (pascal://scene/<id> handler) that focuses an already-open browser tab?

  10. Catalog availability in MCP. Today pascal://catalog/items returns catalog_unavailable in headless mode. If a scene references catalog items, does the editor need to re-hydrate them on open, or does MCP have to snapshot the catalog into the scene graph?