Files
editor/packages/mcp
Adrian PerezandClaude Opus 4.7 f230d9a401 test(mcp): Villa Azul + 10-agent deep verification
Builds a larger, richer house than Casa del Sol via MCP save_scene
(no injection hack), then dispatches 10 parallel verifiers across
schema, geometry, dimensions, openings, HTTP, page render,
parentage, round-trip, spatial, visual.

Villa Azul — 56 nodes, validate_scene=true, 44KB on disk:
- 15x10m building envelope (vs Casa del Sol's 12x8)
- 9 interior zones (master bed/bath, bed 2/3, shared bath,
  living/dining, kitchen, entry hall, corridor)
- 10 doors + 12 windows (all cut successfully)
- 4 exterior zones (pool 8x4 + basin slab at -2m, outdoor kitchen,
  driveway, back patio)
- 5 rail-style fences (vs Casa del Sol's privacy) with 2m entrance gap

Verification: 108 checks, 104 PASS, 4 findings:
- V1 schema: 56/56
- V2 geometry: 7/7 (perimeter closes, interior T-junctions, no
  zone overlaps, fence gap verified)
- V3 dimensions: 13/13 zone areas exact (1 spec mismatch on site
  polygon default, not a build bug)
- V4 openings: 22/22 dimensional fit, surfaced a tool gap in
  cut_opening (no adjacency check) + my build packed too tightly
- V5 HTTP: 10/10 (GET/PUT/PATCH/DELETE/HEAD, If-Match conflicts)
- V6 page: 14/14 (/scene/:id 81KB, /scenes 20KB, 404 fallback)
- V7 parentage: surfaced CROSS_CUTTING §2 site->building->level
  parentId=null (pre-existing in core's loadScene)
- V8 round-trip: 10/10 byte-equal, duplicate_level -> 110 nodes
- V9 spatial: 12/12 (find_nodes, measure, constraints resource)
- V10 visual: HTML fallback (Chrome extension disconnected during
  run); API layer intact

Follow-up tracked: `cut_opening` should check opening-adjacency on
the same wall (minimum gap) to catch tight packing during patch
construction. Currently returns success and relies on the UI to
visualise the overlap.

Live at http://localhost:3002/scene/a6e7919eacbe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:39:08 +02:00
..

@pascal-app/mcp

Model Context Protocol server for the Pascal 3D editor. Drives the @pascal-app/core scene graph from any MCP-compatible AI host.

The server runs headlessly in Node — no browser, no WebGPU, no React — and exposes the same scene mutations used by the editor UI (create walls, place items, cut openings, undo, etc.) as MCP tools, resources, and prompts.

Install

bun add @pascal-app/mcp       # or: npm i @pascal-app/mcp

@pascal-app/core is a peer dependency; Bun workspaces resolve it automatically.

Quick start

Launch the server over stdio in one line:

bunx pascal-mcp           # or: npx pascal-mcp

Load an initial scene from disk:

pascal-mcp --stdio --scene ./my-scene.json

Expose it as HTTP for remote hosts:

pascal-mcp --http --port 8787

Claude Desktop config

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "pascal": {
      "command": "bunx",
      "args": ["pascal-mcp"]
    }
  }
}

If bunx isn't on your PATH, substitute npx or point command at the absolute path of the pascal-mcp binary inside your project.

Claude Code config

Via the CLI:

claude mcp add pascal bunx pascal-mcp

Or add to .mcp.json at the repo root:

{
  "mcpServers": {
    "pascal": {
      "command": "bunx",
      "args": ["pascal-mcp"]
    }
  }
}

Cursor config

In Cursor settings (settings.json):

{
  "mcp.servers": {
    "pascal": {
      "command": "bunx",
      "args": ["pascal-mcp"]
    }
  }
}

Programmatic use

Embed the server in your own Node process using the in-memory transport. The example below runs a full client/server pair inside a single script — useful for agent frameworks and tests.

import { createPascalMcpServer, SceneBridge } from '@pascal-app/mcp'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'

const bridge = new SceneBridge()
bridge.loadDefault()
const server = createPascalMcpServer({ bridge })

const [srvT, cliT] = InMemoryTransport.createLinkedPair()
const client = new Client({ name: 'my-agent', version: '0.1.0' })
await Promise.all([server.connect(srvT), client.connect(cliT)])

const tools = await client.listTools()
console.log('available tools:', tools.tools.map((t) => t.name))

const scene = await client.callTool({ name: 'get_scene', arguments: {} })
console.log(scene)

See examples/embed-in-agent.ts for a compilable version.

Tools

All tools validate their inputs and outputs with Zod. Mutation tools are captured by Zundo's temporal middleware as a single undoable step.

Name Purpose Key input Output
get_scene Return the full scene graph. { nodes, rootNodeIds, collections }
get_node Fetch a node by id. { id } the node, or InvalidParams if not found
describe_node Node summary with ancestry, children count and properties. { id } { id, type, parentId, ancestry[], childrenCount, properties, description }
find_nodes Filter nodes by type / parent / zone / level. { type?, parentId?, zoneId?, levelId? } { nodes: AnyNode[] }
measure Distance between two nodes; area when applicable. { fromId, toId } { distanceMeters, areaSqMeters?, units: 'meters' }
apply_patch Batched create/update/delete/move, validated and dry-run before commit. { patches: Patch[] } { applied: number }
create_level Add a new level to a building. { buildingId, elevation, height, label? } { levelId }
create_wall Add a wall to a level. { levelId, start, end, thickness?, height? } { wallId }
place_item Place a catalog item on a slab, ceiling, or wall with placement validation. { catalogItemId, targetNodeId, position, rotation? } { itemId } or { error: 'invalid_placement', reason }
cut_opening Cut a door or window opening into a wall. { wallId, type: 'door' | 'window', position, width, height } { openingId }
set_zone Create a zone/room polygon on a level. { levelId, polygon, label, properties? } { zoneId }
duplicate_level Clone a level and all of its descendants. { levelId } { newLevelId, newNodeIds[] }
delete_node Delete a node; cascades when cascade: true. { id, cascade? } { deletedIds: [] }
undo Step back through temporal history. { steps? } { undone: number }
redo Step forward through temporal history. { steps? } { redone: number }
export_json Serialize the scene graph as JSON. { pretty? } { json: string }
export_glb Stubbed: GLB export requires the browser renderer. throws not_implemented
validate_scene Zod-validate every node and parent-child integrity. { valid, errors: { nodeId, path, message }[] }
check_collisions Find overlapping items and out-of-bounds placements. { levelId? } { collisions: { aId, bId, kind }[] }
analyze_floorplan_image Vision tool: extract walls, rooms, and approximate dimensions from a floorplan image. { image, scaleHint? } { walls, rooms, approximateDimensions, confidence }
analyze_room_photo Vision tool: extract approximate dimensions and fixtures from a room photo. { image } { approximateDimensions, identifiedFixtures, identifiedWindows }

The vision tools require the MCP host to support the sampling capability (createMessage). Hosts that don't will see a structured sampling_unavailable error.

Resources

URI MIME Purpose
pascal://scene/current application/json Full { nodes, rootNodeIds, collections } snapshot.
pascal://scene/current/summary text/markdown Human-readable summary with node counts, bounding box, and level areas.
pascal://catalog/items application/json Item catalog; returns { status: 'catalog_unavailable', items: [] } in headless mode if no catalog is provided.
pascal://constraints/{levelId} application/json Slab footprints and wall polygons for the given level — useful as planner context.

Prompts

Name Args Purpose
from_brief { brief: string, constraints?: string } Guided workflow for turning a prose brief (e.g. "2-bed apartment in 80 m²") into an incremental sequence of apply_patch calls starting from an empty site.
iterate_on_feedback { feedback: string } Minimal-diff instructions: examine the current scene, then propose the smallest patch set that satisfies the feedback.
renovation_from_photos { currentPhotos: string[], referencePhotos: string[], goals: string } Chains the vision tools with the scene mutation tools to produce a renovation plan grounded in photos.

Limitations

  • export_glb returns not_implemented. GLB export depends on the Three.js renderer and isn't reachable headlessly without a large additional effort.
  • Vision tools require MCP host sampling support. Claude Desktop supports this; some MCP clients don't.
  • Systems (wall mitering, slab triangulation, CSG cutouts, roof / stair generation) run inside React hooks in the editor. Headless mode doesn't regenerate derived geometry — but all node data remains fully manipulable. Consumers that need rendered geometry run @pascal-app/viewer in a browser host.
  • Core's loadAssetUrl / saveAsset are browser-only; items that reference asset://<id> URLs aren't resolvable in Node. Supply absolute URLs or data: URLs for item assets if you need them usable outside the browser.
  • dirtyNodes accumulates in headless mode because no renderer consumes it. Call bridge.flushDirty() if observability matters to your consumer.

Development

bun install
bun run --cwd packages/mcp build
bun test

Smoke-test the stdio binary end-to-end:

bun run --cwd packages/mcp smoke

License

MIT