@pascal-app/mcp 1.0.4 → 1.0.5

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.
Files changed (244) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +166 -62
  3. package/dist/bin/pascal-mcp.js +1 -1
  4. package/dist/bridge/agent-session.d.ts +18 -0
  5. package/dist/bridge/agent-session.d.ts.map +1 -0
  6. package/dist/bridge/agent-session.js +15 -0
  7. package/dist/bridge/index.d.ts +1 -1
  8. package/dist/bridge/index.d.ts.map +1 -1
  9. package/dist/bridge/scene-bridge.d.ts +3 -0
  10. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  11. package/dist/bridge/scene-bridge.js +7 -0
  12. package/dist/lib/safe-fetch.d.ts +22 -4
  13. package/dist/lib/safe-fetch.d.ts.map +1 -1
  14. package/dist/lib/safe-fetch.js +233 -134
  15. package/dist/operations/scene-operations.d.ts +18 -0
  16. package/dist/operations/scene-operations.d.ts.map +1 -1
  17. package/dist/operations/scene-operations.js +11 -0
  18. package/dist/prompts/from-brief.d.ts +1 -1
  19. package/dist/prompts/from-brief.d.ts.map +1 -1
  20. package/dist/prompts/from-brief.js +3 -3
  21. package/dist/prompts/index.d.ts +1 -1
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/iterate-on-feedback.d.ts +1 -1
  24. package/dist/prompts/iterate-on-feedback.d.ts.map +1 -1
  25. package/dist/prompts/iterate-on-feedback.js +2 -2
  26. package/dist/prompts/renovation-from-photos.d.ts +1 -1
  27. package/dist/prompts/renovation-from-photos.d.ts.map +1 -1
  28. package/dist/prompts/renovation-from-photos.js +2 -2
  29. package/dist/prompts/scene-guidance.d.ts.map +1 -1
  30. package/dist/prompts/scene-guidance.js +10 -3
  31. package/dist/resources/agent-guide.d.ts +1 -1
  32. package/dist/resources/agent-guide.d.ts.map +1 -1
  33. package/dist/resources/agent-guide.js +52 -44
  34. package/dist/resources/catalog-items.d.ts +1 -1
  35. package/dist/resources/catalog-items.d.ts.map +1 -1
  36. package/dist/resources/constraints.d.ts +1 -1
  37. package/dist/resources/constraints.d.ts.map +1 -1
  38. package/dist/resources/constraints.js +1 -1
  39. package/dist/resources/design-schema.d.ts +1 -1
  40. package/dist/resources/design-schema.d.ts.map +1 -1
  41. package/dist/resources/index.d.ts +1 -1
  42. package/dist/resources/index.d.ts.map +1 -1
  43. package/dist/resources/scene-current.d.ts +1 -1
  44. package/dist/resources/scene-current.d.ts.map +1 -1
  45. package/dist/resources/scene-summary.d.ts +1 -1
  46. package/dist/resources/scene-summary.d.ts.map +1 -1
  47. package/dist/server.d.ts +7 -2
  48. package/dist/server.d.ts.map +1 -1
  49. package/dist/server.js +59 -21
  50. package/dist/storage/disk-artifact-store.d.ts.map +1 -1
  51. package/dist/storage/sqlite-scene-store.d.ts +1 -1
  52. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  53. package/dist/storage/types.d.ts +4 -4
  54. package/dist/storage/types.d.ts.map +1 -1
  55. package/dist/templates/empty-studio.d.ts +3 -3
  56. package/dist/templates/empty-studio.d.ts.map +1 -1
  57. package/dist/templates/garden-house.d.ts +3 -3
  58. package/dist/templates/garden-house.d.ts.map +1 -1
  59. package/dist/templates/index.d.ts.map +1 -1
  60. package/dist/templates/two-bedroom.d.ts +3 -3
  61. package/dist/templates/two-bedroom.d.ts.map +1 -1
  62. package/dist/tools/add-column.d.ts +1 -1
  63. package/dist/tools/add-column.d.ts.map +1 -1
  64. package/dist/tools/add-column.js +3 -2
  65. package/dist/tools/add-object.d.ts +19 -3
  66. package/dist/tools/add-object.d.ts.map +1 -1
  67. package/dist/tools/add-object.js +66 -35
  68. package/dist/tools/annotations.d.ts.map +1 -1
  69. package/dist/tools/apply-patch.d.ts +6 -2
  70. package/dist/tools/apply-patch.d.ts.map +1 -1
  71. package/dist/tools/apply-patch.js +13 -8
  72. package/dist/tools/asset-catalog.d.ts.map +1 -1
  73. package/dist/tools/asset-catalog.js +23 -0
  74. package/dist/tools/author-scene.d.ts +10 -0
  75. package/dist/tools/author-scene.d.ts.map +1 -0
  76. package/dist/tools/author-scene.js +140 -0
  77. package/dist/tools/check-collisions.d.ts +6 -6
  78. package/dist/tools/check-collisions.d.ts.map +1 -1
  79. package/dist/tools/check-collisions.js +6 -6
  80. package/dist/tools/clear-scene.d.ts +1 -1
  81. package/dist/tools/clear-scene.d.ts.map +1 -1
  82. package/dist/tools/clear-scene.js +3 -3
  83. package/dist/tools/collections.d.ts +1 -1
  84. package/dist/tools/collections.d.ts.map +1 -1
  85. package/dist/tools/collections.js +3 -2
  86. package/dist/tools/construction-tools.d.ts +6 -49
  87. package/dist/tools/construction-tools.d.ts.map +1 -1
  88. package/dist/tools/construction-tools.js +9 -158
  89. package/dist/tools/create-unit.d.ts +3 -3
  90. package/dist/tools/create-unit.d.ts.map +1 -1
  91. package/dist/tools/create-unit.js +2 -2
  92. package/dist/tools/errors.d.ts +5 -5
  93. package/dist/tools/errors.d.ts.map +1 -1
  94. package/dist/tools/errors.js +4 -4
  95. package/dist/tools/export-glb.d.ts +1 -1
  96. package/dist/tools/export-glb.d.ts.map +1 -1
  97. package/dist/tools/export-glb.js +2 -2
  98. package/dist/tools/export-json.d.ts +1 -1
  99. package/dist/tools/export-json.d.ts.map +1 -1
  100. package/dist/tools/export-json.js +2 -2
  101. package/dist/tools/get-scene.d.ts +1 -1
  102. package/dist/tools/get-scene.d.ts.map +1 -1
  103. package/dist/tools/get-scene.js +2 -2
  104. package/dist/tools/hosted-services.d.ts +1 -1
  105. package/dist/tools/hosted-services.d.ts.map +1 -1
  106. package/dist/tools/hosted-services.js +13 -3
  107. package/dist/tools/index.d.ts +4 -3
  108. package/dist/tools/index.d.ts.map +1 -1
  109. package/dist/tools/index.js +8 -13
  110. package/dist/tools/level-height-guard.d.ts +2 -0
  111. package/dist/tools/level-height-guard.d.ts.map +1 -0
  112. package/dist/tools/level-height-guard.js +11 -0
  113. package/dist/tools/list-units.d.ts +2 -2
  114. package/dist/tools/list-units.d.ts.map +1 -1
  115. package/dist/tools/list-units.js +2 -2
  116. package/dist/tools/live-sync.d.ts +1 -1
  117. package/dist/tools/live-sync.d.ts.map +1 -1
  118. package/dist/tools/live-sync.js +4 -4
  119. package/dist/tools/measure.d.ts +1 -1
  120. package/dist/tools/measure.d.ts.map +1 -1
  121. package/dist/tools/measure.js +7 -7
  122. package/dist/tools/node-world-point.d.ts +1 -1
  123. package/dist/tools/node-world-point.d.ts.map +1 -1
  124. package/dist/tools/normalize-schema-dialect.d.ts +1 -1
  125. package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
  126. package/dist/tools/normalize-schema-dialect.js +2 -3
  127. package/dist/tools/patch-guards.d.ts +6 -4
  128. package/dist/tools/patch-guards.d.ts.map +1 -1
  129. package/dist/tools/patch-guards.js +5 -2
  130. package/dist/tools/photo-to-scene/index.d.ts +1 -1
  131. package/dist/tools/photo-to-scene/index.d.ts.map +1 -1
  132. package/dist/tools/photo-to-scene/photo-to-scene.d.ts +3 -3
  133. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  134. package/dist/tools/photo-to-scene/photo-to-scene.js +7 -7
  135. package/dist/tools/project-files.d.ts +4 -0
  136. package/dist/tools/project-files.d.ts.map +1 -0
  137. package/dist/tools/project-files.js +30 -0
  138. package/dist/tools/redo.d.ts +2 -2
  139. package/dist/tools/redo.d.ts.map +1 -1
  140. package/dist/tools/redo.js +2 -2
  141. package/dist/tools/retired-tools.d.ts +10 -0
  142. package/dist/tools/retired-tools.d.ts.map +1 -0
  143. package/dist/tools/retired-tools.js +22 -0
  144. package/dist/tools/roof-level-guard.d.ts +2 -0
  145. package/dist/tools/roof-level-guard.d.ts.map +1 -0
  146. package/dist/tools/roof-level-guard.js +22 -0
  147. package/dist/tools/room-tools.d.ts +47 -46
  148. package/dist/tools/room-tools.d.ts.map +1 -1
  149. package/dist/tools/room-tools.js +6 -5
  150. package/dist/tools/scene-lifecycle/create-project.d.ts +1 -1
  151. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  152. package/dist/tools/scene-lifecycle/create-project.js +5 -5
  153. package/dist/tools/scene-lifecycle/delete-scene.d.ts +1 -1
  154. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  155. package/dist/tools/scene-lifecycle/delete-scene.js +6 -6
  156. package/dist/tools/scene-lifecycle/get-project-status.d.ts +1 -1
  157. package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -1
  158. package/dist/tools/scene-lifecycle/get-project-status.js +6 -6
  159. package/dist/tools/scene-lifecycle/index.d.ts +1 -1
  160. package/dist/tools/scene-lifecycle/index.d.ts.map +1 -1
  161. package/dist/tools/scene-lifecycle/list-scenes.d.ts +1 -1
  162. package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -1
  163. package/dist/tools/scene-lifecycle/list-scenes.js +4 -4
  164. package/dist/tools/scene-lifecycle/load-scene.d.ts +2 -2
  165. package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -1
  166. package/dist/tools/scene-lifecycle/load-scene.js +5 -5
  167. package/dist/tools/scene-lifecycle/metadata.d.ts +1 -1
  168. package/dist/tools/scene-lifecycle/rename-scene.d.ts +1 -1
  169. package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -1
  170. package/dist/tools/scene-lifecycle/rename-scene.js +6 -6
  171. package/dist/tools/scene-lifecycle/save-scene.d.ts +5 -3
  172. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  173. package/dist/tools/scene-lifecycle/save-scene.js +80 -12
  174. package/dist/tools/scene-lifecycle/test-utils.d.ts +1 -1
  175. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  176. package/dist/tools/set-unit-members.d.ts +2 -2
  177. package/dist/tools/set-unit-members.d.ts.map +1 -1
  178. package/dist/tools/set-unit-members.js +6 -6
  179. package/dist/tools/set-zone.d.ts +2 -2
  180. package/dist/tools/set-zone.d.ts.map +1 -1
  181. package/dist/tools/set-zone.js +30 -18
  182. package/dist/tools/shared-tools.d.ts +15 -5
  183. package/dist/tools/shared-tools.d.ts.map +1 -1
  184. package/dist/tools/shared-tools.js +242 -29
  185. package/dist/tools/structure-tools.d.ts +3 -3
  186. package/dist/tools/templates/create-from-template.d.ts +2 -2
  187. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  188. package/dist/tools/templates/create-from-template.js +6 -6
  189. package/dist/tools/templates/create-house-from-brief.d.ts +1 -1
  190. package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -1
  191. package/dist/tools/templates/create-house-from-brief.js +6 -6
  192. package/dist/tools/templates/index.d.ts +1 -1
  193. package/dist/tools/templates/index.d.ts.map +1 -1
  194. package/dist/tools/templates/list-templates.d.ts +1 -1
  195. package/dist/tools/templates/list-templates.d.ts.map +1 -1
  196. package/dist/tools/templates/list-templates.js +2 -2
  197. package/dist/tools/undo.d.ts +2 -2
  198. package/dist/tools/undo.d.ts.map +1 -1
  199. package/dist/tools/undo.js +2 -2
  200. package/dist/tools/unit-members.d.ts.map +1 -1
  201. package/dist/tools/unit-members.js +6 -6
  202. package/dist/tools/variants/generate-variants.d.ts +6 -6
  203. package/dist/tools/variants/generate-variants.d.ts.map +1 -1
  204. package/dist/tools/variants/generate-variants.js +6 -6
  205. package/dist/tools/variants/index.d.ts +1 -1
  206. package/dist/tools/variants/index.d.ts.map +1 -1
  207. package/dist/tools/view-scene.d.ts +8 -2
  208. package/dist/tools/view-scene.d.ts.map +1 -1
  209. package/dist/tools/view-scene.js +17 -5
  210. package/dist/tools/vision/analyze-floorplan-image.d.ts +1 -1
  211. package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -1
  212. package/dist/tools/vision/analyze-floorplan-image.js +7 -7
  213. package/dist/tools/vision/analyze-room-photo.d.ts +1 -1
  214. package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -1
  215. package/dist/tools/vision/analyze-room-photo.js +7 -7
  216. package/dist/tools/vision/index.d.ts +1 -1
  217. package/dist/tools/vision/index.d.ts.map +1 -1
  218. package/dist/transports/http.d.ts +8 -2
  219. package/dist/transports/http.d.ts.map +1 -1
  220. package/dist/transports/http.js +9 -3
  221. package/dist/transports/stdio.d.ts +15 -9
  222. package/dist/transports/stdio.d.ts.map +1 -1
  223. package/dist/transports/stdio.js +18 -11
  224. package/examples/coordinate-conventions-demo.md +12 -13
  225. package/examples/embed-in-agent.ts +5 -6
  226. package/examples/generate-apartment.md +4 -4
  227. package/examples/photo-to-scene.md +2 -3
  228. package/examples/renovate-from-photos.md +3 -5
  229. package/package.json +13 -9
  230. package/dist/tools/describe-node.d.ts +0 -17
  231. package/dist/tools/describe-node.d.ts.map +0 -1
  232. package/dist/tools/describe-node.js +0 -99
  233. package/dist/tools/find-nodes.d.ts +0 -16
  234. package/dist/tools/find-nodes.d.ts.map +0 -1
  235. package/dist/tools/find-nodes.js +0 -204
  236. package/dist/tools/place-design.d.ts +0 -34
  237. package/dist/tools/place-design.d.ts.map +0 -1
  238. package/dist/tools/place-design.js +0 -91
  239. package/dist/tools/validate-design.d.ts +0 -136
  240. package/dist/tools/validate-design.d.ts.map +0 -1
  241. package/dist/tools/validate-design.js +0 -28
  242. package/dist/tools/validate-scene.d.ts +0 -19
  243. package/dist/tools/validate-scene.d.ts.map +0 -1
  244. package/dist/tools/validate-scene.js +0 -28
