@pascal-app/mcp 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -10
- package/dist/bin/pascal-mcp.js +2 -0
- package/dist/bridge/agent-session.d.ts +2 -1
- package/dist/bridge/agent-session.d.ts.map +1 -1
- package/dist/bridge/agent-session.js +0 -1
- package/dist/bridge/scene-bridge.d.ts +21 -10
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +178 -194
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/safe-fetch.d.ts +6 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -1
- package/dist/lib/safe-fetch.js +135 -41
- package/dist/operations/scene-operations.d.ts +7 -0
- package/dist/operations/scene-operations.d.ts.map +1 -1
- package/dist/operations/scene-operations.js +4 -0
- package/dist/prompts/from-brief.js +1 -1
- package/dist/prompts/iterate-on-feedback.js +1 -1
- package/dist/prompts/renovation-from-photos.js +1 -1
- package/dist/prompts/scene-guidance.js +2 -2
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +8 -3
- package/dist/resources/catalog-items.d.ts +4 -1
- package/dist/resources/catalog-items.d.ts.map +1 -1
- package/dist/resources/catalog-items.js +14 -6
- package/dist/server.d.ts +9 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -5
- package/dist/templates/empty-studio.d.ts.map +1 -1
- package/dist/templates/empty-studio.js +2 -3
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +2 -3
- package/dist/templates/two-bedroom.d.ts.map +1 -1
- package/dist/templates/two-bedroom.js +2 -3
- package/dist/tools/add-column.d.ts.map +1 -1
- package/dist/tools/add-column.js +9 -6
- package/dist/tools/add-object.d.ts +44 -5
- package/dist/tools/add-object.d.ts.map +1 -1
- package/dist/tools/add-object.js +75 -21
- package/dist/tools/asset-catalog.d.ts +5 -6
- package/dist/tools/asset-catalog.d.ts.map +1 -1
- package/dist/tools/asset-catalog.js +1 -9
- package/dist/tools/author-scene.d.ts +8 -5
- package/dist/tools/author-scene.d.ts.map +1 -1
- package/dist/tools/author-scene.js +302 -51
- package/dist/tools/clear-scene.js +2 -2
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +8 -5
- package/dist/tools/honest-patch-guard.js +6 -5
- package/dist/tools/hosted-services.d.ts.map +1 -1
- package/dist/tools/hosted-services.js +2 -2
- package/dist/tools/index.d.ts +7 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -9
- package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
- package/dist/tools/local-geometry-scripts.js +7 -2
- package/dist/tools/patch-guards.d.ts.map +1 -1
- package/dist/tools/patch-guards.js +24 -14
- package/dist/tools/project-files.d.ts.map +1 -1
- package/dist/tools/project-files.js +2 -2
- package/dist/tools/room-tools.d.ts +1 -0
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +11 -8
- package/dist/tools/scene-scope.d.ts +4 -0
- package/dist/tools/scene-scope.d.ts.map +1 -0
- package/dist/tools/scene-scope.js +4 -0
- package/dist/tools/shared-tools.d.ts +0 -2
- package/dist/tools/shared-tools.d.ts.map +1 -1
- package/dist/tools/shared-tools.js +146 -31
- package/dist/tools/templates/create-house-from-brief.js +1 -1
- package/dist/tools/view-scene.js +2 -2
- package/examples/coordinate-conventions-demo.md +1 -0
- package/examples/photo-to-scene.md +1 -0
- package/examples/renovate-from-photos.md +2 -0
- package/package.json +10 -10
- package/dist/tools/create-unit.d.ts +0 -29
- package/dist/tools/create-unit.d.ts.map +0 -1
- package/dist/tools/create-unit.js +0 -36
- package/dist/tools/list-units.d.ts +0 -38
- package/dist/tools/list-units.d.ts.map +0 -1
- package/dist/tools/list-units.js +0 -61
- package/dist/tools/set-unit-members.d.ts +0 -22
- package/dist/tools/set-unit-members.d.ts.map +0 -1
- package/dist/tools/set-unit-members.js +0 -43
- package/dist/tools/unit-members.d.ts +0 -4
- package/dist/tools/unit-members.d.ts.map +0 -1
- package/dist/tools/unit-members.js +0 -24
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @pascal-app/mcp
|
|
2
2
|
|
|
3
|
+
Also listed on [Smithery](https://smithery.ai/servers/pascalapp/editor).
|
|
4
|
+
|
|
3
5
|
Model Context Protocol server for the Pascal 3D editor. Drives the
|
|
4
6
|
`@pascal-app/core` scene graph from any MCP-compatible AI host.
|
|
5
7
|
|
|
@@ -8,7 +10,10 @@ Cursor, and OpenClaw, read [Connect an AI agent](https://editor.pascal.app/docs/
|
|
|
8
10
|
The hosted endpoint edits projects in a Pascal account; this package is the
|
|
9
11
|
open-source, local server for custom hosts and local scene storage.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Listed in the official MCP Registry as
|
|
14
|
+
[`app.pascal/editor`](https://registry.modelcontextprotocol.io/v0/servers/app.pascal%2Feditor/versions/latest).
|
|
15
|
+
|
|
16
|
+
The server runs headlessly in Node.js (22.23.4+ on 22.x, 24.22.1+ on 24.x, or 26.12.1+) or Bun, with no browser,
|
|
12
17
|
WebGPU, React, or external database service. It exposes the same scene mutations used
|
|
13
18
|
by the editor UI (create walls, place items, cut openings, undo, etc.) as MCP tools,
|
|
14
19
|
resources, and prompts.
|
|
@@ -113,6 +118,7 @@ against a loaded saved scene are persisted to SQLite and recorded in a local
|
|
|
113
118
|
apply scene graph snapshots as the agent edits the scene.
|
|
114
119
|
|
|
115
120
|
The flow is intentionally local and lightweight:
|
|
121
|
+
|
|
116
122
|
1. Open or create a scene in the editor so it is saved in the local database.
|
|
117
123
|
2. Load that scene through MCP with `load_scene`.
|
|
118
124
|
3. Run MCP mutation tools such as `create_room`, `add_door`, `furnish_room`,
|
|
@@ -234,6 +240,9 @@ import { createPascalMcpServer, SceneBridge } from '@pascal-app/mcp'
|
|
|
234
240
|
import { Client, InMemoryTransport } from '@modelcontextprotocol/client'
|
|
235
241
|
|
|
236
242
|
const bridge = new SceneBridge()
|
|
243
|
+
// Or over a detached scene a host opened (`@pascal-app/core/runtime`): every tool then reads and
|
|
244
|
+
// writes that scene, with its ids, and the host turns it into a patch.
|
|
245
|
+
// const bridge = new SceneBridge({ runtime: openDetachedScene(snapshot, options) })
|
|
237
246
|
bridge.loadDefault()
|
|
238
247
|
const server = createPascalMcpServer({ bridge })
|
|
239
248
|
|
|
@@ -248,6 +257,13 @@ const scene = await client.callTool({ name: 'get_scene', arguments: {} })
|
|
|
248
257
|
console.log(scene)
|
|
249
258
|
```
|
|
250
259
|
|
|
260
|
+
A host that passes its own `operations` (`createPascalMcpServer({ operations })`) may implement
|
|
261
|
+
`runInScene(run)`: every tool handler starts inside it, and tools re-enter it after their awaits,
|
|
262
|
+
so the ids a tool mints and the operations it plans run in the scene's synchronous scope. It is
|
|
263
|
+
optional; without it handlers run as they always have, with random ids. An implementation that
|
|
264
|
+
computes on a detached scene must provide it, delegating to `SceneBridge.runInScene`, or its tools'
|
|
265
|
+
ids will not be the scene's and a recomputed effect will not match.
|
|
266
|
+
|
|
251
267
|
See [`examples/embed-in-agent.ts`](./examples/embed-in-agent.ts) for a
|
|
252
268
|
compilable version.
|
|
253
269
|
|
|
@@ -392,14 +408,14 @@ is a "pitch" fraction like `"1/4 pitch"`, which is rise over span.
|
|
|
392
408
|
| `find_by_type` | Find everything of one type: nodes of a kind (any core or plugin kind), items of a category, lights, and typed parts inside authored objects, windows and doors; narrowed by level, parent, zone (on its level, by plan point, holes left out) or import source id (`provenance.refs[].id` or legacy `metadata.sourceIds`, exact or prefix); without a type, every node the filters match. Each result is the node's id, name, kind, level and parent, with its metadata and provenance (one over 1 KB named in `omitted`). | `{ type?, levelId?, parentId?, zoneId?, sourceId?, sourceIdPrefix?, limit? }` | `{ type?, count, nodes, parts, results[], truncated?, hint? }`, at most `limit` (200) results listed; refusals `unknown_type`, `level_not_found`, `node_not_found`, `zone_not_found`, `not_a_zone` |
|
|
393
409
|
| `list_levels` | List every building's levels in floor order with their role (storey, roof-only, support), their standing against the ground line (`gradeStanding`, `isGroundLevel`) and, per building, the level at grade. | — | `{ activeSceneId, activeLevelId, levelCount, occupiedStoryCount, supportLevelCount, roofLevelIds, groundLevels[], levels[] }` |
|
|
394
410
|
| `get_level_summary` | Compact summary of one level: role, counts, walls with openings, zones with areas, slabs, ceilings, items, stairs, roofs, and everything else by type. Omit `levelId` for the lowest storey. | `{ levelId?, level? }` | `{ levelId, role, counts, walls, zones, slabs, ceilings, items, stairs, roofs, other }` |
|
|
395
|
-
| `get_walls` | Walls on a level with length, stored and resolved height, and child doors/windows.
|
|
396
|
-
| `get_zones` | Room/zone polygons with holes, areas (holes taken out), bounds and floor choices.
|
|
411
|
+
| `get_walls` | Walls on a level with length, stored and resolved height, and child doors/windows. Each wall reports type `{ id, label }` or null; wallTypes lists available preset and project types. wallType reads standard (when absent), railing or curtain. | `{ levelId?, level? }` | `{ levelId, walls[] }` |
|
|
412
|
+
| `get_zones` | Room/zone polygons with holes, areas (holes taken out), bounds and floor choices. Reports roomType (room/porch/deck/patio), floor.drop in metres, porchFraming (auto, postSize, postSpacing in metres, beamSize, postCount, beamCount; null with no intent), and decking (boardSize, gap in metres, direction), matching set_zone_intent. | `{ levelId?, level? }` | `{ levelId, zones[] }` |
|
|
397
413
|
| `get_site` | The site a building stands on, in its level coordinates (a lot with no building yet, in site coordinates): the lot and its area with the footprint a house usually covers (30 to 45%), the buildable area inside the setbacks with its centre, area, each edge's frontage and depth and its front edge, the street side with the lot's frontage and depth, north, the ground (sloped, read flat or not read), address, parcel and zoning, whether the lot is the new-project placeholder, and the footprint's fit and yards. | `{ building?, site? }` | `{ siteId, buildingId, pose, lot, placeholder, buildable, street, ground, northDeg, address, parcel, zone, footprint, others, summary }`; refusals `no_site`, `building_required`, `site_not_found`, `not_a_site`, `building_not_on_site` |
|
|
398
414
|
| `place_building` | Stand a building on its site by moving the building node only (walls keep their level coordinates): its footprint, the outline about to be drawn or the walls standing, centred in the buildable area or at the yards given; a house standing keeps its rotation unless `rotationDeg` is given, one with no walls yet is squared to the street. | `{ building?, outline?, yards?: { front?, rear?, left?, right? }, rotationDeg? }` | `{ buildingId, position, rotationDeg, moved, footprint, yards, fit, overhang, movedWithIt?, message }`; refusals `no_site`, `needs_footprint`, `other_building_on_site`, `no_street`, `yards_conflict`, `yards_infeasible`, `building_required` |
|
|
399
415
|
| `measure` | Distance between two nodes' world-space reference points (hosted doors, windows and items resolved through their host; every node kind); area when applicable. | `{ fromId, toId }` | `{ distanceMeters, fromPoint?, toPoint?, areaSqMeters?, units: 'meters' }` |
|
|
400
|
-
| `view_scene` | Look at the finished building, every storey and the roof, from a viewpoint the agent picks (a compass side, a side as seen from the house's street, a street-height eye, a photo's camera, square-on, from above), in the scene's own light or a photo's, and get the picture back, with the photo's crop beside it when a region is given. `interior: { zoneId }` stands inside a room at eye height (1.6 m by default), a step in from a corner and looking across it; `interior: { levelId }` looks inside one floor: the other floors and its roofs and ceilings left out, everything on it (walls, openings, furniture) cut at 0.65 of the storey's height, for the picture only. Pictures are 1920 × 1200 unless `maxPx` (64 to 1920) asks for a smaller longest side. The server has no renderer: the host's `sceneViews` renders it
|
|
401
|
-
| `search_assets` | Search the host's item library (the built-in list on a standalone server), several queries in one call; a query matches name, id, category or tags.
|
|
402
|
-
| `design_brief` | The request read as a brief, once
|
|
416
|
+
| `view_scene` | Look at the finished building, every storey and the roof, from a viewpoint the agent picks (a compass side, a side as seen from the house's street, a street-height eye, a photo's camera, square-on, from above), in the scene's own light or a photo's, and get the picture back, with the photo's crop beside it when a region is given. `interior: { zoneId }` stands inside a room at eye height (1.6 m by default), a step in from a corner and looking across it; `interior: { levelId }` looks inside one floor: the other floors and its roofs and ceilings left out, everything on it (walls, openings, furniture) cut at 0.65 of the storey's height, for the picture only. Pictures are 1920 × 1200 unless `maxPx` (64 to 1920) asks for a smaller longest side. The server has no renderer: the host's `sceneViews` renders it, through an editor tab open on the project or a renderer of the host's own. Read-only. | `{ target?, interior?, from?, elevation?, eyeHeight?, position?, fov?, projection?, camera?, lighting?, maxPx?, photo? }` | image content plus `{ status, camera, size, tab, capturedAt }`; refusals `view_unavailable`, `no_project`, `which_building` (a street side, no target, buildings on several sites), `no_current_camera` (`camera: 'current'`: the server has no view of the person's), `camera_and_viewpoint` (a camera with a viewpoint of the agent's own), `photo_crop_unavailable`, `interior_level_invalid`, `interior_hidden`, `interior_transform_unsupported`, `interior_target_elsewhere`; on the hosted server `editor_tab_required`, `editor_tab_hidden`, `editor_tab_busy` (a tab open but busy or still loading, no link: wait a few seconds and call again, do not ask the user), `editor_tab_timeout` (a tab claimed the view and then stopped answering); what the refusal says about opening the editor follows the person's "Agents may open the editor for me" setting (ask first, allow, never) |
|
|
417
|
+
| `search_assets` | Search the host's item library (the built-in list on a standalone server), several queries in one call; a query matches name, id, category or tags. Each query lists its best matches first, at most `limit` (24 by default, up to 60); a cut query says how to narrow it, one that finds nothing gets a hint to build the item with `add_object`. The text result carries each item's id, name and placement facts; the structured result adds its category. | `{ queries: [{ query, category? }], limit? }` | `{ groups: [{ query, total, truncated, results[] }], total, hint? }`; refusal `no_catalog` |
|
|
418
|
+
| `design_brief` | The request read as a brief, once: `axes` (the request read axis by axis: `action`, `size`, `form`, `facade`, `pieces`, `openings`, `style`, `materials`, `dressing`, each bounded — the person's own words as `quote`, the reading in a short line, `pieces` as a `minimum` or `exact` — or open with a design statement of at least three words; an absent axis is refused `axis_missing`, except `form` and `facade`, which are optional, a quote not in the person's message in the chat `axis_quote_not_in_request`, an open axis without its design `axis_open_without_design`; an open `form` states its `massing`, an open `facade` its `treatment`, an open form owes `rooms`; over MCP an axis quote is required and not compared), the tenth axis `source` (`open`, `catalog`, `brand` with its `brand`, `existing`), a `vision` for what is open, the dressing of the rooms layer by layer (`beyond`: textiles, light, windows, walls, floor, plants, objects, small furniture, each pieces or `{ none: why }`; an open `dressing` axis owes 6 of the 8, `beyond_too_thin`, a bounded one owes none, and the named pieces read as a minimum do not bound it, `dressing_bounded_by_minimum`; handed back as `dressing`), the person's words that set a source other than `open` (`sourceQuote`, required, naming the brand, the catalog or what is already there; not compared here) and, with an open source, technical equipment and plants placed from the catalog by its tags (furniture is made). Furnishing follows a brief: `place_items`, `furnish_room`, `place_in_room` (the catalog's) and `add_object`, `place_design` (made), inside `run_batch` too, are refused before one with `design_brief_required`.`search_assets` and `view_assets` stay open too. Source `open` opens the made pieces, places technical equipment and plants from the catalog and keeps `furnish_room` closed (`brief_source_open`); `catalog`, `brand` and `existing` open the catalog's tools, and creation for what it lacks. A batch checks each call under its own `onRefusal` policy. The brief is the session's: a scene load or clear starts another, and the next `design_brief` replaces it. Changes nothing in the scene. |
|
|
403
419
|
| `view_assets` | Pictures of 1 to 6 catalog items by id, each labelled with its size, category and the names of its paintable surfaces (its model's `slot_<id>` materials; `paint` takes one as `role`), returned as image content. The picture and surfaces are read before the call (a hosted server fetches and resizes the thumbnail to at most 512 px; a standalone server's built-in list has no host to fetch from, so it answers sizes and says it has no picture). Opt-in: nothing changes when it is not called. | `{ ids: string[] (1 to 6) }` | `{ items: [{ id, name, category, dimensions, surfaces, picture, unavailable? }], shown, note? }` plus one labelled image per item shown; refusals `unknown_asset`, `no_catalog` |
|
|
404
420
|
| `search_materials` | Search or browse the material library, several queries in one call: words match as the library writes them (timber is wood, render is stucco), shades and surfaces; a query without words lists a kind, a surface or the whole library by name. A search lists 12 at a time and a browse 150, a long one saying how many more (`more`) and the offset that lists them (`next`). Reads Pascal's library and the materials the host registered (the editor registers its library; this server registers none). Read-only. | `{ queries: [{ query?, kind?, surface?, offset? }] }` | `{ groups: [{ query?, kind?, surface?, offset?, results: [{ ref, label, kind, surfaces, color? }], missing?, more?, next? }] }` |
|
|
405
421
|
| `paint` | Give walls, slabs, ceilings, roofs and every other part with surfaces (columns, fences, doors, windows, stairs, blocks, cabinets, shelves…) a finish in one call, as the editor's Paint does: a colour (the nearest library colour), a `library:` or `scene:` ref, a native material kept as a scene material (reused when an equal one exists), or erase. `role` picks the surface (a wall's `exterior`/`interior` faces, by its rooms or else its stored sides, refused when rooms stand on only part of a face, a slot by name, a roof's `top`/`edge`/`wall`, or its `fascia`, the fascia and rake boards, held by every segment; a glazed roof's `glazing-frame`, `glazing-glass` or `glazing-solid`, and without a role its frame and solid panels, the glass kept); a roof shows one finish per surface, so a segment's finish goes on its roof and a roof's clears its segments' own; a wall face is painted whole, over its room's wall finish, its painted parts keeping theirs (named in `note`). | `{ targets[], role?, color?, material?, materialName?, erase? }` (one finish) | `{ ok, painted[], finish, materialId?, material?, createdMaterial?, note?, achieved }`; refusals `target_not_found`, `unknown_role`, `side_unknown`, `unknown_material`, `materials_unavailable`, `not_paintable` |
|
|
@@ -410,27 +426,32 @@ is a "pitch" fraction like `"1/4 pitch"`, which is rise over span.
|
|
|
410
426
|
| `create_roof` | Cap the top storey with a roof over its footprint (its walls' outline, else its floor plates, else its rooms), one segment per rectangle, or over a rectangle of your own (`width`, `depth`, `center`). The roof goes on the building's roof level above the storey (reused, the empty level directly above when it is the only one, else made and named Roof) and stands on the storey's wall tops; the hand roof tool follows the same rule. `roofType: "barrel"` arches each segment (`depth` is the span, `barrelRise` at most half of it, `barrelSegments` facets from 4 to 32); `roofSystem: "glazed"` builds a gable, straight shed or barrel roof as panes and framing, set by `glazing` (see [glazed roofs](../../wiki/glazed-roofs.md)). | `{ levelId? (or level), roofType?, roofSystem? ('standard' \| 'glazed'), glazing?, barrelRise?, barrelSegments?, pitch? (degrees, or rise over run: "6:12"), overhang? (the eaves' reach past the walls in plan), wallHeight?, wallThickness?, width?, depth?, center?, materialPreset?, fasciaHeight?, fasciaMaterialPreset?, name? }` | `{ roofId, roofLevelId, createdRoofLevelId, coveredLevelId, segmentIds, roofType, achieved }`; refusals `storey_above`, `roof_exists`, `building_required` (several buildings, no level named), `storey_without_building`, `conflicting_level`, `no_levels`, `no_rooms`, `rectangle_incomplete`, `unknown_material` (the roof's or the fascia's finish) |
|
|
411
427
|
| `move_roofs_to_roof_level` | Move the roofs a storey holds on its own walls to the level the roof rule gives them (the building's roof level over the top storey, made when there is none; the storey above a lower one), as a roof's panel does by hand. Each roof stands where it stood, segments, finishes and id kept. For projects built before the roof level, when `verify_scene` reports `roof_mixed_in_storey`. | `{ levelId }` | `{ roofIds, roofLevelId, createdRoofLevelId, achieved }`; refusals `level_not_found`, `no_roof_on_walls` |
|
|
412
428
|
| `create_room` | Create a room from a polygon: one wall per edge (an edge a wall already runs along reuses it) and the room zone; its doors and windows can be declared in the same call by edge (`wallIndex`) and `t`, each placed with `add_door`'s and `add_window`'s rules or listed in `skippedOpenings`. The floor plate and ceiling are derived, never authored. `outdoor: true` makes a terrace: separators (the editor's Separator: a room boundary with no wall) where no wall runs, no walls of its own, no ceiling. Where every edge of the polygon already runs along a wall (nothing to build) and the polygon fits the room those walls make, the call names that room instead of stacking a second zone on it, and `zoneId` is that room's. If that room has a name of its own (not a number the editor gave it: any name but the editor's "Room N", whoever gave it) the call is refused (`room_named_by_person`, worded with the room's name): pass `rename: true` **only when the person asked for the rename; otherwise ask them**. A room is never renamed unasked, and the same name over it says there is nothing to create. A terrace (`outdoor: true`) drawn over a fully walled courtyard names it outdoor. The refusal is agent-only: the editor's own room tool behaves as before (a person sees and names rooms directly). | `{ name, polygon, levelId?, color?, wallHeight?, wallThickness?, outdoor?, rename?, doors?, windows? }` | `{ zoneId, slabId, ceilingId, wallIds, reusedWalls, areaSqMeters, doorIds, windowIds, skippedOpenings? }`; refusals `cannot_enclose`, `outdoor_room_overlap`, `room_named_by_person` |
|
|
413
|
-
| `add_door` | Add a door to a straight wall at `t` (0..1 along it) with the editor's placement rules, or a passage with no leaf (`openingKind: "opening"`); it slides to stay on the wall and reports `clamped`.`doorType`, `style` and the outline (`openingShape`) match a reference, `code` builds what they cannot,
|
|
429
|
+
| `add_door` | Add a door to a straight wall at `t` (0..1 along it) with the editor's placement rules, or a passage with no leaf (`openingKind: "opening"`); it slides to stay on the wall and reports `clamped`.`doorType`, `style` and the outline (`openingShape`) match a reference, `code` builds what they cannot, `nodeId` rebuilds a scripted door, and the rare `sillHeight` lifts it off the floor or, negative, lowers it to a floor sunk beside the wall (down to the wall's bottom). | `{ wallId, t?, width?, height?, sillHeight?, openingKind?, hingesSide?, swingDirection?, doorType?, style?, openingShape?, code?, nodeId?, force? }` | `{ doorId, wallId, localX, t, clamped, sillHeight?, achieved }`; refusals `wall_not_found`, `not_a_wall`, `curved_wall`, `wall_too_short`, `opening_overlap` (unless `force`) |
|
|
414
430
|
| `add_window` | Add a window as `add_door` places a door, centred vertically on the wall unless `sillHeight` (its bottom edge above the floor) is given; `windowType`, `columns` / `rows`, `style` (a Fixed window's panes; refused on another type, `style_needs_fixed_window`) and the outline match a reference. | `{ wallId, t?, width?, height?, sillHeight?, windowType?, columns?, rows?, style?, openingShape?, code?, nodeId?, force? }` | `{ windowId, wallId, localX, t, clamped, sillHeight, achieved }`; the same refusals as `add_door` |
|
|
415
431
|
| `furnish_room` | Furnish a room for its type from the host's item catalog: the main piece against the wall facing the door, the rest along a side wall. Nothing lands in a door's clear zone or on another item: a piece that does not fit is nudged, else skipped, and `skipped` says why. | `{ zoneId?, polygon?, levelId?, roomType, doorWallIndex? }` | `{ placed, itemIds, skipped, doorWallIndex, doorsDetected }`; refusals `room_required`, `roof_level`, `level_not_found`, `no_catalog` |
|
|
416
432
|
| `add_corner_window` | A window wrapping the corner where two walls meet: one window per wall, both running to the corner and joined, the glass fused (a fixed window) or at a thin post (the other window types, whose frames stay), at any angle from 10° to 170° (a square corner, a bay's 135°, an acute one). `corner` is the point the walls of `levelId` end at (default the floor in view: stacked storeys share their corners' points), `[x, z]` in the level's plan as `get_walls` gives their ends; `width` is how far the glass runs along each wall from the corner (default 1.2 m) and `widths` overrides it per wall id; `post` is `'none'` (the glass fused, a fixed window's default) or `'post'` (the other types' default and only choice); `height` defaults to 1.5 m and the window is centred on the wall unless `sillHeight` (its bottom edge above the floor) is given; `wallIds` (two) is needed only when more than two walls end at the corner; `windowType` and `style` are `add_window`'s (a style needs a Fixed window). By hand: select a window that ends at a corner and use the window panel's Corner section (Wrap the corner, This side and Other side, Fused glass or Corner post for a fixed window, Unwrap). | `{ corner: [x, z], levelId? (or level), wallIds?, width?, widths?, height?, sillHeight?, post?, windowType?, style? }` | `{ windowIds, wallIds, angle, post, achieved }`; refusals `no_corner` (no two walls end there), `corner_ambiguous` (more than two do: pass `wallIds`), `corner_angle` (under 10° or over 170°: use `add_window`), `corner_not_fused` (`post: 'none'` on a type other than fixed), `width_exceeds_wall`, `style_needs_fixed_window` |
|
|
417
433
|
| `merge_windows` | Make two windows that exist one window, as the editor's Merge windows button does; nothing is recreated. On two walls that meet at a corner, each window within 0.30 m of it, they become one corner window (the `add_corner_window` result): each is slid to the corner keeping its width, the glass fused (a fixed window's; other types meet at a post), and both take the first window's height, sill and type, so name first the window whose look you want; the corner is refused over 170° or under 10°. On one wall, two windows with at most 0.30 m between them and nothing between become one window spanning both (overlapping or one inside the other, their union), from the lower sill to the higher head, with the first window's type and style. Use it for two windows already built that open a corner or one wide opening (`verify_scene`'s `corner_unjoined` names a pair for the first); `add_corner_window` builds a new pair. By hand: select two windows and use Merge windows in the multi-selection panel, or place or drag a window near a corner where one waits and click or release (Alt places it plain). | `{ windowIds: [a, b] }` | `{ kind: 'corner' \| 'same_wall', windowIds, removedIds, achieved }`; refusals `window_not_found`, `not_a_window`, `same_window`, `window_not_on_wall`, `window_already_joined` (unwrap it first), `windows_already_joined`, `windows_not_adjacent`, `opening_between`, `walls_not_meeting`, `corner_angle`, `window_not_at_corner`, `height_exceeds_wall`, `opening_overlap` |
|
|
418
434
|
| `apply_patch` | Batched create/update/delete/move, validated and dry-run before commit. Batch-first is the default: send all create/update/delete ops for a build step in one atomic call (stable order, later ops may reference earlier created ids); do not loop one-op calls. A create with an id already in the scene is refused (`node_exists`); delete it earlier in the same patch to replace it. Updates cannot change `id` or `type` (`identity_change`), `object` or `children` (`immutable_field`), move a node under a missing or childless parent (`invalid_parent`), or add schema issues (`invalid_update`); default gutters and downspouts a delete regenerates are addressable only in a later call (`regenerated_default`). Refusals are tool errors with JSON text `{ code, patchIndex, id, message }`. A level created without a `height`, or an update that clears it, gets the height the editor's load derives from what the level holds (an empty level: 2.5 m), one line per level in `notes`. A roof the patch makes or moves onto the storey whose walls carry it is seated on the level above those walls, one line per roof in `notes`; a new storey at or above the roof level moves it up. Refused: storey content on a roof level (`roof_level`), declaring a level that holds storey content the roof level (`roof_level_holds_storey`), floor indexes that put the roof level under a storey (`roof_level_on_top`). | `{ patches: Patch[] }` | `{ applied: number, notes?: string[] }` |
|
|
419
435
|
| `add_level` | Add an empty level to a building, as the editor does: on top of its storeys, under its roof level (which moves up one), or below the lowest for a basement (every existing floor, the ground floor included, keeps its place). Omit `buildingId` for the scene's only building. | `{ buildingId?, position?, name?, height? }` | `{ levelId, buildingId, floorIndex, height, shiftedLevelIds? }`; refusals `building_required`, `building_not_found`, `no_building` |
|
|
420
|
-
| `set_ground_level` | Make a level the building's ground floor, as the level panel's "Set as ground floor": the building names it, is seated so that floor stands
|
|
421
|
-
| `add_wall` | Add a wall to a level, straight or, with `curveOffset`, an arc. Omit `levelId` for the lowest storey.
|
|
436
|
+
| `set_ground_level` | Make a level the building's ground floor, as the level panel's "Set as ground floor": the building names it, is seated so that floor stands on grade (height only, the plan stays), and the floor indexes are renumbered so it is 0 and the levels under it are the basements. | `{ levelId }` | `{ levelId, buildingId, floorElevation, gradeElevation, movedBuildingBy, renumbered, unchanged? }`; refusals `level_not_found`, `not_a_level`, `no_building`, `not_a_storey` |
|
|
437
|
+
| `add_wall` | Add a wall to a level, straight or, with `curveOffset`, an arc. Omit `levelId` for the lowest storey. wallType railing builds a guard (default 0.9144 m high); railing sets style, posts, infill and cap; postSize sets thickness. | `{ start, end, levelId?, thickness?, height?, curveOffset?, type?, wallType?, railing? }` | `{ wallId, levelId, length }`; refusals `roof_level`, `wall_too_short`, `level_not_found`, `unknown_wall_type` |
|
|
438
|
+
| `set_wall_type` | Apply a preset or project wall assembly to existing standard walls; copies layers and total thickness. null clears the assembly. | `{ wallIds, type }` | `{ ok, wallIds, type }`; refusals `unknown_wall_type`, `target_not_found`, `curtain_wall`, `railing_wall` |
|
|
439
|
+
| `add_column` | Add or rebuild a column. role post uses the Post tile: square pressure-treated wood, postSize 4x4, 6x6 (default), 8x8; postBase none (default), post-base (steel), pier (concrete). | `{ x?, z?, level?, role?, postSize?, postBase?, height?, nodeId?, ... }` | `{ columnId, nodeId, height, width, depth }`; `not_a_post` for post choices on an ordinary column |
|
|
422
440
|
| `add_fence` | Add a fence along points on a level, one fence per side; `closed` runs it back to the first point (a lot's boundary). Privacy panels unless `style` says otherwise. | `{ points, closed?, height?, style?, baseStyle?, materialPreset?, levelId? }` | `{ count, fenceIds, levelId, length, achieved }`; refusals `fence_too_short`, `roof_level`, `unknown_material`, `level_not_found` |
|
|
423
441
|
| `place_items` | Place catalog items in one call, each placed or refused on its own, at level `(x, z)`: on the level's floor, or on the host each names (`targetNodeId`): a wall (`y`, the height of its bottom, required), a ceiling, or an item standing on the floor. A floor item in a door's clearance is refused `blocks_door` (naming the door and a spot that clears every door), one its room cannot hold in any turn `too_large_for_room`. | `{ items: [{ assetId, x, z, y?, targetNodeId?, rotation? }], levelId? }` | `{ levelId, items: [{ ok, itemId?, hostId?, restingOn?, code? }] }`; per-item refusals `asset_not_found`, `outside_rooms`, `blocks_door`, `too_large_for_room`, `height_required`, `item_too_tall`, `unsupported_host`, `host_not_found`, `host_not_on_level` |
|
|
424
442
|
| `place_design` | Create one design (procedural item recipe, object or JSON string) that passes `validate_design`. Its mounting picks the host: level/slab/zone or a design surface (`surfaceId`), a straight wall face, or a ceiling. Create-only, one undo step, inline designs up to 24 KiB, with coded refusals. | `{ design, hostId, position, rotation?, side?, surfaceId?, parameters?, slots?, name?, id? }` | `{ designId, parentId, surfaceId }` |
|
|
425
443
|
| `set_zone` | Create a zone/room polygon on a level; over walls that already enclose a room, it is that room, never a second zone: one still named "Room N", or already named `label`, takes the label; one a person named otherwise is refused. | `{ levelId, polygon, label, properties? }` | `{ zoneId }` |
|
|
426
444
|
| `duplicate_level` | Copy a level as the editor does (units whose rooms are all on it included; plan references, scans and spawns left behind), above or below, shifting the floors past it. | `{ levelId, position?, name?, preset? }` | `{ newLevelId, name, floorIndex, shiftedLevelIds, copied, skipped, newNodeIds[] }` |
|
|
445
|
+
| `create_unit` | Create a unit (a dwelling, suite or tenancy of a building with several: apartment, hotel-room, commercial, common) under a building, holding zones from any of its levels; a zone already in another unit is refused unless `move`. A zone in no unit is common area. | `{ buildingId?, name, kind?, color?, memberZoneIds?, move? }` | `{ unitId, buildingId, name, kind, color, members, memberCount, message }` |
|
|
446
|
+
| `set_unit_members` | Replace the zones a unit holds; a zone left out becomes common area. Every zone must be on a level of the unit's building; one in another unit is refused unless `move`. | `{ unitId, memberZoneIds, move? }` | `{ unitId, memberCount, members, message }` |
|
|
447
|
+
| `list_units` | Units of a building, or of every building, each with its member zone ids and a report (member count, level span, gross area, each member's name, level and area). | `{ buildingId? }` | `{ units[] }` |
|
|
427
448
|
| `delete_node` | Delete a node with everything under it, as the editor's Delete does. | `{ id }` | `{ deletedIds: [] }` |
|
|
428
449
|
| `clear_scene` | Empty the project on purpose, back to the default site, building and level. The only way to empty a project: a write that would leave it empty is refused (`scene_wipe_blocked`) by a store that guards against accidental wipes, and the session keeps the project as stored. | `{ reason }` | `{ cleared: { removed }, version, graphHash }` |
|
|
429
450
|
| `undo` | Step back through temporal history. | `{ steps? }` | `{ undone: number }` |
|
|
430
451
|
| `redo` | Step forward through temporal history. | `{ steps? }` | `{ redone: number }` |
|
|
431
452
|
| `export_json` | Serialize the scene graph as JSON. | `{ pretty? }` | `{ json: string }` |
|
|
432
453
|
| `export_glb` | Stubbed: GLB export requires the browser renderer. | — | throws `not_implemented` |
|
|
433
|
-
| `validate_design` | Read-only: validate a design (procedural item recipe, object or JSON string) and measure it. The authority over `pascal://schema/design`. | `{ design, parameters? }` | `{ valid, diagnostics[], design, sweep, measurements }`
|
|
454
|
+
| `validate_design` | Read-only: validate a design (procedural item recipe, object or JSON string) and measure it. The authority over `pascal://schema/design`. | `{ design, parameters? }` | `{ valid, diagnostics[], design, sweep, measurements, schemaUri?, example? }` |
|
|
434
455
|
| `verify_scene` | The one scene check: schema validity, per-level counts, roles and the spaces the walls enclose, empty levels, and typed practical issues (every node field its schema rejects as `schema_invalid` with its `nodeId` and `path`, a node kind no schema knows once per kind, a space with no room or with several, a room the walls do not close or with no name, storeys with no stair, openings off their wall, stairs off their slab, blocked doors, a house past its lot line or in its required yards…). | — | `{ valid, levels[], issues: { type, message, … }[], hasIssues }` |
|
|
435
456
|
| `check_collisions` | Find overlapping items and out-of-bounds placements. | `{ levelId? }` | `{ collisions: { aId, bId, kind }[] }` |
|
|
436
457
|
| `analyze_floorplan_image` | Vision tool: extract walls, rooms, and approximate dimensions from a floorplan image. | `{ image, scaleHint? }` | `{ walls, rooms, approximateDimensions, confidence }` |
|
package/dist/bin/pascal-mcp.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import '../bridge/node-shims.js';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
+
import { sdfChecksFromEnv, setSdfChecks } from '@pascal-app/core/usage-field';
|
|
6
7
|
import { SceneBridge } from '../bridge/scene-bridge.js';
|
|
7
8
|
import { version } from '../index.js';
|
|
8
9
|
import { createPascalMcpServer } from '../server.js';
|
|
@@ -48,6 +49,7 @@ async function main() {
|
|
|
48
49
|
console.log(version);
|
|
49
50
|
process.exit(0);
|
|
50
51
|
}
|
|
52
|
+
setSdfChecks(sdfChecksFromEnv(process.env.PASCAL_SDF_CHECKS));
|
|
51
53
|
const store = await createSceneStore();
|
|
52
54
|
const createServer = () => {
|
|
53
55
|
const bridge = new SceneBridge();
|
|
@@ -6,13 +6,14 @@ import { type SessionKeep, type SessionRecord } from '@pascal-app/core/agent-ope
|
|
|
6
6
|
export type AgentSession = {
|
|
7
7
|
read(): SessionRecord;
|
|
8
8
|
keep(keep: SessionKeep): void;
|
|
9
|
+
/** Starts another session, or puts one back: a host reloading the scene it is bound to keeps it. */
|
|
10
|
+
replace(record?: SessionRecord): void;
|
|
9
11
|
};
|
|
10
12
|
/** The standalone server's: in memory, for as long as the bridge holds a scene. */
|
|
11
13
|
export declare class InMemoryAgentSession implements AgentSession {
|
|
12
14
|
#private;
|
|
13
15
|
read(): SessionRecord;
|
|
14
16
|
keep(keep: SessionKeep): void;
|
|
15
|
-
/** Binding another scene starts another session. */
|
|
16
17
|
replace(record?: SessionRecord): void;
|
|
17
18
|
}
|
|
18
19
|
//# sourceMappingURL=agent-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../src/bridge/agent-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,mCAAmC,CAAA;AAE1C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,IAAI,aAAa,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../src/bridge/agent-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,mCAAmC,CAAA;AAE1C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,IAAI,aAAa,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,oGAAoG;IACpG,OAAO,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,mFAAmF;AACnF,qBAAa,oBAAqB,YAAW,YAAY;;IAGvD,IAAI,IAAI,aAAa,CAEpB;IAED,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAE5B;IAED,OAAO,CAAC,MAAM,GAAE,aAA6B,GAAG,IAAI,CAEnD;CACF"}
|