Files

12 KiB

feat(mcp): add @pascal-app/mcp — Model Context Protocol server

Summary

Introduces a new workspace package @pascal-app/mcp (v0.1.0) that exposes the Pascal scene graph (@pascal-app/core) as MCP tools, resources, and prompts so any MCP-compatible AI host — Claude Desktop, Claude Code, Codex CLI, Cursor, or a custom agent — can read, mutate, save, and reopen Pascal projects programmatically with full Zod validation, atomic patches, undo-safe mutations, multimodal image inputs, and local SQLite persistence.

The branch is now local-first: scenes persist to ~/.pascal/data/pascal.db through SQLite, using bun:sqlite in the MCP CLI and node:sqlite when the Next.js editor server imports the storage package. The earlier Supabase adapter, SQL migrations, and committed test-reports/ artifacts have been removed.

Motivation

Issue #74 "Viewer component API definition" opens the question of how external consumers should drive Pascal. The viewer answers "embed in a React app." This PR answers the complementary case: drive Pascal from anything, without a browser — AI agents, CLI scripts, background services, or IDE plugins.

What's in the box

Tool inventory

Tool Purpose
get_scene Return full scene JSON
get_node Fetch one node by ID
describe_node Human summary: ancestry, children, properties
find_nodes Filter by type / parentId / levelId / zoneId
measure Distance between two nodes; area if zone
apply_patch Atomic multi-op (create / update / delete). All-or-nothing
create_level Create a level under a building
create_wall Create a wall on a level with 2D endpoints
place_item Place an item on a wall / ceiling / site
cut_opening Cut a door or window into a wall at t ∈ [0,1]
set_zone Create a zone polygon on a level
duplicate_level Deep-clone a level subtree with new IDs
delete_node Delete a node (with optional cascade)
undo / redo Drive Zundo temporal store
export_json Serialize scene to JSON (pretty or compact)
export_glb Stub (not_implemented — renderer required)
validate_scene Zod-validate every node
check_collisions Item placement conflicts per level
analyze_floorplan_image (Vision/sampling) Extract structured floor plan
analyze_room_photo (Vision/sampling) Extract room dimensions + fixtures
save_scene / load_scene / list_scenes / rename_scene / delete_scene Persist scenes in local SQLite
list_templates / create_from_template Seed scenes from bundled templates
generate_variants Fork and mutate scene variants
photo_to_scene Vision sampling to scene graph, optionally saved

Resources

URI MIME Purpose
pascal://scene/current application/json Full scene
pascal://scene/current/summary text/markdown Counts, areas, bbox
pascal://catalog/items application/json Item catalog (unavailable headless)
pascal://constraints/{levelId} application/json Slabs + wall footprints

Prompts

Prompt Args
from_brief brief, constraints?
iterate_on_feedback feedback
renovation_from_photos currentPhotos, referencePhotos, goals

Architecture

┌─── MCP host (Claude Desktop / Code / Cursor / custom) ───┐
│                          ▲                                │
│         stdio │ HTTP                                      │
│                          ▼                                │
│   ┌──────── packages/mcp/src/bin/pascal-mcp.ts ────────┐  │
│   │ (Bun CLI, loads node-shims first)                  │  │
│   └────────────────────────────────────────────────────┘  │
│                          │                                │
│                          ▼                                │
│   ┌──── createPascalMcpServer({ bridge }) ────┐            │
│   │  registerTools()                          │            │
│   │  registerVisionTools()                    │            │
│   │  registerResources()                      │            │
│   │  registerPrompts()                        │            │
│   └────────────────────────────────────────────┘           │
│                          │                                 │
│                          ▼                                 │
│   ┌─────────────── SceneOperations ─────────────────────┐  │
│   │  shared MCP / REST operation boundary               │  │
│   │  wraps SceneBridge + local SQLite SceneStore        │  │
│   │  Zod validation at every boundary                   │  │
│   └──────────────────────────────────────────────────────┘  │
│                          │                                 │
│                          ▼                                 │
│            @pascal-app/core (unchanged, new subpath exports) │
└──────────────────────────────────────────────────────────┘

How to test locally

# From the repo root
bun install
bun run --cwd packages/core build
bun run --cwd packages/mcp build

# Unit + integration tests (248 tests across 40 files)
bun test --cwd packages/mcp

# End-to-end smoke test (spawns stdio server and exercises 4 tools)
bun run --cwd packages/mcp smoke

# Biome lint
bunx biome check packages/mcp

# Turbo build
bunx turbo build --filter=@pascal-app/mcp

Try it with Claude Desktop, Claude Code, or Codex

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "pascal": {
      "command": "bun",
      "args": ["/absolute/path/to/editor/packages/mcp/dist/bin/pascal-mcp.js"],
      "env": {
        "PASCAL_DATA_DIR": "/Users/you/.pascal/data"
      }
    }
  }
}

For Codex CLI:

codex mcp add pascal-dev \
  --env PASCAL_DATA_DIR="$HOME/.pascal/data" \
  -- bun "$PWD/packages/mcp/dist/bin/pascal-mcp.js"

Run the editor with the same PASCAL_DATA_DIR, then ask the MCP host to create and save_scene; the scene is openable at /scene/<id>.