package/README.md CHANGED
@@ -101,8 +101,8 @@ editor reads and writes the same database-side store.
101
101
  Local MCP compilation is opt-in with `PASCAL_SERVER_SCRIPT_COMPILE=1`. It runs in a separate
102
102
  process without inherited environment variables, with a deadline; this is not a sandbox for
103
103
  untrusted code. Blender (`bpy`) modules run in the Blender binary `PASCAL_BLENDER` names
104
- (`blender -b`), with the same caveat. `get_source` can read saved scripts without enabling
105
- compilation.
104
+ (`blender -b`), with the same caveat; [Blender geometry scripts](../../wiki/blender-geometry-scripts.md)
105
+ walks through one. `get_source` can read saved scripts without enabling compilation.
106
106
 
107
107
  ## Live editor updates
108
108
 
@@ -113,7 +113,6 @@ against a loaded saved scene are persisted to SQLite and recorded in a local
113
113
  apply scene graph snapshots as the agent edits the scene.
114
114
 
115
115
  The flow is intentionally local and lightweight:
116
-
117
116
  1. Open or create a scene in the editor so it is saved in the local database.
118
117
  2. Load that scene through MCP with `load_scene`.
119
118
  3. Run MCP mutation tools such as `create_room`, `add_door`, `furnish_room`,
