@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.
- package/CHANGELOG.md +76 -0
- package/README.md +181 -56
- package/dist/bin/pascal-mcp.js +9 -3
- package/dist/bridge/agent-session.d.ts +18 -0
- package/dist/bridge/agent-session.d.ts.map +1 -0
- package/dist/bridge/agent-session.js +15 -0
- package/dist/bridge/index.d.ts +1 -1
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.d.ts +42 -6
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +232 -45
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/safe-fetch.d.ts +22 -4
- package/dist/lib/safe-fetch.d.ts.map +1 -1
- package/dist/lib/safe-fetch.js +233 -134
- package/dist/operations/scene-operations.d.ts +41 -3
- package/dist/operations/scene-operations.d.ts.map +1 -1
- package/dist/operations/scene-operations.js +42 -1
- package/dist/prompts/from-brief.d.ts +1 -1
- package/dist/prompts/from-brief.d.ts.map +1 -1
- package/dist/prompts/from-brief.js +3 -3
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/iterate-on-feedback.d.ts +1 -1
- package/dist/prompts/iterate-on-feedback.d.ts.map +1 -1
- package/dist/prompts/iterate-on-feedback.js +3 -3
- package/dist/prompts/renovation-from-photos.d.ts +1 -1
- package/dist/prompts/renovation-from-photos.d.ts.map +1 -1
- package/dist/prompts/renovation-from-photos.js +2 -2
- package/dist/prompts/scene-guidance.d.ts.map +1 -1
- package/dist/prompts/scene-guidance.js +13 -6
- package/dist/resources/agent-guide.d.ts +1 -1
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +62 -33
- package/dist/resources/catalog-items.d.ts +5 -6
- package/dist/resources/catalog-items.d.ts.map +1 -1
- package/dist/resources/catalog-items.js +7 -9
- package/dist/resources/constraints.d.ts +1 -1
- package/dist/resources/constraints.d.ts.map +1 -1
- package/dist/resources/constraints.js +1 -1
- package/dist/resources/design-schema.d.ts +9 -0
- package/dist/resources/design-schema.d.ts.map +1 -0
- package/dist/resources/design-schema.js +21 -0
- package/dist/resources/index.d.ts +4 -2
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +5 -2
- package/dist/resources/scene-current.d.ts +1 -1
- package/dist/resources/scene-current.d.ts.map +1 -1
- package/dist/resources/scene-summary.d.ts +1 -1
- package/dist/resources/scene-summary.d.ts.map +1 -1
- package/dist/server.d.ts +25 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +71 -25
- package/dist/storage/disk-artifact-store.d.ts +9 -0
- package/dist/storage/disk-artifact-store.d.ts.map +1 -0
- package/dist/storage/disk-artifact-store.js +46 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/sqlite-scene-store.d.ts +3 -3
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
- package/dist/storage/sqlite-scene-store.js +4 -3
- package/dist/storage/types.d.ts +36 -7
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +12 -0
- package/dist/templates/empty-studio.d.ts +3 -3
- package/dist/templates/empty-studio.d.ts.map +1 -1
- package/dist/templates/garden-house.d.ts +3 -3
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/two-bedroom.d.ts +3 -3
- package/dist/templates/two-bedroom.d.ts.map +1 -1
- package/dist/tools/add-column.d.ts +5 -0
- package/dist/tools/add-column.d.ts.map +1 -0
- package/dist/tools/add-column.js +59 -0
- package/dist/tools/add-object.d.ts +76 -0
- package/dist/tools/add-object.d.ts.map +1 -0
- package/dist/tools/add-object.js +187 -0
- package/dist/tools/annotations.d.ts.map +1 -1
- package/dist/tools/apply-patch.d.ts +9 -2
- package/dist/tools/apply-patch.d.ts.map +1 -1
- package/dist/tools/apply-patch.js +36 -5
- package/dist/tools/asset-catalog.d.ts +9 -5
- package/dist/tools/asset-catalog.d.ts.map +1 -1
- package/dist/tools/asset-catalog.js +33 -12
- package/dist/tools/author-scene.d.ts +10 -0
- package/dist/tools/author-scene.d.ts.map +1 -0
- package/dist/tools/author-scene.js +140 -0
- package/dist/tools/check-collisions.d.ts +7 -7
- package/dist/tools/check-collisions.d.ts.map +1 -1
- package/dist/tools/check-collisions.js +7 -8
- package/dist/tools/clear-scene.d.ts +9 -0
- package/dist/tools/clear-scene.d.ts.map +1 -0
- package/dist/tools/clear-scene.js +59 -0
- package/dist/tools/collections.d.ts +4 -0
- package/dist/tools/collections.d.ts.map +1 -0
- package/dist/tools/collections.js +53 -0
- package/dist/tools/construction-tools.d.ts +12 -88
- package/dist/tools/construction-tools.d.ts.map +1 -1
- package/dist/tools/construction-tools.js +122 -381
- package/dist/tools/create-unit.d.ts +5 -3
- package/dist/tools/create-unit.d.ts.map +1 -1
- package/dist/tools/create-unit.js +2 -2
- package/dist/tools/errors.d.ts +15 -4
- package/dist/tools/errors.d.ts.map +1 -1
- package/dist/tools/errors.js +14 -4
- package/dist/tools/export-glb.d.ts +1 -1
- package/dist/tools/export-glb.d.ts.map +1 -1
- package/dist/tools/export-glb.js +2 -2
- package/dist/tools/export-json.d.ts +1 -1
- package/dist/tools/export-json.d.ts.map +1 -1
- package/dist/tools/export-json.js +2 -2
- package/dist/tools/geometry-script-worker.d.ts +2 -0
- package/dist/tools/geometry-script-worker.d.ts.map +1 -0
- package/dist/tools/geometry-script-worker.js +21 -0
- package/dist/tools/get-scene.d.ts +2 -1
- package/dist/tools/get-scene.d.ts.map +1 -1
- package/dist/tools/get-scene.js +5 -3
- package/dist/tools/honest-patch-guard.d.ts +2 -0
- package/dist/tools/honest-patch-guard.d.ts.map +1 -0
- package/dist/tools/honest-patch-guard.js +32 -0
- package/dist/tools/hosted-services.d.ts +4 -0
- package/dist/tools/hosted-services.d.ts.map +1 -0
- package/dist/tools/hosted-services.js +40 -0
- package/dist/tools/index.d.ts +12 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +18 -32
- package/dist/tools/level-height-guard.d.ts +2 -0
- package/dist/tools/level-height-guard.d.ts.map +1 -0
- package/dist/tools/level-height-guard.js +11 -0
- package/dist/tools/list-units.d.ts +2 -2
- package/dist/tools/list-units.d.ts.map +1 -1
- package/dist/tools/list-units.js +2 -2
- package/dist/tools/live-sync.d.ts +25 -6
- package/dist/tools/live-sync.d.ts.map +1 -1
- package/dist/tools/live-sync.js +48 -27
- package/dist/tools/local-geometry-scripts.d.ts +8 -0
- package/dist/tools/local-geometry-scripts.d.ts.map +1 -0
- package/dist/tools/local-geometry-scripts.js +67 -0
- package/dist/tools/measure.d.ts +7 -1
- package/dist/tools/measure.d.ts.map +1 -1
- package/dist/tools/measure.js +38 -70
- package/dist/tools/measurement.d.ts +1 -36
- package/dist/tools/measurement.d.ts.map +1 -1
- package/dist/tools/measurement.js +2 -94
- package/dist/tools/node-world-point.d.ts +37 -0
- package/dist/tools/node-world-point.d.ts.map +1 -0
- package/dist/tools/node-world-point.js +457 -0
- package/dist/tools/normalize-schema-dialect.d.ts +1 -1
- package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
- package/dist/tools/normalize-schema-dialect.js +22 -3
- package/dist/tools/patch-guards.d.ts +60 -0
- package/dist/tools/patch-guards.d.ts.map +1 -0
- package/dist/tools/patch-guards.js +256 -0
- package/dist/tools/photo-to-scene/index.d.ts +1 -1
- package/dist/tools/photo-to-scene/index.d.ts.map +1 -1
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts +3 -3
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
- package/dist/tools/photo-to-scene/photo-to-scene.js +10 -9
- package/dist/tools/project-files.d.ts +4 -0
- package/dist/tools/project-files.d.ts.map +1 -0
- package/dist/tools/project-files.js +30 -0
- package/dist/tools/redo.d.ts +4 -2
- package/dist/tools/redo.d.ts.map +1 -1
- package/dist/tools/redo.js +4 -4
- package/dist/tools/retired-tools.d.ts +10 -0
- package/dist/tools/retired-tools.d.ts.map +1 -0
- package/dist/tools/retired-tools.js +22 -0
- package/dist/tools/roof-level-guard.d.ts +2 -0
- package/dist/tools/roof-level-guard.d.ts.map +1 -0
- package/dist/tools/roof-level-guard.js +22 -0
- package/dist/tools/room-tools.d.ts +36 -98
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +130 -565
- package/dist/tools/scene-lifecycle/create-project.d.ts +1 -1
- package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/create-project.js +31 -6
- package/dist/tools/scene-lifecycle/delete-scene.d.ts +3 -1
- package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/delete-scene.js +10 -9
- package/dist/tools/scene-lifecycle/get-project-status.d.ts +1 -1
- package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/get-project-status.js +6 -6
- package/dist/tools/scene-lifecycle/index.d.ts +1 -1
- package/dist/tools/scene-lifecycle/index.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/list-scenes.d.ts +1 -1
- package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/list-scenes.js +4 -4
- package/dist/tools/scene-lifecycle/load-scene.d.ts +2 -2
- package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/load-scene.js +5 -5
- package/dist/tools/scene-lifecycle/metadata.d.ts +1 -1
- package/dist/tools/scene-lifecycle/rename-scene.d.ts +1 -1
- package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/rename-scene.js +6 -6
- package/dist/tools/scene-lifecycle/save-scene.d.ts +6 -3
- package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/save-scene.js +124 -16
- package/dist/tools/scene-lifecycle/test-utils.d.ts +3 -3
- package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
- package/dist/tools/scene-lifecycle/test-utils.js +2 -1
- package/dist/tools/set-unit-members.d.ts +4 -2
- package/dist/tools/set-unit-members.d.ts.map +1 -1
- package/dist/tools/set-unit-members.js +6 -6
- package/dist/tools/set-zone.d.ts +4 -2
- package/dist/tools/set-zone.d.ts.map +1 -1
- package/dist/tools/set-zone.js +30 -18
- package/dist/tools/shared-tools.d.ts +40 -0
- package/dist/tools/shared-tools.d.ts.map +1 -0
- package/dist/tools/shared-tools.js +574 -0
- package/dist/tools/structure-tools.d.ts +57 -0
- package/dist/tools/structure-tools.d.ts.map +1 -0
- package/dist/tools/structure-tools.js +57 -0
- package/dist/tools/templates/create-from-template.d.ts +2 -2
- package/dist/tools/templates/create-from-template.d.ts.map +1 -1
- package/dist/tools/templates/create-from-template.js +6 -6
- package/dist/tools/templates/create-house-from-brief.d.ts +1 -1
- package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -1
- package/dist/tools/templates/create-house-from-brief.js +6 -6
- package/dist/tools/templates/index.d.ts +1 -1
- package/dist/tools/templates/index.d.ts.map +1 -1
- package/dist/tools/templates/list-templates.d.ts +1 -1
- package/dist/tools/templates/list-templates.d.ts.map +1 -1
- package/dist/tools/templates/list-templates.js +2 -2
- package/dist/tools/undo.d.ts +4 -2
- package/dist/tools/undo.d.ts.map +1 -1
- package/dist/tools/undo.js +4 -4
- package/dist/tools/unit-members.d.ts.map +1 -1
- package/dist/tools/unit-members.js +6 -6
- package/dist/tools/variants/generate-variants.d.ts +6 -6
- package/dist/tools/variants/generate-variants.d.ts.map +1 -1
- package/dist/tools/variants/generate-variants.js +6 -6
- package/dist/tools/variants/index.d.ts +1 -1
- package/dist/tools/variants/index.d.ts.map +1 -1
- package/dist/tools/variants/mutations.js +1 -1
- package/dist/tools/view-scene.d.ts +44 -0
- package/dist/tools/view-scene.d.ts.map +1 -0
- package/dist/tools/view-scene.js +75 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts +1 -1
- package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -1
- package/dist/tools/vision/analyze-floorplan-image.js +7 -7
- package/dist/tools/vision/analyze-room-photo.d.ts +1 -1
- package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -1
- package/dist/tools/vision/analyze-room-photo.js +7 -7
- package/dist/tools/vision/index.d.ts +1 -1
- package/dist/tools/vision/index.d.ts.map +1 -1
- package/dist/transports/http.d.ts +8 -2
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +9 -3
- package/dist/transports/stdio.d.ts +15 -9
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +18 -11
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +17 -3
- package/examples/coordinate-conventions-demo.md +12 -13
- package/examples/embed-in-agent.ts +6 -7
- package/examples/generate-apartment.md +13 -15
- package/examples/photo-to-scene.md +3 -4
- package/examples/renovate-from-photos.md +6 -9
- package/package.json +19 -9
- package/dist/tools/create-level.d.ts +0 -21
- package/dist/tools/create-level.d.ts.map +0 -1
- package/dist/tools/create-level.js +0 -63
- package/dist/tools/create-wall.d.ts +0 -22
- package/dist/tools/create-wall.d.ts.map +0 -1
- package/dist/tools/create-wall.js +0 -57
- package/dist/tools/cut-opening.d.ts +0 -25
- package/dist/tools/cut-opening.d.ts.map +0 -1
- package/dist/tools/cut-opening.js +0 -61
- package/dist/tools/delete-node.d.ts +0 -19
- package/dist/tools/delete-node.d.ts.map +0 -1
- package/dist/tools/delete-node.js +0 -40
- package/dist/tools/describe-node.d.ts +0 -17
- package/dist/tools/describe-node.d.ts.map +0 -1
- package/dist/tools/describe-node.js +0 -97
- package/dist/tools/door-clearance.d.ts +0 -135
- package/dist/tools/door-clearance.d.ts.map +0 -1
- package/dist/tools/door-clearance.js +0 -334
- package/dist/tools/duplicate-level.d.ts +0 -19
- package/dist/tools/duplicate-level.d.ts.map +0 -1
- package/dist/tools/duplicate-level.js +0 -60
- package/dist/tools/find-nodes.d.ts +0 -32
- package/dist/tools/find-nodes.d.ts.map +0 -1
- package/dist/tools/find-nodes.js +0 -107
- package/dist/tools/geometry.d.ts +0 -24
- package/dist/tools/geometry.d.ts.map +0 -1
- package/dist/tools/geometry.js +0 -94
- package/dist/tools/get-node.d.ts +0 -11
- package/dist/tools/get-node.d.ts.map +0 -1
- package/dist/tools/get-node.js +0 -29
- package/dist/tools/layout-clearance.d.ts +0 -105
- package/dist/tools/layout-clearance.d.ts.map +0 -1
- package/dist/tools/layout-clearance.js +0 -190
- package/dist/tools/place-item.d.ts +0 -22
- package/dist/tools/place-item.d.ts.map +0 -1
- package/dist/tools/place-item.js +0 -89
- package/dist/tools/scene-query.d.ts +0 -62
- package/dist/tools/scene-query.d.ts.map +0 -1
- package/dist/tools/scene-query.js +0 -670
- package/dist/tools/validate-scene.d.ts +0 -19
- package/dist/tools/validate-scene.d.ts.map +0 -1
- 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":"
|
|
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"}
|
package/dist/lib/safe-fetch.js
CHANGED
|
@@ -1,83 +1,150 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
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
|
-
*
|
|
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
|
|
25
|
-
const parts = addr.split('.')
|
|
26
|
-
if (parts.length !== 4 || parts.some((
|
|
27
|
-
return
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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 ===
|
|
38
|
-
return true; //
|
|
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
|
|
87
|
+
return true; // multicast, reserved, broadcast
|
|
43
88
|
return false;
|
|
44
89
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
155
|
+
return protocolError('invalid_url', { url });
|
|
89
156
|
}
|
|
90
157
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
91
|
-
|
|
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
|
-
|
|
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,
|
|
120
|
-
*
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
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
|
-
|
|
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 (
|
|
152
|
-
|
|
272
|
+
if (status < 200 || status >= 300) {
|
|
273
|
+
res.resume();
|
|
274
|
+
protocolError('fetch_failed', {
|
|
153
275
|
url: parsed.toString(),
|
|
154
|
-
status
|
|
155
|
-
statusText: res.
|
|
276
|
+
status,
|
|
277
|
+
statusText: res.statusMessage ?? '',
|
|
156
278
|
});
|
|
157
279
|
}
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
total
|
|
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
|
|
200
|
-
contentType: res
|
|
298
|
+
buffer: Buffer.concat(chunks),
|
|
299
|
+
contentType: header(res, 'content-type'),
|
|
201
300
|
finalUrl: parsed.toString(),
|
|
202
301
|
hops,
|
|
203
302
|
};
|
|
204
303
|
}
|
|
205
|
-
|
|
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
|
|
307
|
+
if (err instanceof ProtocolError)
|
|
211
308
|
throw err;
|
|
212
|
-
if (err.name === 'AbortError') {
|
|
213
|
-
|
|
309
|
+
if (controller.signal.aborted || err.name === 'AbortError') {
|
|
310
|
+
return protocolError('fetch_timeout', { url: urlStr, timeoutMs });
|
|
214
311
|
}
|
|
215
|
-
|
|
216
|
-
|
|
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<
|
|
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,
|
|
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()
|
|
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/
|
|
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.
|