@pascal-app/mcp 1.0.3 → 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 (305) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +181 -56
  3. package/dist/bin/pascal-mcp.js +9 -3
  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 +42 -6
  10. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  11. package/dist/bridge/scene-bridge.js +232 -45
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/lib/safe-fetch.d.ts +22 -4
  16. package/dist/lib/safe-fetch.d.ts.map +1 -1
  17. package/dist/lib/safe-fetch.js +233 -134
  18. package/dist/operations/scene-operations.d.ts +41 -3
  19. package/dist/operations/scene-operations.d.ts.map +1 -1
  20. package/dist/operations/scene-operations.js +42 -1
  21. package/dist/prompts/from-brief.d.ts +1 -1
  22. package/dist/prompts/from-brief.d.ts.map +1 -1
  23. package/dist/prompts/from-brief.js +3 -3
  24. package/dist/prompts/index.d.ts +1 -1
  25. package/dist/prompts/index.d.ts.map +1 -1
  26. package/dist/prompts/iterate-on-feedback.d.ts +1 -1
  27. package/dist/prompts/iterate-on-feedback.d.ts.map +1 -1
  28. package/dist/prompts/iterate-on-feedback.js +3 -3
  29. package/dist/prompts/renovation-from-photos.d.ts +1 -1
  30. package/dist/prompts/renovation-from-photos.d.ts.map +1 -1
  31. package/dist/prompts/renovation-from-photos.js +2 -2
  32. package/dist/prompts/scene-guidance.d.ts.map +1 -1
  33. package/dist/prompts/scene-guidance.js +13 -6
  34. package/dist/resources/agent-guide.d.ts +1 -1
  35. package/dist/resources/agent-guide.d.ts.map +1 -1
  36. package/dist/resources/agent-guide.js +62 -33
  37. package/dist/resources/catalog-items.d.ts +5 -6
  38. package/dist/resources/catalog-items.d.ts.map +1 -1
  39. package/dist/resources/catalog-items.js +7 -9
  40. package/dist/resources/constraints.d.ts +1 -1
  41. package/dist/resources/constraints.d.ts.map +1 -1
  42. package/dist/resources/constraints.js +1 -1
  43. package/dist/resources/design-schema.d.ts +9 -0
  44. package/dist/resources/design-schema.d.ts.map +1 -0
  45. package/dist/resources/design-schema.js +21 -0
  46. package/dist/resources/index.d.ts +4 -2
  47. package/dist/resources/index.d.ts.map +1 -1
  48. package/dist/resources/index.js +5 -2
  49. package/dist/resources/scene-current.d.ts +1 -1
  50. package/dist/resources/scene-current.d.ts.map +1 -1
  51. package/dist/resources/scene-summary.d.ts +1 -1
  52. package/dist/resources/scene-summary.d.ts.map +1 -1
  53. package/dist/server.d.ts +25 -1
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/server.js +71 -25
  56. package/dist/storage/disk-artifact-store.d.ts +9 -0
  57. package/dist/storage/disk-artifact-store.d.ts.map +1 -0
  58. package/dist/storage/disk-artifact-store.js +46 -0
  59. package/dist/storage/index.d.ts +1 -0
  60. package/dist/storage/index.d.ts.map +1 -1
  61. package/dist/storage/index.js +1 -0
  62. package/dist/storage/sqlite-scene-store.d.ts +3 -3
  63. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  64. package/dist/storage/sqlite-scene-store.js +4 -3
  65. package/dist/storage/types.d.ts +36 -7
  66. package/dist/storage/types.d.ts.map +1 -1
  67. package/dist/storage/types.js +12 -0
  68. package/dist/templates/empty-studio.d.ts +3 -3
  69. package/dist/templates/empty-studio.d.ts.map +1 -1
  70. package/dist/templates/garden-house.d.ts +3 -3
  71. package/dist/templates/garden-house.d.ts.map +1 -1
  72. package/dist/templates/index.d.ts.map +1 -1
  73. package/dist/templates/two-bedroom.d.ts +3 -3
  74. package/dist/templates/two-bedroom.d.ts.map +1 -1
  75. package/dist/tools/add-column.d.ts +5 -0
  76. package/dist/tools/add-column.d.ts.map +1 -0
  77. package/dist/tools/add-column.js +59 -0
  78. package/dist/tools/add-object.d.ts +76 -0
  79. package/dist/tools/add-object.d.ts.map +1 -0
  80. package/dist/tools/add-object.js +187 -0
  81. package/dist/tools/annotations.d.ts.map +1 -1
  82. package/dist/tools/apply-patch.d.ts +9 -2
  83. package/dist/tools/apply-patch.d.ts.map +1 -1
  84. package/dist/tools/apply-patch.js +36 -5
  85. package/dist/tools/asset-catalog.d.ts +9 -5
  86. package/dist/tools/asset-catalog.d.ts.map +1 -1
  87. package/dist/tools/asset-catalog.js +33 -12
  88. package/dist/tools/author-scene.d.ts +10 -0
  89. package/dist/tools/author-scene.d.ts.map +1 -0
  90. package/dist/tools/author-scene.js +140 -0
  91. package/dist/tools/check-collisions.d.ts +7 -7
  92. package/dist/tools/check-collisions.d.ts.map +1 -1
  93. package/dist/tools/check-collisions.js +7 -8
  94. package/dist/tools/clear-scene.d.ts +9 -0
  95. package/dist/tools/clear-scene.d.ts.map +1 -0
  96. package/dist/tools/clear-scene.js +59 -0
  97. package/dist/tools/collections.d.ts +4 -0
  98. package/dist/tools/collections.d.ts.map +1 -0
  99. package/dist/tools/collections.js +53 -0
  100. package/dist/tools/construction-tools.d.ts +12 -88
  101. package/dist/tools/construction-tools.d.ts.map +1 -1
  102. package/dist/tools/construction-tools.js +122 -381
  103. package/dist/tools/create-unit.d.ts +5 -3
  104. package/dist/tools/create-unit.d.ts.map +1 -1
  105. package/dist/tools/create-unit.js +2 -2
  106. package/dist/tools/errors.d.ts +15 -4
  107. package/dist/tools/errors.d.ts.map +1 -1
  108. package/dist/tools/errors.js +14 -4
  109. package/dist/tools/export-glb.d.ts +1 -1
  110. package/dist/tools/export-glb.d.ts.map +1 -1
  111. package/dist/tools/export-glb.js +2 -2
  112. package/dist/tools/export-json.d.ts +1 -1
  113. package/dist/tools/export-json.d.ts.map +1 -1
  114. package/dist/tools/export-json.js +2 -2
  115. package/dist/tools/geometry-script-worker.d.ts +2 -0
  116. package/dist/tools/geometry-script-worker.d.ts.map +1 -0
  117. package/dist/tools/geometry-script-worker.js +21 -0
  118. package/dist/tools/get-scene.d.ts +2 -1
  119. package/dist/tools/get-scene.d.ts.map +1 -1
  120. package/dist/tools/get-scene.js +5 -3
  121. package/dist/tools/honest-patch-guard.d.ts +2 -0
  122. package/dist/tools/honest-patch-guard.d.ts.map +1 -0
  123. package/dist/tools/honest-patch-guard.js +32 -0
  124. package/dist/tools/hosted-services.d.ts +4 -0
  125. package/dist/tools/hosted-services.d.ts.map +1 -0
  126. package/dist/tools/hosted-services.js +40 -0
  127. package/dist/tools/index.d.ts +12 -2
  128. package/dist/tools/index.d.ts.map +1 -1
  129. package/dist/tools/index.js +18 -32
  130. package/dist/tools/level-height-guard.d.ts +2 -0
  131. package/dist/tools/level-height-guard.d.ts.map +1 -0
  132. package/dist/tools/level-height-guard.js +11 -0
  133. package/dist/tools/list-units.d.ts +2 -2
  134. package/dist/tools/list-units.d.ts.map +1 -1
  135. package/dist/tools/list-units.js +2 -2
  136. package/dist/tools/live-sync.d.ts +25 -6
  137. package/dist/tools/live-sync.d.ts.map +1 -1
  138. package/dist/tools/live-sync.js +48 -27
  139. package/dist/tools/local-geometry-scripts.d.ts +8 -0
  140. package/dist/tools/local-geometry-scripts.d.ts.map +1 -0
  141. package/dist/tools/local-geometry-scripts.js +67 -0
  142. package/dist/tools/measure.d.ts +7 -1
  143. package/dist/tools/measure.d.ts.map +1 -1
  144. package/dist/tools/measure.js +38 -70
  145. package/dist/tools/measurement.d.ts +1 -36
  146. package/dist/tools/measurement.d.ts.map +1 -1
  147. package/dist/tools/measurement.js +2 -94
  148. package/dist/tools/node-world-point.d.ts +37 -0
  149. package/dist/tools/node-world-point.d.ts.map +1 -0
  150. package/dist/tools/node-world-point.js +457 -0
  151. package/dist/tools/normalize-schema-dialect.d.ts +1 -1
  152. package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
  153. package/dist/tools/normalize-schema-dialect.js +22 -3
  154. package/dist/tools/patch-guards.d.ts +60 -0
  155. package/dist/tools/patch-guards.d.ts.map +1 -0
  156. package/dist/tools/patch-guards.js +256 -0
  157. package/dist/tools/photo-to-scene/index.d.ts +1 -1
  158. package/dist/tools/photo-to-scene/index.d.ts.map +1 -1
  159. package/dist/tools/photo-to-scene/photo-to-scene.d.ts +3 -3
  160. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  161. package/dist/tools/photo-to-scene/photo-to-scene.js +10 -9
  162. package/dist/tools/project-files.d.ts +4 -0
  163. package/dist/tools/project-files.d.ts.map +1 -0
  164. package/dist/tools/project-files.js +30 -0
  165. package/dist/tools/redo.d.ts +4 -2
  166. package/dist/tools/redo.d.ts.map +1 -1
  167. package/dist/tools/redo.js +4 -4
  168. package/dist/tools/retired-tools.d.ts +10 -0
  169. package/dist/tools/retired-tools.d.ts.map +1 -0
  170. package/dist/tools/retired-tools.js +22 -0
  171. package/dist/tools/roof-level-guard.d.ts +2 -0
  172. package/dist/tools/roof-level-guard.d.ts.map +1 -0
  173. package/dist/tools/roof-level-guard.js +22 -0
  174. package/dist/tools/room-tools.d.ts +36 -98
  175. package/dist/tools/room-tools.d.ts.map +1 -1
  176. package/dist/tools/room-tools.js +130 -565
  177. package/dist/tools/scene-lifecycle/create-project.d.ts +1 -1
  178. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  179. package/dist/tools/scene-lifecycle/create-project.js +31 -6
  180. package/dist/tools/scene-lifecycle/delete-scene.d.ts +3 -1
  181. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  182. package/dist/tools/scene-lifecycle/delete-scene.js +10 -9
  183. package/dist/tools/scene-lifecycle/get-project-status.d.ts +1 -1
  184. package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -1
  185. package/dist/tools/scene-lifecycle/get-project-status.js +6 -6
  186. package/dist/tools/scene-lifecycle/index.d.ts +1 -1
  187. package/dist/tools/scene-lifecycle/index.d.ts.map +1 -1
  188. package/dist/tools/scene-lifecycle/list-scenes.d.ts +1 -1
  189. package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -1
  190. package/dist/tools/scene-lifecycle/list-scenes.js +4 -4
  191. package/dist/tools/scene-lifecycle/load-scene.d.ts +2 -2
  192. package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -1
  193. package/dist/tools/scene-lifecycle/load-scene.js +5 -5
  194. package/dist/tools/scene-lifecycle/metadata.d.ts +1 -1
  195. package/dist/tools/scene-lifecycle/rename-scene.d.ts +1 -1
  196. package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -1
  197. package/dist/tools/scene-lifecycle/rename-scene.js +6 -6
  198. package/dist/tools/scene-lifecycle/save-scene.d.ts +6 -3
  199. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  200. package/dist/tools/scene-lifecycle/save-scene.js +124 -16
  201. package/dist/tools/scene-lifecycle/test-utils.d.ts +3 -3
  202. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  203. package/dist/tools/scene-lifecycle/test-utils.js +2 -1
  204. package/dist/tools/set-unit-members.d.ts +4 -2
  205. package/dist/tools/set-unit-members.d.ts.map +1 -1
  206. package/dist/tools/set-unit-members.js +6 -6
  207. package/dist/tools/set-zone.d.ts +4 -2
  208. package/dist/tools/set-zone.d.ts.map +1 -1
  209. package/dist/tools/set-zone.js +30 -18
  210. package/dist/tools/shared-tools.d.ts +40 -0
  211. package/dist/tools/shared-tools.d.ts.map +1 -0
  212. package/dist/tools/shared-tools.js +574 -0
  213. package/dist/tools/structure-tools.d.ts +57 -0
  214. package/dist/tools/structure-tools.d.ts.map +1 -0
  215. package/dist/tools/structure-tools.js +57 -0
  216. package/dist/tools/templates/create-from-template.d.ts +2 -2
  217. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  218. package/dist/tools/templates/create-from-template.js +6 -6
  219. package/dist/tools/templates/create-house-from-brief.d.ts +1 -1
  220. package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -1
  221. package/dist/tools/templates/create-house-from-brief.js +6 -6
  222. package/dist/tools/templates/index.d.ts +1 -1
  223. package/dist/tools/templates/index.d.ts.map +1 -1
  224. package/dist/tools/templates/list-templates.d.ts +1 -1
  225. package/dist/tools/templates/list-templates.d.ts.map +1 -1
  226. package/dist/tools/templates/list-templates.js +2 -2
  227. package/dist/tools/undo.d.ts +4 -2
  228. package/dist/tools/undo.d.ts.map +1 -1
  229. package/dist/tools/undo.js +4 -4
  230. package/dist/tools/unit-members.d.ts.map +1 -1
  231. package/dist/tools/unit-members.js +6 -6
  232. package/dist/tools/variants/generate-variants.d.ts +6 -6
  233. package/dist/tools/variants/generate-variants.d.ts.map +1 -1
  234. package/dist/tools/variants/generate-variants.js +6 -6
  235. package/dist/tools/variants/index.d.ts +1 -1
  236. package/dist/tools/variants/index.d.ts.map +1 -1
  237. package/dist/tools/variants/mutations.js +1 -1
  238. package/dist/tools/view-scene.d.ts +44 -0
  239. package/dist/tools/view-scene.d.ts.map +1 -0
  240. package/dist/tools/view-scene.js +75 -0
  241. package/dist/tools/vision/analyze-floorplan-image.d.ts +1 -1
  242. package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -1
  243. package/dist/tools/vision/analyze-floorplan-image.js +7 -7
  244. package/dist/tools/vision/analyze-room-photo.d.ts +1 -1
  245. package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -1
  246. package/dist/tools/vision/analyze-room-photo.js +7 -7
  247. package/dist/tools/vision/index.d.ts +1 -1
  248. package/dist/tools/vision/index.d.ts.map +1 -1
  249. package/dist/transports/http.d.ts +8 -2
  250. package/dist/transports/http.d.ts.map +1 -1
  251. package/dist/transports/http.js +9 -3
  252. package/dist/transports/stdio.d.ts +15 -9
  253. package/dist/transports/stdio.d.ts.map +1 -1
  254. package/dist/transports/stdio.js +18 -11
  255. package/dist/version.d.ts +5 -0
  256. package/dist/version.d.ts.map +1 -1
  257. package/dist/version.js +17 -3
  258. package/examples/coordinate-conventions-demo.md +12 -13
  259. package/examples/embed-in-agent.ts +6 -7
  260. package/examples/generate-apartment.md +13 -15
  261. package/examples/photo-to-scene.md +3 -4
  262. package/examples/renovate-from-photos.md +6 -9
  263. package/package.json +19 -9
  264. package/dist/tools/create-level.d.ts +0 -21
  265. package/dist/tools/create-level.d.ts.map +0 -1
  266. package/dist/tools/create-level.js +0 -63
  267. package/dist/tools/create-wall.d.ts +0 -22
  268. package/dist/tools/create-wall.d.ts.map +0 -1
  269. package/dist/tools/create-wall.js +0 -57
  270. package/dist/tools/cut-opening.d.ts +0 -25
  271. package/dist/tools/cut-opening.d.ts.map +0 -1
  272. package/dist/tools/cut-opening.js +0 -61
  273. package/dist/tools/delete-node.d.ts +0 -19
  274. package/dist/tools/delete-node.d.ts.map +0 -1
  275. package/dist/tools/delete-node.js +0 -40
  276. package/dist/tools/describe-node.d.ts +0 -17
  277. package/dist/tools/describe-node.d.ts.map +0 -1
  278. package/dist/tools/describe-node.js +0 -97
  279. package/dist/tools/door-clearance.d.ts +0 -135
  280. package/dist/tools/door-clearance.d.ts.map +0 -1
  281. package/dist/tools/door-clearance.js +0 -334
  282. package/dist/tools/duplicate-level.d.ts +0 -19
  283. package/dist/tools/duplicate-level.d.ts.map +0 -1
  284. package/dist/tools/duplicate-level.js +0 -60
  285. package/dist/tools/find-nodes.d.ts +0 -32
  286. package/dist/tools/find-nodes.d.ts.map +0 -1
  287. package/dist/tools/find-nodes.js +0 -107
  288. package/dist/tools/geometry.d.ts +0 -24
  289. package/dist/tools/geometry.d.ts.map +0 -1
  290. package/dist/tools/geometry.js +0 -94
  291. package/dist/tools/get-node.d.ts +0 -11
  292. package/dist/tools/get-node.d.ts.map +0 -1
  293. package/dist/tools/get-node.js +0 -29
  294. package/dist/tools/layout-clearance.d.ts +0 -105
  295. package/dist/tools/layout-clearance.d.ts.map +0 -1
  296. package/dist/tools/layout-clearance.js +0 -190
  297. package/dist/tools/place-item.d.ts +0 -22
  298. package/dist/tools/place-item.d.ts.map +0 -1
  299. package/dist/tools/place-item.js +0 -89
  300. package/dist/tools/scene-query.d.ts +0 -62
  301. package/dist/tools/scene-query.d.ts.map +0 -1
  302. package/dist/tools/scene-query.js +0 -670
  303. package/dist/tools/validate-scene.d.ts +0 -19
  304. package/dist/tools/validate-scene.d.ts.map +0 -1
  305. package/dist/tools/validate-scene.js +0 -28