@@ -232,8 +231,7 @@ for agent frameworks and tests.
232
231
 
233
232
  ```ts
234
233
  import { createPascalMcpServer, SceneBridge } from '@pascal-app/mcp'
235
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
236
- import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
234
+ import { Client, InMemoryTransport } from '@modelcontextprotocol/client'
237
235
 
238
236
  const bridge = new SceneBridge()
239
237
  bridge.loadDefault()
@@ -253,6 +251,39 @@ console.log(scene)
253
251
  See [`examples/embed-in-agent.ts`](./examples/embed-in-agent.ts) for a
254
252
  compilable version.
255
253
 
254
+ ### Plugin tools and the person's floor
255
+
256
+ A plugin's `'operation'` tools register with the same runner as the shared
257
+ tools: it parses the input, applies the outcome as one history step and answers
258
+ with `achieved` and the refusals. The annotations are the host's, never read
259
+ from the plugin's contract.
260
+
261
+ ```ts
262
+ import { createSceneOperations } from '@pascal-app/mcp/operations'
263
+ import { registerOperationTool } from '@pascal-app/mcp/tools/shared-tools'
264
+ import { signageAgentTools } from 'acme-signage/agent-tools'
265
+ import { signageAgentOperations } from 'acme-signage/agent-operations'
266
+
267
+ const operations = createSceneOperations({
268
+ bridge,
269
+ // The floor the person is viewing: a tool call that names no level reads it.
270
+ // Leave it out on a host with no view, and the lowest storey is read.
271
+ activeLevelId: () => myHost.viewedLevelId(),
272
+ })
273
+
274
+ for (const contract of signageAgentTools.tools) {
275
+ if (contract.runsIn !== 'operation') continue
276
+ registerOperationTool(server, operations, {
277
+ contract,
278
+ operation: signageAgentOperations[contract.name],
279
+ annotations: { readOnlyHint: false, destructiveHint: false },
280
+ })
281
+ }
282
+ ```
283
+
284
+ The contract shapes, versions and trust rules are in
285
+ [plugin authoring](../../wiki/architecture/plugin-authoring.md#agent-tools-and-chat-extensions).
286
+
256
287
  ## Coordinate conventions
257
288
 
258
289
  Pascal is a **right-handed** scene where **X and Z form the ground plane and Y
@@ -282,7 +313,7 @@ panel rotates its content by the user's view rotation (north-aligned = 0°,
282
313
  the 3-D "top-down" snap preserves the camera's current azimuth, so when invoked
283
314
  from the iso default position, world and screen axes are offset by ~45° until
284
315
  you orbit to an axis-aligned view. So a layout authored as if
285
- *"Y = north, viewed top-down"* — common in land surveys, north-up site plans,
316
+ _"Y = north, viewed top-down"_ — common in land surveys, north-up site plans,
286
317
  and 2-D plotting libraries — will arrive **rotated** relative to its source
287
318
  when viewed in Pascal (and possibly further reflected, depending on which
288
319
  viewport and camera state you're in). The editor's own 2-D and 3-D tools are
@@ -328,72 +359,145 @@ wall-attached rotations are wall-local too.
328
359
  All tools validate their inputs and outputs with Zod. Mutation tools are
329
360
  captured by Zundo's temporal middleware as a single undoable step.
330
361
 
331
- | Name | Purpose | Key input | Output |
332
- | --- | --- | --- | --- |
333
- | `get_scene` | Return the full scene graph. | — | `{ nodes, rootNodeIds, collections }` |
334
- | `get_node` | Fetch a node by id, whole, with the scene materials its finishes use. | `{ id }` | `{ node, materials? }`, or refusal `node_not_found` |
335
- | `describe_node` | Node summary with ancestry, children count and properties. | `{ id }` | `{ id, type, parentId, ancestry[], childrenCount, properties, description }` |
336
- | `find_nodes` | Filter nodes by type (any node kind) / parent / zone / level, or by import source id (`provenance.refs[].id` or legacy `metadata.sourceIds`, exact or prefix). | `{ type?, parentId?, zoneId?, levelId?, sourceId?, sourceIdPrefix? }` | `{ nodes: AnyNode[] }` |
337
- | `list_levels` | List every building's levels in floor order with their role (storey, roof-only, support). | — | `{ activeSceneId, activeLevelId, levelCount, occupiedStoryCount, supportLevelCount, roofLevelIds, levels[] }` |
338
- | `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 }` |
339
- | `get_walls` | Walls on a level with length, stored and resolved height, and child doors/windows. | `{ levelId?, level? }` | `{ levelId, walls[] }` |
340
- | `get_zones` | Room/zone polygons with holes, areas (holes taken out), bounds and floor choices. | `{ levelId?, level? }` | `{ levelId, zones[] }` |
341
- | `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' }` |
342
- | `view_scene` | Look at the building from a viewpoint the agent picks (a side, a street-height eye, a photo's camera, square-on, from above) and get the picture back, with the photo's crop beside it when a region is given. `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. The server has no renderer: the host's `sceneViews` renders it (the hosted app, through the user's open editor tab). Read-only. | `{ target?, interior?, from?, elevation?, eyeHeight?, position?, fov?, projection?, camera?, photo? }` | image content plus `{ status, camera, size, tab, capturedAt }`; refusals `view_unavailable`, `no_project`, `photo_crop_unavailable`, `interior_level_invalid`, `interior_hidden`, `interior_transform_unsupported`, `interior_target_elsewhere`; on the hosted server `editor_tab_required`, `editor_tab_hidden` |
343
- | `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. A query that finds nothing gets a hint to build the item with `add_object`. | `{ queries: [{ query, category? }] }` | `{ groups: [{ query, total, results[] }], total, hint? }`; refusal `no_catalog` |
344
- | `search_materials` | Search the material library by words, several queries in one call: building words as the library writes them (timber is wood, render is stucco), shades and surfaces. Read-only. | `{ queries: [{ query, surface? }] }` | `{ groups: [{ query, results: [{ ref, label, kind, surfaces, color? }], missing? }] }` |
345
- | `paint` | Give walls, columns, slabs, ceilings, fences, doors, windows or roofs 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` face, a slot, a roof's `top`/`edge`/`wall`); 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`, `unknown_material`, `materials_unavailable`, `shadowed_field`, `not_paintable` |
346
- | `create_story_shell` | Create one level-owned story shell from a footprint: the perimeter walls. The floor plate and ceiling are derived from the enclosed rooms; `createSlab` / `createCeiling` / `slabElevation` are recorded as room intent. Use once per story. | `{ levelId, footprint, wallHeight?, wallThickness?, createSlab?, createCeiling? }` | `{ wallIds, zoneIds, slabId, ceilingId, createdIds }` |
347
- | `create_stair` | Create a straight stair as the editor's stair tool does: from a level to the next one above (made when there is none), owning the floor openings it cuts. `(x, z)` is the back-centre of the bottom step, `rotation` in degrees (0 climbs toward +Z). | `{ x, z, levelId?, toLevelId?, rotation?, width?, length?, height?, steps? }` | `{ stairId, segmentId, upperLevelId, createdUpperLevel, stepCount, slabHoleCut, openingIds? }`; refusals `roof_level`, `not_above`, `level_not_found`, `slab_not_found` / `ceiling_not_found`, `slab_not_on_level` / `ceiling_not_on_level` |
348
- | `measure_stair` | Read actual risers, going at the walking line, slope, uniformity, arrival, headroom against floors/ceilings/stair bodies, and design-target diagnostics. | `{ stairId, available?: { width, length } }` | `{ measurements, layouts }` |
349
- | `fit_stair` | Fit uniform risers or replace the chain with a straight/L/U preset with width-sized landings or quarter-turn winders in one undoable edit. | `{ stairId, fitRun?, targets?, layout?, turn?, width?, landingDepth?, turningStrategy?, innerGap?, walkingLineOffset?, division? }` | `{ stairId, measurements }` |
350
- | `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 }` |
351
- | `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. | `{ name, polygon, levelId?, color?, wallHeight?, wallThickness?, outdoor?, doors?, windows? }` | `{ zoneId, slabId, ceilingId, wallIds, reusedWalls, areaSqMeters, doorIds, windowIds, skippedOpenings? }`; refusals `cannot_enclose`, `outdoor_room_overlap` |
352
- | `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, and `nodeId` rebuilds a scripted door. | `{ wallId, t?, width?, height?, openingKind?, hingesSide?, swingDirection?, doorType?, style?, openingShape?, code?, nodeId?, force? }` | `{ doorId, wallId, localX, t, clamped, achieved }`; refusals `wall_not_found`, `not_a_wall`, `curved_wall`, `wall_too_short`, `opening_overlap` (unless `force`) |
353
- | `add_window` | Add a window as `add_door` places a door, with its sill height; `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` |
354
- | `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` |
355
- | `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 }`. | `{ patches: Patch[] }` | `{ applied: number }` |
356
- | `add_level` | Add an empty level to a building, as the editor does: above the highest, or below the lowest for a basement. Omit `buildingId` for the scene's only building. | `{ buildingId?, position?, name?, height? }` | `{ levelId, buildingId, floorIndex, height }`; refusals `building_required`, `building_not_found`, `no_building` |
357
- | `add_wall` | Add a wall to a level, straight or, with `curveOffset`, an arc. Omit `levelId` for the lowest storey. | `{ start, end, levelId?, thickness?, height?, curveOffset? }` | `{ wallId, levelId, length }`; refusals `roof_level`, `wall_too_short`, `level_not_found` |
358
- | `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` |
359
- | `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 }` |
360
- | `set_zone` | Create a zone/room polygon on a level. | `{ levelId, polygon, label, properties? }` | `{ zoneId }` |
361
- | `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[] }` |
362
- | `delete_node` | Delete a node with everything under it, as the editor's Delete does. | `{ id }` | `{ deletedIds: [] }` |
363
- | `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 }` |
364
- | `undo` | Step back through temporal history. | `{ steps? }` | `{ undone: number }` |
365
- | `redo` | Step forward through temporal history. | `{ steps? }` | `{ redone: number }` |
366
- | `export_json` | Serialize the scene graph as JSON. | `{ pretty? }` | `{ json: string }` |
367
- | `export_glb` | Stubbed: GLB export requires the browser renderer. | — | throws `not_implemented` |
368
- | `validate_scene` | Zod-validate every node and parent-child integrity. | — | `{ valid, errors: { nodeId, path, message }[] }` |
369
- | `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 }` |
370
- | `verify_scene` | High-level check with validation status, per-level counts and roles, empty levels, and typed practical issues (storeys with no stair, openings off their wall, stairs off their slab, blocked doors…). | — | `{ valid, levels[], issues: { type, message }[], hasIssues }` |
371
- | `check_collisions` | Find overlapping items and out-of-bounds placements. | `{ levelId? }` | `{ collisions: { aId, bId, kind }[] }` |
372
- | `analyze_floorplan_image` | Vision tool: extract walls, rooms, and approximate dimensions from a floorplan image. | `{ image, scaleHint? }` | `{ walls, rooms, approximateDimensions, confidence }` |
373
- | `analyze_room_photo` | Vision tool: extract approximate dimensions and fixtures from a room photo. | `{ image }` | `{ approximateDimensions, identifiedFixtures, identifiedWindows }` |
362
+ Arguments a tool's input schema rejects come back as a tool error whose JSON
363
+ text is `{ error, code: "invalid_input" }`, as every other refusal names its
364
+ code: the arguments are the wrong shape or type, so read `error`, fix them and
365
+ call again. It is the schema's refusal, before the tool ran; the codes a tool
366
+ raises itself (`apply_patch`'s `invalid_update` and `invalid_parent`, a room's
367
+ `cannot_enclose`) come back as `{ error, code, ...details }`, and `apply_patch`
368
+ adds `patchIndex` and `id`. A level an agent writes without a `height` (a
369
+ `create` in `apply_patch`, or an update that clears it) is given the height the
370
+ editor's own load would derive from what the level holds: the top of its
371
+ tallest wall or ceiling, and for an empty level the default storey height, 2.5 m.
372
+ `apply_patch` says so in `notes`, one line per level filled. `add_level` takes
373
+ an optional `height` and uses the same default.
374
+
375
+ Every length and angle a shared tool takes reads one notation: a bare number is
376
+ in the unit the field states (metres, degrees, radians), and text carries its
377
+ own unit, inches, feet and millimetres included. A roof pitch is degrees or rise
378
+ over run:
379
+
380
+ ```json
381
+ { "wallHeight": "9 ft", "overhang": "18 in", "pitch": "6:12" }
382
+ ```
383
+
384
+ `"26.6"`, `"26.6°"`, `"0.46 rad"`, `"6/12"`, `"6 in 12"` and `"6 on 12"` are
385
+ read too. Minutes and seconds are refused (`'6"'` is a builder's inches), and so
386
+ is a "pitch" fraction like `"1/4 pitch"`, which is rise over span.
387
+
388
+ | Name | Purpose | Key input | Output |
389
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
390
+ | `get_scene` | Return the full scene graph. | — | `{ nodes, rootNodeIds, collections }` |
391
+ | `get_node` | Fetch a node by id, whole, with what the scene decides for it beside its stored fields (a wall's or ceiling's `resolvedHeight` and `heightIsExplicit`, a wall's `resolvedThickness`, a door's or window's `resolvedOpening`, a level's or floor plate's `displayName`), its ancestors parent first, and the scene materials its finishes use; a node its schema rejects comes back as stored, nothing derived. | `{ id }` | `{ node, ancestorIds, missingChildIds?, materials? }`, or refusal `node_not_found` |
392
+ | `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
+ | `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
+ | `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. | `{ levelId?, level? }` | `{ levelId, walls[] }` |
396
+ | `get_zones` | Room/zone polygons with holes, areas (holes taken out), bounds and floor choices. | `{ levelId?, level? }` | `{ levelId, zones[] }` |
397
+ | `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
+ | `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
+ | `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 (the hosted app, through the user's open editor tab). 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) |
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. A query that finds nothing gets a hint to build the item with `add_object`. | `{ queries: [{ query, category? }] }` | `{ groups: [{ query, total, results[] }], total, hint? }`; refusal `no_catalog` |
402
+ | `design_brief` | The request read as a brief, once for the whole request: `axes` (the request read axis by axis: `action`, `size`, `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`, 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`; over MCP an axis quote is required and not compared, there being no message), the eighth 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: at least two words naming the brand, the catalog or what is already there; not compared with a request here) and, with an open source, technical equipment placed as it is (`asIs`: sanitaryware, appliances, fixed services; each piece shown by `view_assets` first, each with a reason; furniture is made). Placing or making furnishings 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. Source `open` opens the made pieces, places technical equipment only when named in `asIs` 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 holding a refused call is refused whole. 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
+ | `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
+ | `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
+ | `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` |
406
+ | `create_story_shell` | Create one level-owned story shell from a footprint: the perimeter walls. The floor plate and ceiling are derived from the enclosed rooms; `createSlab` / `createCeiling` / `slabElevation` are recorded as room intent. Use once per story. | `{ levelId, footprint, wallHeight?, wallThickness?, createSlab?, createCeiling? }` | `{ wallIds, zoneIds, slabId, ceilingId, createdIds }` |
407
+ | `create_stair` | Create a straight, L or U stair as the editor's stair tool does: from a level to the next one above (made when there is none), owning the floor openings it cuts, or to no level (`toLevelId: null`).`(x, z)` is the middle of the bottom riser, `rotation` in degrees (0 climbs toward +Z). | `{ x, z, levelId?, toLevelId?, rotation?, layout?, turn?, lowerSteps?, width?, length?, height?, steps?, railingMode?, railingStyle?, railingMaterial? }` | `{ stairId, segmentIds, upperLevelId, createdUpperLevel, layout, stepCount, rise, length, footprint, arrival, slabHoleCut, openingIds? }`; refusals `roof_level`, `not_above`, `level_not_found`, `slab_not_found` / `ceiling_not_found`, `slab_not_on_level` / `ceiling_not_on_level`, `stair_layout`, `stair_layout_required`, `no_level_above` |
408
+ | `measure_stair` | Read actual risers, going at the walking line, slope, uniformity, arrival, headroom against floors/ceilings/stair bodies, and design-target diagnostics. | `{ stairId, available?: { width, length } }` | `{ measurements, layouts }` |
409
+ | `fit_stair` | Fit uniform risers or replace the chain with a straight/L/U preset with width-sized landings or quarter-turn winders in one undoable edit. | `{ stairId, fitRun?, targets?, layout?, turn?, width?, landingDepth?, turningStrategy?, innerGap?, walkingLineOffset?, division? }` | `{ stairId, measurements }` |
410
+ | `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
+ | `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
+ | `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, and `nodeId` rebuilds a scripted door. | `{ wallId, t?, width?, height?, openingKind?, hingesSide?, swingDirection?, doorType?, style?, openingShape?, code?, nodeId?, force? }` | `{ doorId, wallId, localX, t, clamped, achieved }`; refusals `wall_not_found`, `not_a_wall`, `curved_wall`, `wall_too_short`, `opening_overlap` (unless `force`) |
414
+ | `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
+ | `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
+ | `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
+ | `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
+ | `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
+ | `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 over grade as the building's foundation construction seats it, the top of the foundation (8 in, 0.203 m, on block: the default and the US; 0.15 m on cast concrete in Europe; 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, aboveGrade, foundation { region, construction, basis }, movedBuildingBy, renumbered, unchanged? }`; refusals `level_not_found`, `not_a_level`, `no_building`, `not_a_storey` |
421
+ | `add_wall` | Add a wall to a level, straight or, with `curveOffset`, an arc. Omit `levelId` for the lowest storey. | `{ start, end, levelId?, thickness?, height?, curveOffset? }` | `{ wallId, levelId, length }`; refusals `roof_level`, `wall_too_short`, `level_not_found` |
422
+ | `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
+ | `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
+ | `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
+ | `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
+ | `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[] }` |
427
+ | `delete_node` | Delete a node with everything under it, as the editor's Delete does. | `{ id }` | `{ deletedIds: [] }` |
428
+ | `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
+ | `undo` | Step back through temporal history. | `{ steps? }` | `{ undone: number }` |
430
+ | `redo` | Step forward through temporal history. | `{ steps? }` | `{ redone: number }` |
431
+ | `export_json` | Serialize the scene graph as JSON. | `{ pretty? }` | `{ json: string }` |
432
+ | `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 }` |
434
+ | `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
+ | `check_collisions` | Find overlapping items and out-of-bounds placements. | `{ levelId? }` | `{ collisions: { aId, bId, kind }[] }` |
436
+ | `analyze_floorplan_image` | Vision tool: extract walls, rooms, and approximate dimensions from a floorplan image. | `{ image, scaleHint? }` | `{ walls, rooms, approximateDimensions, confidence }` |
437
+ | `analyze_room_photo` | Vision tool: extract approximate dimensions and fixtures from a room photo. | `{ image }` | `{ approximateDimensions, identifiedFixtures, identifiedWindows }` |
374
438
 
375
439
  The vision tools require the MCP host to support the sampling capability
376
440
  (`createMessage`). Hosts that don't will see a structured
377
441
  `sampling_unavailable` error.
378
442
 
443
+ A retired tool is listed no more and still answered: a call to its name
444
+ returns an error with the code `tool_retired`, the tool that replaced it
445
+ (`replacement`) and how to use that one. `validate_scene` retired into
446
+ `verify_scene`, `find_nodes` into `find_by_type` and `describe_node` into
447
+ `get_node` (`RETIRED_MCP_TOOLS` in `@pascal-app/core/agent-tools`).
448
+
449
+ ```jsonc
450
+ // find_nodes { zoneId } → find_by_type, with or without a type
451
+ { "name": "find_by_type", "arguments": { "type": "window", "zoneId": "zone_kitchen" } }
452
+ { "name": "find_by_type", "arguments": { "sourceIdPrefix": "ground/exterior-01" } }
453
+ // describe_node → get_node: resolvedHeight, resolvedThickness, ancestorIds, displayName…
454
+ { "name": "get_node", "arguments": { "id": "wall_host" } }
455
+ // validate_scene → verify_scene: schema_invalid issues by nodeId and path, room findings
456
+ { "name": "verify_scene", "arguments": {} }
457
+ ```
458
+
459
+ ### Examples
460
+
461
+ ```jsonc
462
+ // The lot before the house: frontage, depth, each buildable edge as a front, the ground
463
+ { "name": "get_site", "arguments": {} }
464
+ // An L stair with a glass railing, and porch steps to no level
465
+ { "name": "create_stair", "arguments": { "x": 2, "z": 1, "layout": "l", "turn": "left", "lowerSteps": 6, "railingStyle": "glass" } }
466
+ { "name": "create_stair", "arguments": { "x": 0, "z": -4, "toLevelId": null, "steps": 3, "height": "0.5 m" } }
467
+ // A glazed barrel roof over a 6 × 4 m rectangle
468
+ { "name": "create_roof", "arguments": { "roofType": "barrel", "roofSystem": "glazed", "width": 6, "depth": 4, "barrelRise": 1.4, "barrelSegments": 12 } }
469
+ // Gable ends reaching 0.6 m past their walls, apart from the eaves
470
+ { "name": "apply_patch", "arguments": { "patches": [{ "op": "update", "id": "rseg_main", "data": { "gableOverhang": 0.6 } }] } }
471
+ // Find a finish, then paint every exterior wall face with it
472
+ { "name": "search_materials", "arguments": { "queries": [{ "query": "white render", "surface": "wall" }] } }
473
+ { "name": "paint", "arguments": { "targets": ["wall_a", "wall_b"], "role": "exterior", "material": "library:<id>" } }
474
+ // The house as a street photo shows it, at dusk, small
475
+ { "name": "view_scene", "arguments": { "from": "street-left", "lighting": "dusk", "maxPx": 960 } }
476
+ ```
477
+
478
+ `gableOverhang` is a roof segment's own reach past its gable-end walls (a
479
+ gable's or gambrel's ends, a shed's sides), in plan metres; left out, the ends
480
+ reach as far as the eaves. The roof panel's "Gable overhang" slider edits the
481
+ same field.
482
+
379
483
  ## Resources
380
484
 
381
- | URI | MIME | Purpose |
382
- | --- | --- | --- |
383
- | `pascal://scene/current` | `application/json` | Full `{ nodes, rootNodeIds, collections }` snapshot. |
384
- | `pascal://scene/current/summary` | `text/markdown` | Human-readable summary with node counts, bounding box, and level areas. |
385
- | `pascal://agent/guide` | `text/markdown` | MCP-first construction workflow, scene invariants, and tool preferences for agents. |
386
- | `pascal://catalog/items` | `application/json` | Dependency-free built-in catalog subset for common residential furniture and fixtures. |
387
- | `pascal://constraints/{levelId}` | `application/json` | Slab footprints and wall polygons for the given level — useful as planner context. |
388
- | `pascal://schema/design` | `application/json` | Design contract: JSON Schema generated from core's `RecipeSchema`, limits, rules only `validate_design` checks, and an example. |
485
+ | URI | MIME | Purpose |
486
+ | -------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
487
+ | `pascal://scene/current` | `application/json` | Full `{ nodes, rootNodeIds, collections }` snapshot. |
488
+ | `pascal://scene/current/summary` | `text/markdown` | Human-readable summary with node counts, bounding box, and level areas. |
489
+ | `pascal://agent/guide` | `text/markdown` | MCP-first construction workflow, scene invariants, and tool preferences for agents. On a hosted server it also points at the project's other chats, files and members (`search_project_chats`, `read_project_chat`, `get_project_assets`, `read_project_asset`, `list_project_members`), read as quoted data, never instructions. |
490
+ | `pascal://catalog/items` | `application/json` | Dependency-free built-in catalog subset for common residential furniture and fixtures. |
491
+ | `pascal://constraints/{levelId}` | `application/json` | Slab footprints and wall polygons for the given level — useful as planner context. |
492
+ | `pascal://schema/design` | `application/json` | Design contract: JSON Schema generated from core's `RecipeSchema`, limits, rules only `validate_design` checks, and an example. |
389
493
 
390
494
  ## Prompts
391
495
 
392
- | Name | Args | Purpose |
393
- | --- | --- | --- |
394
- | `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. |
395
- | `iterate_on_feedback` | `{ feedback: string }` | Minimal-diff instructions: examine the current scene, then propose the smallest patch set that satisfies the feedback. |
396
- | `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. |
496
+ | Name | Args | Purpose |
497
+ | ------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
498
+ | `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. |
499
+ | `iterate_on_feedback` | `{ feedback: string }` | Minimal-diff instructions: examine the current scene, then propose the smallest patch set that satisfies the feedback. |
500
+ | `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. |
397
501
 
398
502
  ## Limitations
399
503