Known limitations

  1. GLB export is not implemented. Three.js is browser-only; headless GLB export would require a significant additional effort. export_glb returns a structured { status: 'not_implemented' } response.
  2. Vision tools require host sampling support. analyze_floorplan_image / analyze_room_photo defer the vision work to the host via MCP sampling. Hosts without sampling capability get a structured sampling_unavailable error. No vision model is bundled.
  3. Headless mode doesn't regenerate geometry. Wall mitering, slab triangulation, CSG cutouts, etc. run only in the browser renderer. MCP clients can manipulate node data freely, but derived geometry (mitered wall corners, cut-out walls with door/window holes) is recomputed only when a browser loads the scene via @pascal-app/viewer.
  4. loadAssetUrl/saveAsset are browser-only. Items with asset://<id> URLs can't be resolved in Node. Supply absolute URLs or data: URIs if you need them usable outside the browser.
  5. SiteNode.children inconsistency. Site's children hold full node objects while every other container holds ID strings (see CROSS_CUTTING.md §2). MCP works around this by traversing via the flat nodes dict. Upstream alignment proposed as a follow-up.
  6. Catalog unavailable in headless mode. pascal://catalog/items and place_item's catalog resolution fall back to a placeholder asset payload until the core exposes a Node-consumable catalog.
  7. HTTP/API exposure is guarded. MCP HTTP binds to 127.0.0.1 by default and requires PASCAL_MCP_HTTP_TOKEN/--auth-token before binding non-loopback hosts. The editor scene API allows tokenless loopback development, but non-loopback requests require PASCAL_SCENE_API_TOKEN; both paths include CORS handling and in-memory rate limiting.

Cross-cutting changes

Documented in packages/mcp/CROSS_CUTTING.md:

  1. packages/core/package.json — additive subpath exports. Adds ./schema, ./store, ./material-library, ./spatial-grid, ./wall. Needed because the main entry re-exports browser-only systems; subpath entries let Node consumers skip them. Zero impact on existing consumers (apps/editor, @pascal-app/viewer still use the main entry).
  2. .github/workflows/mcp-ci.yml — new CI. Kept because the repo otherwise only has manual release CI. It runs on PRs touching MCP/core/editor scene API code; installs with Bun 1.3.0, builds MCP, runs MCP tests, runs focused editor scene API tests, and biome-checks the touched surface.
  3. apps/editor scene routes. Adds scene API routes and pages that read from the same SQLite-backed SceneOperations layer as MCP.
  4. (Observation, not fixed) SiteNode.children inconsistency. Detailed in CROSS_CUTTING §2.

Checklist

  • bunx biome check packages/mcp — clean
  • bun run --cwd packages/mcp build — tsc OK
  • bunx turbo build --filter=@pascal-app/mcp — 2/2 tasks successful
  • bun test --cwd packages/mcp — 248/248 tests pass across 40 files (965 expects)
  • bun run --cwd packages/mcp smoke — spawns stdio server, registers 30 tools, exercises get_scene / create_level / validate_scene / undo end-to-end
  • bun test apps/editor/lib/scene-store-server.test.ts — editor store singleton test passes
  • Editor smoke — /api/scenes/<id> and /scene/<id> return 200 for a scene saved through MCP using the shared SQLite DB
  • Local Codex MCP probe with gpt-5.5 — saved a template scene through pascal-dev, then reloaded it and created a wall
  • Docs: README with Claude Desktop, Claude Code, Codex CLI, Cursor configs + tool/resource/prompt tables, CHANGELOG, 3 examples
  • Conventional commit series (9 commits on feat/mcp-server)
  • No Supabase dependency, SQL migrations, or committed test-report artifacts
  • packages/core changes are additive subpath exports plus URL-schema hardening
  • Bun CLI; RAF polyfill loads before any core import
  • Strict TypeScript (no any without reason; no @ts-expect-error); Zod at every boundary
  • Every mutation goes through the Zustand store (undo-safe via Zundo)

Commit series

feat(mcp): scaffold package and confirm headless bridge viability
feat(mcp): finalize scaffolding and factory entry
feat(mcp): add headless scene bridge with RAF polyfill
feat(mcp): implement 19 scene query and mutation tools
feat(mcp): add resources and prompts
feat(mcp): add multimodal vision tools via MCP sampling
feat(mcp): add stdio + streamable HTTP transports, CLI, and smoke test
docs(mcp): add README, examples, and changelog
chore(mcp): add CI workflow and document cross-cutting changes
feat(mcp,editor): add local SQLite scene persistence and editor scene routes
fix(mcp): remove Supabase backend and committed test reports

Follow-up (future PRs)

  • Align SiteNode.children to IDs-only (with setScene migration) — CROSS_CUTTING §2.
  • Extract shared operation/service layer so MCP, CLI, and future REST/OpenAPI adapters do not duplicate business validation.
  • Expose a Node-consumable item catalog from @pascal-app/core so place_item can resolve real catalog IDs.
  • Surface real spatial-grid collision detection (currently a simple AABB pass in check_collisions).
  • Post-build chmod +x dist/bin/pascal-mcp.js step so fresh installs get an executable bin without a manual chmod.
  • Consider a separate @pascal-app/systems package so @pascal-app/core can go data-only (breaking change, larger refactor).