@@ -1 +1 @@
1
- {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/safe-fetch.ts"],"names":[],"mappings":"AAkHA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,eAAe,CAAC,CA6F1B"}
1
+ {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/safe-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,WAAW,CAAA;AAExE,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AA2GpD,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYzD;AAkDD,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AA2BrE,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,cAAc,CAcvE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAqBvF,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAOrE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAOD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,EAC3B,IAAI,GAAE,aAA2B,GAChC,OAAO,CAAC,eAAe,CAAC,CAwE1B"}
@@ -1,83 +1,150 @@
1
- import { isIPv4, isIPv6 } from 'node:net';
2
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
1
+ import { lookup as dnsLookup } from 'node:dns/promises';
2
+ import { request as httpRequest } from 'node:http';
3
+ import { request as httpsRequest } from 'node:https';
4
+ import { isIP } from 'node:net';
5
+ import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';
3
6
  /**
4
7
  * SSRF-safe fetch for user-supplied URLs (image URLs in vision tools).
5
8
  *
6
- * Blocks the usual server-side-request-forgery attack surface:
7
- * - loopback (127.0.0.0/8, ::1)
8
- * - link-local (169.254.0.0/16 — includes cloud metadata at 169.254.169.254)
9
- * - private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
10
- * - non-http(s) schemes
11
- * - manual-redirect with the same allowlist applied to each hop
12
- * - max body size (default 20 MB)
13
- * - request timeout (default 10 s)
9
+ * Every hop (the initial URL and each redirect target) is checked the same way:
10
+ * - only http(s) schemes
11
+ * - well-known local hostnames are refused outright
12
+ * - the host is resolved once and *every* resolved address must be public;
13
+ * IPv6 forms that embed an IPv4 address (mapped, compatible, 6to4) are
14
+ * judged by the embedded address
15
+ * - the socket connects only to the addresses that were checked, so a DNS
16
+ * answer that changes between check and connect cannot reach a private host
17
+ * - optional origin allowlist via `PASCAL_ALLOWED_ASSET_ORIGINS` (comma-separated)
18
+ * - max body size (default 20 MB) and request timeout (default 10 s)
14
19
  *
15
- * Optional allowlist via `PASCAL_ALLOWED_ASSET_ORIGINS` env var (comma-separated).
16
- *
17
- * Phase 10 A2 found that photo_to_scene / analyze_floorplan_image /
18
- * analyze_room_photo all called raw `fetch(url)` with no protection, giving
19
- * a direct `169.254.169.254` exfil primitive on any host.
20
+ * Used by photo_to_scene / analyze_floorplan_image / analyze_room_photo.
20
21
  */
21
22
  const DEFAULT_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
22
23
  const DEFAULT_TIMEOUT_MS = 10_000;
23
24
  const MAX_REDIRECTS = 3;
24
- function isPrivateOrLoopbackV4(addr) {
25
- const parts = addr.split('.').map(Number);
26
- if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
27
- return true; // malformed → treat as unsafe
28
- const [a, b] = parts;
29
- if (a === 127)
30
- return true; // 127.0.0.0/8 loopback
25
+ function parseIPv4(addr) {
26
+ const parts = addr.split('.');
27
+ if (parts.length !== 4 || parts.some((p) => !/^\d{1,3}$/.test(p)))
28
+ return null;
29
+ const octets = parts.map(Number);
30
+ return octets.every((n) => n <= 255) ? octets : null;
31
+ }
32
+ /** Expands any valid IPv6 text form (incl. `::` and a dotted IPv4 tail) to 8 groups. */
33
+ function parseIPv6(addr) {
34
+ let s = addr.toLowerCase();
35
+ const zone = s.indexOf('%');
36
+ if (zone >= 0)
37
+ s = s.slice(0, zone);
38
+ let tail = [];
39
+ const lastColon = s.lastIndexOf(':');
40
+ if (lastColon < 0)
41
+ return null;
42
+ if (s.slice(lastColon + 1).includes('.')) {
43
+ const v4 = parseIPv4(s.slice(lastColon + 1));
44
+ if (!v4)
45
+ return null;
46
+ tail = [(v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]];
47
+ const prefix = s.slice(0, lastColon + 1);
48
+ s = prefix.endsWith('::') ? prefix : prefix.slice(0, -1);
49
+ }
50
+ const halves = s.split('::');
51
+ if (halves.length > 2)
52
+ return null;
53
+ const groups = (part) => part === ''
54
+ ? []
55
+ : part.split(':').map((h) => (/^[0-9a-f]{1,4}$/.test(h) ? parseInt(h, 16) : NaN));
56
+ const head = groups(halves[0]);
57
+ const rest = halves.length === 2 ? groups(halves[1]) : [];
58
+ const explicit = head.length + rest.length + tail.length;
59
+ if ([...head, ...rest].some(Number.isNaN))
60
+ return null;
61
+ if (halves.length === 1)
62
+ return explicit === 8 ? [...head, ...tail] : null;
63
+ if (explicit > 7)
64
+ return null;
65
+ return [...head, ...new Array(8 - explicit).fill(0), ...rest, ...tail];
66
+ }
67
+ function isBlockedV4([a, b]) {
68
+ if (a === 0)
69
+ return true; // 0.0.0.0/8 "this network"
31
70
  if (a === 10)
32
71
  return true; // 10.0.0.0/8 private
72
+ if (a === 100 && b >= 64 && b <= 127)
73
+ return true; // 100.64.0.0/10 CGNAT (incl. Alibaba metadata)
74
+ if (a === 127)
75
+ return true; // 127.0.0.0/8 loopback
76
+ if (a === 169 && b === 254)
77
+ return true; // 169.254.0.0/16 link-local (cloud metadata)
33
78
  if (a === 172 && b >= 16 && b <= 31)
34
79
  return true; // 172.16.0.0/12 private
80
+ if (a === 192 && b === 0)
81
+ return true; // 192.0.0.0/24 IETF protocol assignments
35
82
  if (a === 192 && b === 168)
36
83
  return true; // 192.168.0.0/16 private
37
- if (a === 169 && b === 254)
38
- return true; // link-local incl. cloud metadata
39
- if (a === 0)
40
- return true; // current-network
84
+ if (a === 198 && (b === 18 || b === 19))
85
+ return true; // 198.18.0.0/15 benchmarking
41
86
  if (a >= 224)
42
- return true; // multicast / reserved
87
+ return true; // multicast, reserved, broadcast
43
88
  return false;
44
89
  }
45
- function isPrivateOrLoopbackV6(addr) {
46
- const lower = addr.toLowerCase();
47
- if (lower === '::1' || lower === '::')
48
- return true;
49
- if (lower.startsWith('fe80:') ||
50
- lower.startsWith('fe90:') ||
51
- lower.startsWith('fea0:') ||
52
- lower.startsWith('feb0:'))
53
- return true; // link-local
54
- if (lower.startsWith('fc') || lower.startsWith('fd'))
55
- return true; // ULA fc00::/7
56
- if (lower.startsWith('::ffff:')) {
57
- // v4-mapped
58
- const v4 = lower.slice(7);
59
- if (isIPv4(v4))
60
- return isPrivateOrLoopbackV4(v4);
90
+ function embeddedV4(hi, lo) {
91
+ return [hi >> 8, hi & 0xff, lo >> 8, lo & 0xff];
92
+ }
93
+ function isBlockedV6(g) {
94
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = g;
95
+ const first4Zero = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0;
96
+ // ::/96 (unspecified, loopback, IPv4-compatible), ::ffff:0:0/96 (mapped),
97
+ // ::ffff:0:0:0/96 (translated) — judge by the embedded IPv4 address.
98
+ if (first4Zero && ((g4 === 0 && (g5 === 0 || g5 === 0xffff)) || (g4 === 0xffff && g5 === 0))) {
99
+ return isBlockedV4(embeddedV4(g6, g7));
61
100
  }
101
+ if (g0 === 0x64 && g1 === 0xff9b)
102
+ return true; // 64:ff9b::/96 and 64:ff9b:1::/48 NAT64
103
+ if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0)
104
+ return true; // 100::/64 discard
105
+ if (g0 === 0x2001 && g1 === 0)
106
+ return true; // 2001::/32 Teredo
107
+ if (g0 === 0x2002)
108
+ return isBlockedV4(embeddedV4(g1, g2)); // 2002::/16 6to4
109
+ if ((g0 & 0xfe00) === 0xfc00)
110
+ return true; // fc00::/7 ULA (incl. fd00:ec2::254 metadata)
111
+ if ((g0 & 0xffc0) === 0xfe80)
112
+ return true; // fe80::/10 link-local
113
+ if ((g0 & 0xffc0) === 0xfec0)
114
+ return true; // fec0::/10 site-local
115
+ if ((g0 & 0xff00) === 0xff00)
116
+ return true; // ff00::/8 multicast
62
117
  return false;
63
118
  }
64
- function isUnsafeHost(hostname) {
65
- const host = hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets if any
66
- if (isIPv4(host))
67
- return isPrivateOrLoopbackV4(host);
68
- if (isIPv6(host))
69
- return isPrivateOrLoopbackV6(host);
70
- // Hostname (not IP) — block well-known local names.
71
- const lower = host.toLowerCase();
72
- if (lower === 'localhost' ||
119
+ /** True for any address a server-side fetch must not reach. Unparseable input is blocked. */
120
+ export function isBlockedAddress(address) {
121
+ const literal = address.replace(/^\[|\]$/g, '');
122
+ const family = isIP(literal);
123
+ if (family === 4) {
124
+ const v4 = parseIPv4(literal);
125
+ return v4 ? isBlockedV4(v4) : true;
126
+ }
127
+ if (family === 6) {
128
+ const v6 = parseIPv6(literal);
129
+ return v6 ? isBlockedV6(v6) : true;
130
+ }
131
+ return true;
132
+ }
133
+ function isLocalHostname(hostname) {
134
+ const lower = hostname.toLowerCase().replace(/\.+$/, '');
135
+ return (lower === 'localhost' ||
73
136
  lower.endsWith('.localhost') ||
74
137
  lower === 'broadcasthost' ||
75
138
  lower.endsWith('.local') ||
76
139
  lower.endsWith('.internal') ||
77
- lower.endsWith('.corp')) {
78
- return true;
79
- }
80
- return false;
140
+ lower.endsWith('.corp'));
141
+ }
142
+ function blocked(reason, details, log = {}) {
143
+ console.error(`[pascal-mcp] safe_fetch_blocked ${JSON.stringify({ reason, ...details, ...log })}`);
144
+ return protocolError(reason, details);
145
+ }
146
+ function protocolError(reason, details) {
147
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, reason, details);
81
148
  }
82
149
  function assertAllowedUrl(url) {
83
150
  let parsed;
@@ -85,21 +152,11 @@ function assertAllowedUrl(url) {
85
152
  parsed = new URL(url);
86
153
  }
87
154
  catch {
88
- throw new McpError(ErrorCode.InvalidParams, 'invalid_url', { url });
155
+ return protocolError('invalid_url', { url });
89
156
  }
90
157
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
91
- throw new McpError(ErrorCode.InvalidParams, 'url_scheme_not_allowed', {
92
- url,
93
- protocol: parsed.protocol,
94
- });
158
+ blocked('url_scheme_not_allowed', { url, protocol: parsed.protocol });
95
159
  }
96
- if (isUnsafeHost(parsed.hostname)) {
97
- throw new McpError(ErrorCode.InvalidParams, 'url_host_blocked', {
98
- url,
99
- hostname: parsed.hostname,
100
- });
101
- }
102
- // Optional env-allowlist narrowing.
103
160
  const allowEnv = process.env.PASCAL_ALLOWED_ASSET_ORIGINS;
104
161
  if (allowEnv) {
105
162
  const origins = allowEnv
@@ -107,20 +164,85 @@ function assertAllowedUrl(url) {
107
164
  .map((s) => s.trim())
108
165
  .filter(Boolean);
109
166
  if (!origins.includes(parsed.origin)) {
110
- throw new McpError(ErrorCode.InvalidParams, 'url_origin_not_allowlisted', {
111
- url,
112
- origin: parsed.origin,
113
- });
167
+ blocked('url_origin_not_allowlisted', { url, origin: parsed.origin });
114
168
  }
115
169
  }
116
170
  return parsed;
117
171
  }
172
+ /** Resolves `url`'s host and returns its addresses only if every one of them is public. */
173
+ async function resolveCheckedAddresses(url, resolve) {
174
+ const host = url.hostname.replace(/^\[|\]$/g, '');
175
+ const details = { url: url.toString(), hostname: url.hostname };
176
+ const literalFamily = isIP(host);
177
+ let addresses;
178
+ if (literalFamily) {
179
+ addresses = [{ address: host, family: literalFamily }];
180
+ }
181
+ else {
182
+ if (isLocalHostname(host))
183
+ blocked('url_host_blocked', details);
184
+ try {
185
+ addresses = await resolve(host);
186
+ }
187
+ catch (err) {
188
+ return protocolError('dns_lookup_failed', {
189
+ ...details,
190
+ message: err instanceof Error ? err.message : String(err),
191
+ });
192
+ }
193
+ }
194
+ if (addresses.length === 0)
195
+ protocolError('dns_lookup_failed', details);
196
+ const unsafe = addresses.find((a) => isBlockedAddress(a.address));
197
+ if (unsafe)
198
+ blocked('url_host_blocked', details, { address: unsafe.address });
199
+ return addresses;
200
+ }
201
+ /** A socket `lookup` that only ever answers with the given, already-checked addresses. */
202
+ export function pinnedLookup(addresses) {
203
+ return (_hostname, options, callback) => {
204
+ const family = options.family === 'IPv4' ? 4 : options.family === 'IPv6' ? 6 : options.family;
205
+ const matching = family === 4 || family === 6 ? addresses.filter((a) => a.family === family) : addresses;
206
+ const first = matching[0];
207
+ if (!first) {
208
+ callback(Object.assign(new Error('no checked address for family'), { code: 'ENOTFOUND' }), '');
209
+ }
210
+ else if (options.all) {
211
+ callback(null, matching);
212
+ }
213
+ else {
214
+ callback(null, first.address, first.family);
215
+ }
216
+ };
217
+ }
218
+ function requestPinned(url, init) {
219
+ const send = url.protocol === 'https:' ? httpsRequest : httpRequest;
220
+ return new Promise((resolve, reject) => {
221
+ const req = send(url, {
222
+ method: 'GET',
223
+ agent: false,
224
+ headers: init.headers,
225
+ lookup: init.lookup,
226
+ signal: init.signal,
227
+ }, resolve);
228
+ req.on('error', reject);
229
+ req.end();
230
+ });
231
+ }
232
+ const defaultDeps = {
233
+ resolve: (hostname) => dnsLookup(hostname, { all: true, verbatim: true }),
234
+ request: requestPinned,
235
+ };
236
+ function header(res, name) {
237
+ const value = res.headers[name];
238
+ return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);
239
+ }
118
240
  /**
119
- * SSRF-safe fetch that follows redirects manually, revalidating the host
120
- * allowlist + private-IP check on every hop. Throws `McpError` for blocked
121
- * URLs, non-2xx responses, oversize bodies, or timeouts.
241
+ * SSRF-safe fetch that follows redirects manually, re-resolving and rechecking
242
+ * the host on every hop and connecting only to the checked addresses. Throws
243
+ * `ProtocolError` for blocked URLs, non-2xx responses, oversize bodies, or timeouts.
122
244
  */
123
- export async function safeFetch(urlStr, opts = {}) {
245
+ export async function safeFetch(urlStr, opts = {}, deps = defaultDeps) {
124
246
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
125
247
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
126
248
  const hops = [];
@@ -131,89 +253,66 @@ export async function safeFetch(urlStr, opts = {}) {
131
253
  for (let i = 0; i <= MAX_REDIRECTS; i++) {
132
254
  const parsed = assertAllowedUrl(current);
133
255
  hops.push(parsed.toString());
134
- const res = await fetch(parsed, {
135
- redirect: 'manual',
256
+ const addresses = await resolveCheckedAddresses(parsed, deps.resolve);
257
+ const res = await deps.request(parsed, {
258
+ headers: opts.accept ? { Accept: opts.accept } : {},
259
+ lookup: pinnedLookup(addresses),
136
260
  signal: controller.signal,
137
- headers: opts.accept ? { Accept: opts.accept } : undefined,
138
261
  });
139
- // Manual redirect handling
140
- if (res.status >= 300 && res.status < 400) {
141
- const location = res.headers.get('location');
262
+ const status = res.statusCode ?? 0;
263
+ if (status >= 300 && status < 400) {
264
+ res.resume();
265
+ const location = header(res, 'location');
142
266
  if (!location) {
143
- throw new McpError(ErrorCode.InvalidParams, 'redirect_without_location', {
144
- url: parsed.toString(),
145
- status: res.status,
146
- });
267
+ protocolError('redirect_without_location', { url: parsed.toString(), status });
147
268
  }
148
269
  current = new URL(location, parsed).toString();
149
270
  continue;
150
271
  }
151
- if (!res.ok) {
152
- throw new McpError(ErrorCode.InvalidParams, 'fetch_failed', {
272
+ if (status < 200 || status >= 300) {
273
+ res.resume();
274
+ protocolError('fetch_failed', {
153
275
  url: parsed.toString(),
154
- status: res.status,
155
- statusText: res.statusText,
276
+ status,
277
+ statusText: res.statusMessage ?? '',
156
278
  });
157
279
  }
158
- // Enforce Content-Length up front if present.
159
- const declared = Number(res.headers.get('content-length'));
280
+ const declared = Number(header(res, 'content-length') ?? Number.NaN);
160
281
  if (Number.isFinite(declared) && declared > maxBytes) {
161
- throw new McpError(ErrorCode.InvalidParams, 'response_too_large', {
162
- url: parsed.toString(),
163
- declared,
164
- maxBytes,
165
- });
166
- }
167
- // Stream with a running cap so servers that lie about length still get bounded.
168
- const reader = res.body?.getReader();
169
- if (!reader) {
170
- throw new McpError(ErrorCode.InvalidParams, 'empty_response', {
171
- url: parsed.toString(),
172
- });
282
+ res.destroy();
283
+ protocolError('response_too_large', { url: parsed.toString(), declared, maxBytes });
173
284
  }
285
+ // Running cap so servers that lie about length still get bounded.
174
286
  const chunks = [];
175
287
  let total = 0;
176
- while (true) {
177
- const { done, value } = await reader.read();
178
- if (done)
179
- break;
180
- if (value) {
181
- total += value.byteLength;
182
- if (total > maxBytes) {
183
- try {
184
- await reader.cancel();
185
- }
186
- catch {
187
- // ignore
188
- }
189
- throw new McpError(ErrorCode.InvalidParams, 'response_too_large', {
190
- url: parsed.toString(),
191
- received: total,
192
- maxBytes,
193
- });
194
- }
195
- chunks.push(value);
288
+ for await (const chunk of res) {
289
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
290
+ total += buf.byteLength;
291
+ if (total > maxBytes) {
292
+ res.destroy();
293
+ protocolError('response_too_large', { url: parsed.toString(), received: total, maxBytes });
196
294
  }
295
+ chunks.push(buf);
197
296
  }
198
297
  return {
199
- buffer: Buffer.concat(chunks.map((c) => Buffer.from(c))),
200
- contentType: res.headers.get('content-type'),
298
+ buffer: Buffer.concat(chunks),
299
+ contentType: header(res, 'content-type'),
201
300
  finalUrl: parsed.toString(),
202
301
  hops,
203
302
  };
204
303
  }
205
- throw new McpError(ErrorCode.InvalidParams, 'too_many_redirects', {
206
- hops: hops.slice(0, MAX_REDIRECTS + 1),
207
- });
304
+ return protocolError('too_many_redirects', { hops: hops.slice(0, MAX_REDIRECTS + 1) });
208
305
  }
209
306
  catch (err) {
210
- if (err instanceof McpError)
307
+ if (err instanceof ProtocolError)
211
308
  throw err;
212
- if (err.name === 'AbortError') {
213
- throw new McpError(ErrorCode.InvalidParams, 'fetch_timeout', { url: urlStr, timeoutMs });
309
+ if (controller.signal.aborted || err.name === 'AbortError') {
310
+ return protocolError('fetch_timeout', { url: urlStr, timeoutMs });
214
311
  }
215
- const message = err instanceof Error ? err.message : String(err);
216
- throw new McpError(ErrorCode.InvalidParams, 'fetch_error', { url: urlStr, message });
312
+ return protocolError('fetch_error', {
313
+ url: urlStr,
314
+ message: err instanceof Error ? err.message : String(err),
315
+ });
217
316
  }
218
317
  finally {
219
318
  clearTimeout(timer);
@@ -1,12 +1,22 @@
1
+ import type { NodeDeletionPlan, NodeDeletionScene } from '@pascal-app/core';
1
2
  import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
2
- import type { AnyNode, AnyNodeId, AnyNodeType } from '@pascal-app/core/schema';
3
+ import type { AnyNode, AnyNodeId, AnyNodeType, Collection, CollectionId, SceneMaterial, SceneMaterialId } from '@pascal-app/core/schema';
4
+ import type { ZodType } from 'zod';
5
+ import type { AgentSession } from '../bridge/agent-session.js';
3
6
  import type { ActiveSceneMeta, Patch, SceneBridge, ValidationResult } from '../bridge/scene-bridge.js';
4
- import type { ProjectCreateOptions, ProjectStatus, SceneEvent, SceneEventAppendOptions, SceneEventListOptions, SceneListOptions, SceneMeta, SceneMutateOptions, SceneSaveOptions, SceneStore, SceneWithGraph } from '../storage/types.js';
7
+ import type { ProjectCreateOptions, ProjectStatus, SceneDeleteResult, SceneEvent, SceneEventAppendOptions, SceneEventListOptions, SceneListOptions, SceneMeta, SceneMutateOptions, SceneSaveOptions, SceneStore, SceneWithGraph } from '../storage/types.js';
5
8
  export type CreateSceneOperationsOptions = {
6
9
  bridge?: SceneBridge;
7
10
  store?: SceneStore;
11
+ /**
12
+ * The floor the host's person is viewing, which an operation that names no level defaults to,
13
+ * as the chat's does. A host with no view (the remote MCP) leaves it out: the lowest storey.
14
+ */
15
+ activeLevelId?: () => string | null;
8
16
  };
9
17
  export interface SceneOperations {
18
+ /** A host with an evidence-led design workflow owns its brief policy. */
19
+ designBriefRequired?: boolean;
10
20
  readonly hasBridge: boolean;
11
21
  readonly hasStore: boolean;
12
22
  readonly hasSceneEvents: boolean;
@@ -15,6 +25,15 @@ export interface SceneOperations {
15
25
  readonly canCreateProject: boolean;
16
26
  readonly canGetProjectStatus: boolean;
17
27
  readonly storeBackend: SceneStore['backend'] | null;
28
+ /** What the agent's session has kept (named moments, what it made); absent on a host with none. */
29
+ readonly agentSession?: AgentSession;
30
+ /**
31
+ * Whether `applyPatch` takes `upsert_material` patches with the nodes that use them, in one
32
+ * history step. A host that does lends `getMaterials`, and paint keeps native materials there.
33
+ */
34
+ readonly supportsMaterialUpserts?: boolean;
35
+ /** The floor the host's person is viewing, when the host has a view. */
36
+ readonly activeLevelId?: () => string | null;
18
37
  setActiveScene(meta: ActiveSceneMeta): void;
19
38
  getActiveScene(): ActiveSceneMeta | null;
20
39
  clearActiveScene(): void;
@@ -27,6 +46,10 @@ export interface SceneOperations {
27
46
  loadJSON(json: string | SceneGraph): void;
28
47
  getNode(id: AnyNodeId): AnyNode | null;
29
48
  getNodes(): Record<AnyNodeId, AnyNode>;
49
+ getCollections(): Record<CollectionId, Collection>;
50
+ getMaterials?(): Readonly<Record<SceneMaterialId, SceneMaterial>>;
51
+ /** Replace the scene's collections (one undo step, like any edit). */
52
+ setCollections(collections: Record<CollectionId, Collection>): void;
30
53
  getRootNodeIds(): AnyNodeId[];
31
54
  getChildren(parentId: AnyNodeId): AnyNode[];
32
55
  getAncestry(id: AnyNodeId): AnyNode[];
@@ -44,6 +67,20 @@ export interface SceneOperations {
44
67
  deletedIds: AnyNodeId[];
45
68
  createdIds: AnyNodeId[];
46
69
  };
70
+ /**
71
+ * The active bridge's deletion preview, when it has one. Absent for bridges
72
+ * that cannot preview their own deletes; the apply_patch guard is then stricter.
73
+ */
74
+ readonly planDeletion?: (scene: NodeDeletionScene, ids: AnyNodeId[]) => NodeDeletionPlan;
75
+ /**
76
+ * The schema the active bridge checks a stored node of a kind by, for each kind it checks: a host
77
+ * that validates its plugins' nodes at its own write boundary lends it (verify_scene reads it).
78
+ */
79
+ readonly nodeSchema?: (kind: string) => ZodType | undefined;
80
+ deriveStructure(levelIds?: AnyNodeId[]): {
81
+ createdIds: AnyNodeId[];
82
+ deletedIds: AnyNodeId[];
83
+ };
47
84
  undo(steps?: number): number;
48
85
  redo(steps?: number): number;
49
86
  validateScene(): ValidationResult;
@@ -53,12 +90,13 @@ export interface SceneOperations {
53
90
  futureCount: number;
54
91
  };
55
92
  clearHistory(): void;
93
+ runAsSingleHistoryStep<T>(run: () => T): T;
56
94
  createProject(options: ProjectCreateOptions): Promise<ProjectStatus>;
57
95
  getProjectStatus(id: string): Promise<ProjectStatus | null>;
58
96
  saveScene(options: SceneSaveOptions): Promise<SceneMeta>;
59
97
  loadStoredScene(id: string): Promise<SceneWithGraph | null>;
60
98
  listScenes(options?: SceneListOptions): Promise<SceneMeta[]>;
61
- deleteStoredScene(id: string, options?: SceneMutateOptions): Promise<boolean>;
99
+ deleteStoredScene(id: string, options?: SceneMutateOptions): Promise<SceneDeleteResult>;
62
100
  renameStoredScene(id: string, newName: string, options?: SceneMutateOptions): Promise<SceneMeta>;
63
101
  appendSceneEvent(options: SceneEventAppendOptions): Promise<SceneEvent | null>;
64
102
  listSceneEvents(id: string, options?: SceneEventListOptions): Promise<SceneEvent[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"scene-operations.d.ts","sourceRoot":"","sources":["../../src/operations/scene-operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACnG,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACV,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACf,MAAM,kBAAkB,CAAA;AAEzB,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;IAChC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAA;IACtC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAA;IACpC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;IAClC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAA;IACrC,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IAEnD,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAA;IAC3C,cAAc,IAAI,eAAe,GAAG,IAAI,CAAA;IACxC,gBAAgB,IAAI,IAAI,CAAA;IACxB,WAAW,IAAI,IAAI,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC3E,UAAU,IAAI,UAAU,GAAG;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;IACnE,gBAAgB,IAAI,UAAU,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAA;IACzC,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IACtC,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACtC,cAAc,IAAI,SAAS,EAAE,CAAA;IAC7B,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO,EAAE,CAAA;IAC3C,WAAW,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,EAAE,CAAA;IACrC,SAAS,CAAC,MAAM,EAAE;QAChB,IAAI,CAAC,EAAE,WAAW,CAAA;QAClB,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;QAC3B,OAAO,CAAC,EAAE,SAAS,CAAA;KACpB,GAAG,OAAO,EAAE,CAAA;IACb,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAAA;IAC/C,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC1D,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;IACvD,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IACtD,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG;QAC5B,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,SAAS,EAAE,CAAA;QACvB,UAAU,EAAE,SAAS,EAAE,CAAA;KACxB,CAAA;IACD,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B,aAAa,IAAI,gBAAgB,CAAA;IACjC,UAAU,IAAI,MAAM,EAAE,CAAA;IACtB,UAAU,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD,YAAY,IAAI,IAAI,CAAA;IAEpB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IACpE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC3D,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACxD,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;IAC3D,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;IAC5D,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7E,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IAChG,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC9E,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;CACpF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,eAAe,CAE5F"}
1
+ {"version":3,"file":"scene-operations.d.ts","sourceRoot":"","sources":["../../src/operations/scene-operations.ts"],"names":[],"mappings":"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"}
@@ -4,13 +4,30 @@ export function createSceneOperations(options) {
4
4
  class SceneOperationsFacade {
5
5
  #bridge;
6
6
  #store;
7
+ planDeletion;
8
+ nodeSchema;
9
+ activeLevelId;
7
10
  constructor(options) {
8
11
  this.#bridge = options.bridge;
9
12
  this.#store = options.store;
13
+ if (options.activeLevelId)
14
+ this.activeLevelId = options.activeLevelId;
15
+ const bridge = options.bridge;
16
+ if (typeof bridge?.planDeletion === 'function') {
17
+ this.planDeletion = (scene, ids) => bridge.planDeletion(scene, ids);
18
+ }
19
+ if (typeof bridge?.nodeSchema === 'function')
20
+ this.nodeSchema = (kind) => bridge.nodeSchema(kind);
10
21
  }
11
22
  get hasBridge() {
12
23
  return this.#bridge !== undefined;
13
24
  }
25
+ get supportsMaterialUpserts() {
26
+ return this.#bridge?.supportsMaterialUpserts === true;
27
+ }
28
+ get agentSession() {
29
+ return this.#bridge?.agentSession;
30
+ }
14
31
  get hasStore() {
15
32
  return this.#store !== undefined;
16
33
  }
@@ -61,7 +78,9 @@ class SceneOperationsFacade {
61
78
  };
62
79
  }
63
80
  loadJSON(json) {
64
- this.requireBridge().loadJSON(json);
81
+ const bridge = this.requireBridge();
82
+ bridge.loadJSON(json);
83
+ bridge.clearHistory();
65
84
  }
66
85
  getNode(id) {
67
86
  return this.requireBridge().getNode(id);
@@ -69,6 +88,15 @@ class SceneOperationsFacade {
69
88
  getNodes() {
70
89
  return this.requireBridge().getNodes();
71
90
  }
91
+ getCollections() {
92
+ return this.requireBridge().getCollections();
93
+ }
94
+ getMaterials() {
95
+ return this.requireBridge().getMaterials();
96
+ }
97
+ setCollections(collections) {
98
+ this.requireBridge().setCollections(collections);
99
+ }
72
100
  getRootNodeIds() {
73
101
  return this.requireBridge().getRootNodeIds();
74
102
  }
@@ -94,8 +122,18 @@ class SceneOperationsFacade {
94
122
  return this.requireBridge().deleteNode(id, cascade);
95
123
  }
96
124
  applyPatch(patches) {
125
+ if (patches.some((patch) => patch.op === 'upsert_material') && !this.supportsMaterialUpserts)
126
+ throw new Error('materials_unavailable');
97
127
  return this.requireBridge().applyPatch(patches);
98
128
  }
129
+ /**
130
+ * Derive construction from the intent the tools just wrote. The hosted
131
+ * bridge reconciles inside every mutation and exposes no hook, so the call
132
+ * is optional: there it is already done by the time a tool builds its result.
133
+ */
134
+ deriveStructure(levelIds) {
135
+ return this.requireBridge().deriveStructure?.(levelIds) ?? { createdIds: [], deletedIds: [] };
136
+ }
99
137
  undo(steps) {
100
138
  return this.requireBridge().undo(steps);
101
139
  }
@@ -114,6 +152,9 @@ class SceneOperationsFacade {
114
152
  clearHistory() {
115
153
  this.requireBridge().clearHistory();
116
154
  }
155
+ runAsSingleHistoryStep(run) {
156
+ return this.requireBridge().runAsSingleHistoryStep(run);
157
+ }
117
158
  async createProject(options) {
118
159
  const store = this.requireStore();
119
160
  if (!store.createProject) {
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import type { SceneOperations } from '../operations/index.js';
3
3
  /**
4
4
  * Build the user-facing prompt text for `from_brief`. Pure function for testability.