* feat(core): stored storey heights groundwork — pure slab-support module + level height schema Extract pointInPolygon/computeWallSlabSupport and friends into a cycle-free packages/core/src/systems/slab/slab-support.ts (severs level-height -> spatial-grid-manager -> use-scene), add deriveLegacyLevelHeight as the pure mesh-free equivalent of the viewer's stacked level height, and add the optional LevelNode.height field plus the storey service (getStoredLevelHeight, getLevelElevations per-building prefix sums). No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): storey height badge + edit popover on level rows Each floating-level-selector row shows its storey height; clicking opens a popover with 2.5/3.0/3.5 presets and a free slider writing level.height. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core): vertical-model load migration — stored heights, ordinal compaction, wall-top classification Pass 3 in migrateNodes: derive and store each legacy level's exact stacked height (never snapped), compact ordinals per building anchored at zero so basements stay basements, classify wall tops against the derived plane (|plane - top| < 0.20 strictly -> plane-bound, else explicit height materialized), and drop the blind totalRise 2.5 stair default on legacy scenes only. Epsilon and strictness validated by a prod census. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: pin wall tops to the storey plane; stored heights become the only vertical truth Wall-top inversion: a wall without an explicit height now tops out at its storey plane (resolveWallTop); slabs lift only the base. Window/door caps resolve the real top through the same slab election instead of Infinity. All level stacking (viewer, elevator, first-person, stair openings, MCP scene queries) reads stored LevelNode.height; the four divergent live derivations and level.metadata.height are deleted. Stair totalRise becomes optional and derives from the storey height when absent. MCP create-level stops writing its elevation param into the ordinal. Level creation sites write explicit heights; templates carry their true derived heights. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: clamp slabs/ceilings under the storey plane; sweep wall-height fallbacks; wall Top control Slab elevation writes clamp to plane − MIN_WALL_HEIGHT when plane-bound walls elect the slab (pure clampSlabElevationForWalls + registry handle bounds + shared panels for 2D/3D parity); ceiling heights clamp under the plane and auto-ceilings derive from resolved wall tops. Every remaining wall.height ?? 2.5 fallback resolves through resolveWallTop / resolveWallEffectiveHeight (panels, overlays, measurements, quantities, spatial grid, MCP reports); template walls matching their storey become plane-bound. Wall panel gains a Top control (Follows storey / Custom height) derived purely from height presence; the store update path now deletes keys passed as explicit undefined so plane-binding round-trips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core): persisted support hosts — schema, host-preferring election, rendered-polygon unification Floor-placed nodes and walls gain a nullable supportSlabId. Elections surface the winning slab (getSlabSupportForItem, candidates query) and prefer a still-valid persisted host, falling back silently when the host is gone or reshaped away; deleting the host strips references in the same undo commit. Item-side support now tests the rendered slab polygon (like walls) through a per-level cache invalidated by the spatial-grid sync. Also adds the resolveStairTotalRise unit tests from the stage-1 gates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: persist support hosts at commit; thread wall host preference everywhere Floor-placed commits (draft pipeline, per-kind creation tools, registry move tool) and wall create/move/endpoint commits persist supportSlabId via shared resolveSupportSlabPatch helpers — only when overlapping supports disagree on elevation, clearing it otherwise or when the node leaves the floor. WallSlabSupport surfaces electedSlabId; every wall support read site passes wall.supportSlabId as the preferred host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: split slab into placement + thickness; pools become explicit recess intent slab.elevation stays the walking surface; new thickness grows downward so the solid occupies [elevation − thickness, elevation]. Migration writes thickness := elevation for solids (byte-identical intervals, including degenerate zero) and recessed: true for legacy negative pools. Geometry branches on recessed instead of the elevation sign; presets keep today's intervals; free elevation edits move the body without coupling thickness (the deck semantic). Dead viewer SlabSystem component deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: clamp ceilings to covering-slab undersides across levels getLevelAbove + getCoveringSlabUndersideAt give the first cross-level query; ceiling writes clamp to min(storey plane, lowest covering underside) − 0.01, and the space-detection reconcile now clamps manual ceilings down (never up) when a deck above intrudes — a flush deck reactively lowers the ceiling below it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: mezzanine and balcony build-tab tools One-gesture composites over the kernel: draw a deck footprint and commit deck slab + railings + stair (mezzanine) or deck + railings (balcony) in a single undo step. Fences gain supportSlabId and lift onto their host deck; railing runs split around the stair mouth; edges near wall centerlines are treated as closed. Stairs target the deck via explicit totalRise with no level-to-level opening sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: adaptive slab vertical editing; level vocabulary in UI copy Dragging a grounded slab's top stretches it (elevation and thickness move together — gaps impossible); floating decks move with thickness preserved and land grounded at zero; pools keep the drag-through-zero gesture. User-facing copy says level, not storey (Follows level, Level height). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: clamp plane-bound wall tops to covering-slab undersides; fix vertical reactivity getWallPlaneTop samples the wall span against the level above's slabs, so a thick or flush upper floor shortens the walls below it instead of colliding (automatic attach, no dialog). Level-height edits now dirty the level's walls, stairs, ceilings, and fences; covering-slab changes dirty the level below. The effective-height helper triplicated across editor overlays moved to core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: ceilings follow the level top by default ceiling.height becomes optional — absent means the ceiling tracks min(level top, covering slab underside) − 0.01 live, so level-height edits no longer require ceiling fixes. Ceiling panel gains the same Follows level / Custom height control as walls; auto-from-walls ceilings are created height-less and their height-derivation machinery is deleted; migration drops stored heights within 0.20 of the bound on legacy scenes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: wall plane clamp missed max-side boundary walls Auto slabs derive their polygon from wall centerlines, so covering-clamp samples sat exactly on the boundary where ray-cast point-in-polygon is side-dependent (min edges in, max edges out) — walls clamped or not by orientation. getWallPlaneTop now clips the wall's thickness band against the covering polygon (boundary-inclusive, arc-aware) and the ceiling bound's point sampling gained an explicit on-boundary test. Verified against the reported repro scene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: stairs attach to decks; pointer-decided placement surface Stairs gain deckSlabId — rise follows the deck's elevation live (straight flights re-converge via a write-sync mirroring auto-openings), the panel shows a unified To destination with Follows deck / Custom rise, and the mezzanine tool attaches instead of baking a stale rise. Item placement under an elevated deck no longer flickers: grid events fed a feedback loop (the grid plane rode the ghost's elected height), so the support election is now capped at the surface the pointer ray actually hits, with a ground sentinel keeping under-deck commits deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: remove the mezzanine/balcony composite tools Decks ship as catalog presets instead; the kernel the presets rely on (fence deck-hosting, stair deck attachment, clone remaps, pointer-decided placement) all stays. The tool code lives at e30042db for reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: gate wall adoption to grounded slabs; panel moves, drag stretches then unsticks Floating decks keep their drawn polygon (and stop being seam candidates for grounded neighbors) instead of growing into nearby walls. Panel elevation edits are pure placement; the viewport drag stretches a grounded slab up to 0.4 m then unsticks it into a thin deck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: deck-attached stairs land flush with the deck surface The rise now subtracts the stair's own elected base (same election the visual lift uses), so base + rise always equals the deck walking surface; the auto-sync defers a microtask so it reads a settled spatial grid and re-converges on both deck and base-slab moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: draw walls and fences on elevated decks Wall and fence draw tools now publish the pointed surface, so the draw plane rides the deck top (no more perspective-skewed floor hits) and previews sit on the deck. Fences gain real support election: a pure resolveFenceSupportSlabPatch persists the deck host at draw and reshape commits; wall commits thread the pointer cap so aiming under a deck elects the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: unit-aware height presets Level, ceiling, and slab preset buttons show clean values per display system (8/9/10 ft storeys, 8'-9' ceilings, whole-inch slab steps) instead of converted metric labels; metric presets unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: stairs converge to their resolved rise; deck attachment disables the cutout syncStairRises now converges every follows-mode straight stair (level or deck) plus deck-attached custom rises — detaching a stair from a deck re-derives its height, and ordinary stairs finally track level-height changes. Attaching via the panel writes slabOpeningMode none and hides the cutout controls; detaching restores the destination cutout and clears the stale explicit rise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: stacked-slab move hopping — one ray, one surface, one XZ The hop was hysteresis: the election consumed the riding grid plane's perspective-skewed hit, giving two self-consistent fixed points for one pointer ray. getPointedSupportSurface now returns the ray's crossing of the pointed surface and both the support cap and the cursor XZ derive from that single computation, so items stay on the surface the pointer aims at and sit exactly under the cursor across storeys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: single stair per click; tools restore select mode on exit The stair tool subscribed to both node clicks (synthesized on pointerup) and the native-click grid event with none of the guards sibling tools carry — one physical click over any node surface committed twice. A commit gate + follow-up click swallow fix the double dispatch, and the stair and column tools now restore select mode on exit instead of leaving the dead build-mode-without-tool state that ignored every click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: compact multi-selection panel with host footer slot Selecting multiple nodes now docks the collapsed-by-default panel on the right: N selected header, kind breakdown, and Duplicate/Delete mirroring the floating pill. A new multiSelectionFooter slot lets the host app dock actions below it, exactly like inspectorFooter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@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 Bun with no browser, WebGPU, React, or external database service. It 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
@pascal-app/core is a peer dependency; Bun workspaces resolve it automatically.
The MCP CLI is intended to run with Bun. When the storage package is consumed by
the Next.js editor server, it opens the same local database through Node's
built-in SQLite driver.
Quick start
Launch the server over stdio in one line:
bunx pascal-mcp
Load an initial scene from disk:
pascal-mcp --stdio --scene ./my-scene.json
Expose it over loopback HTTP:
pascal-mcp --http --port 8787
Binding a non-loopback host requires a bearer token:
PASCAL_MCP_HTTP_TOKEN="$(openssl rand -hex 32)" \
pascal-mcp --http --host 0.0.0.0 --port 8787 --cors-origin https://editor.example
Local scene storage
Scenes saved through MCP are stored in a local SQLite database:
~/.pascal/data/pascal.db
Set PASCAL_DATA_DIR when you want the MCP server and the running editor to
share a different directory, or PASCAL_DB_PATH when you need an exact database
file path. The store uses WAL mode and transactional version checks so separate
local processes can save and open the same scene database.
During workspace development, run both sides with the same data directory:
# Terminal 1: run the editor
PASCAL_DATA_DIR="$HOME/.pascal/data" bun run dev
# Terminal 2 or an MCP host: run the server
PASCAL_DATA_DIR="$HOME/.pascal/data" bun packages/mcp/dist/bin/pascal-mcp.js
Live editor updates
When the editor and MCP server share the same PASCAL_DATA_DIR, MCP mutations
against a loaded saved scene are persisted to SQLite and recorded in a local
scene_events stream. The editor page subscribes to that stream at
/api/scenes/:id/events with server-sent events, so an open browser tab can
apply scene graph snapshots as the agent edits the scene.
The flow is intentionally local and lightweight:
- Open or create a scene in the editor so it is saved in the local database.
- Load that scene through MCP with
load_scene. - Run MCP mutation tools such as
create_room,add_door,furnish_room,create_wall,place_item, orset_zone.
Each mutation version-checks the saved scene before writing. If the browser or
another MCP process saved a newer version first, the MCP tool returns
live_sync_version_conflict; reload the scene with load_scene before
continuing.
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"],
"env": {
"PASCAL_DATA_DIR": "/Users/you/.pascal/data"
}
}
}
}
If bunx is not on your PATH, point command at the absolute path to bun
and pass the built dist/bin/pascal-mcp.js file as the first arg.
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"],
"env": {
"PASCAL_DATA_DIR": "/Users/you/.pascal/data"
}
}
}
}
For local workspace testing before publish, build first and point Claude Code at the built binary:
{
"mcpServers": {
"pascal": {
"command": "bun",
"args": ["/absolute/path/to/editor/packages/mcp/dist/bin/pascal-mcp.js"],
"env": {
"PASCAL_DATA_DIR": "/Users/you/.pascal/data"
}
}
}
}
Codex CLI config
Via the CLI:
codex mcp add pascal --env PASCAL_DATA_DIR="$HOME/.pascal/data" -- bunx pascal-mcp
For local workspace testing before publish:
bun run --cwd packages/mcp build
codex mcp add pascal-dev \
--env PASCAL_DATA_DIR="$HOME/.pascal/data" \
-- bun "$PWD/packages/mcp/dist/bin/pascal-mcp.js"
This writes an entry like this to ~/.codex/config.toml:
[mcp_servers.pascal-dev]
command = "bun"
args = ["/absolute/path/to/editor/packages/mcp/dist/bin/pascal-mcp.js"]
[mcp_servers.pascal-dev.env]
PASCAL_DATA_DIR = "/Users/you/.pascal/data"
Cursor config
In Cursor settings (settings.json):
{
"mcp.servers": {
"pascal": {
"command": "bunx",
"args": ["pascal-mcp"],
"env": {
"PASCAL_DATA_DIR": "/Users/you/.pascal/data"
}
}
}
}
Programmatic use
Embed the server in your own Bun 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.
Coordinate conventions
Pascal is a right-handed scene where X and Z form the ground plane and Y
is up. Lengths are in metres; rotations are radians, stored as Euler
[x, y, z] tuples.
Plan → world. Every 2-D point you pass is a level/building-local
ground-plane coordinate
[x, z] — this includes wall.start / wall.end and the polygon / holes
arrays of slab, zone, and ceiling. With the default identity building
transform, it appears in world space as:
[x, z] → (x, y, z) // the 2nd component is world Z (depth), not "up"
There is no sign flip in the stored convention: tooling consumes the second
component as world Z directly. The vertical y starts from the owning level's
stacked height as computed by the level system from accumulated level heights,
plus the element's own height; slabs additionally carry an absolute
elevation.
Heads-up when you compute coordinates outside the editor. Pascal's
viewports apply their own rotations on top of the world axes: the 2-D plan
panel rotates its content by the user's view rotation (north-aligned = 0°,
FLOORPLAN_VIEW_ROTATION_DEG baseline, north = world −Z), and
the 3-D "top-down" snap preserves the camera's current azimuth, so when invoked
from the iso default position, world and screen axes are offset by ~45° until
you orbit to an axis-aligned view. So a layout authored as if
"Y = north, viewed top-down" — common in land surveys, north-up site plans,
and 2-D plotting libraries — will arrive rotated relative to its source
when viewed in Pascal (and possibly further reflected, depending on which
viewport and camera state you're in). The editor's own 2-D and 3-D tools are
internally consistent with their stored coordinates, so this only affects
geometry authored programmatically. To verify orientation before trusting
externally-computed coordinates, place a scaled guide image at known anchor
points and check alignment; apply whatever rotation (or reflection) your
authoring side needs to match.
A worked demonstration of all of this — axis-aligned baseline, the rotated
30° example below, and a paired "page-intent vs world-result" L for the
external-coordinate gotcha — lives in
examples/coordinate-conventions-demo.md
and examples/coordinate-conventions-demo.json.
Load the JSON with
pascal-mcp --stdio --scene examples/coordinate-conventions-demo.json.
Example — a 6 × 4 m slab rotated 30° about its first corner (coordinates rounded to 3 dp; sides ≈ 6 m / 4 m; not axis-aligned, so the mapping is actually exercised):
{
"op": "create",
"parentId": "<levelId>",
"node": {
"type": "slab",
"elevation": 0.0,
"polygon": [[0, 0], [5.196, 3.0], [3.196, 6.464], [-2.0, 3.464]]
}
}
This lands flat on the ground (Y = 0), about 6 m along a heading 30° off the +X axis and 4 m along its perpendicular — i.e. occupying world (x, z) directly.
One separate gotcha: wall-attached coordinates are wall-local, not plan
coordinates. Stored door/window position[0], and place_item position[0]
when the target is a wall, are metres along the wall; wall-attached rotations
are wall-local too.
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[] } |
list_levels |
List levels with ids, floor indices, parent ids and child counts. | — | { activeSceneId, levels[] } |
get_level_summary |
Compact summary of one level with counts, wall/opening lists, zones, slabs, ceilings and items. | { levelId? } |
{ levelId, counts, walls, zones, items, slabs, ceilings } |
get_walls |
Walls on a level with length and child doors/windows. | { levelId? } |
{ levelId, walls[] } |
get_zones |
Room/zone polygons with approximate areas and bounds. | { levelId? } |
{ levelId, zones[] } |
measure |
Distance between two nodes; area when applicable. | { fromId, toId } |
{ distanceMeters, areaSqMeters?, units: 'meters' } |
search_assets |
Search the built-in MCP item catalog. | { query, category? } |
{ results, total } |
create_story_shell |
Create one level-owned story shell from a footprint: perimeter walls plus optional slab and ceiling. Use once per story. | { levelId, footprint, wallHeight?, wallThickness?, createSlab?, createCeiling? } |
{ wallIds, slabId, ceilingId, createdIds } |
create_stair_between_levels |
Create a straight stair and one rectangular manual opening in the destination slab/source ceiling, with auto-opening disabled. | { fromLevelId, toLevelId, position, width?, runLength?, totalRise? } |
{ stairId, stairSegmentId, openingPolygon } |
create_roof |
Create a roof container and one roof segment. By default creates a dedicated roof level above the reference occupied level for solo/exploded views. | { levelId, width, depth, roofType?, roofHeight?, roofLevelId?, useDedicatedRoofLevel? } |
{ roofLevelId, createdRoofLevelId, roofId, roofSegmentId } |
create_room |
Create a zone, slab, ceiling, and walls from a polygon. | { levelId, name, polygon, color?, wallHeight?, wallThickness? } |
{ zoneId, slabId, ceilingId, wallIds, areaSqMeters } |
add_door |
Add a door to a wall using parametric placement. | { wallId, t, width?, height?, hingesSide?, swingDirection? } |
{ doorId, localX } |
add_window |
Add a window to a wall using parametric placement and sill height. | { wallId, t, width?, height?, sillHeight? } |
{ windowId, localX, sillHeight } |
furnish_room |
Place realistic furniture for a room type inside a polygon. | { levelId, roomType, polygon, doorWallIndex? } |
{ placed, itemIds, skipped } |
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 level/slab/zone, ceiling, wall, or site. Slab/zone targets resolve to the parent level so floor items render and validate. | { catalogItemId, targetNodeId, position, rotation? } |
{ itemId, status } |
cut_opening |
Cut a door or window opening into a wall. position is 0..1 along the wall and is stored as wall-local meters. |
{ 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 }[] } |
verify_scene |
High-level layout check with validation status, per-level counts, empty levels and practical issues. | — | { valid, levels[], issues, hasIssues } |
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://agent/guide |
text/markdown |
MCP-first construction workflow, scene invariants, and tool preferences for agents. |
pascal://catalog/items |
application/json |
Dependency-free built-in catalog subset for common residential furniture and fixtures. |
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_glbreturnsnot_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.
- The built-in MCP catalog is intentionally small. Host applications can expose their own richer catalog through additional tools/resources without requiring the MCP package to depend on the editor UI bundle.
- 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/viewerin a browser host. - Core's
loadAssetUrl/saveAssetare browser-only; items that referenceasset://<id>URLs aren't resolvable in Node. Supply absolute URLs ordata:URLs for item assets if you need them usable outside the browser. dirtyNodesaccumulates in headless mode because no renderer consumes it. Callbridge.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