@pascal-app/mcp 0.2.0
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 +39 -0
- package/README.md +330 -0
- package/dist/bin/pascal-mcp.d.ts +3 -0
- package/dist/bin/pascal-mcp.d.ts.map +1 -0
- package/dist/bin/pascal-mcp.js +91 -0
- package/dist/bridge/index.d.ts +3 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/node-shims.d.ts +16 -0
- package/dist/bridge/node-shims.d.ts.map +1 -0
- package/dist/bridge/node-shims.js +25 -0
- package/dist/bridge/scene-bridge.d.ts +158 -0
- package/dist/bridge/scene-bridge.d.ts.map +1 -0
- package/dist/bridge/scene-bridge.js +482 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/rehydrate-site-children.d.ts +12 -0
- package/dist/lib/rehydrate-site-children.d.ts.map +1 -0
- package/dist/lib/rehydrate-site-children.js +40 -0
- package/dist/lib/safe-fetch.d.ts +19 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -0
- package/dist/lib/safe-fetch.js +221 -0
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/scene-operations.d.ts +67 -0
- package/dist/operations/scene-operations.d.ts.map +1 -0
- package/dist/operations/scene-operations.js +192 -0
- package/dist/prompts/from-brief.d.ts +11 -0
- package/dist/prompts/from-brief.d.ts.map +1 -0
- package/dist/prompts/from-brief.js +48 -0
- package/dist/prompts/index.d.ts +10 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +14 -0
- package/dist/prompts/iterate-on-feedback.d.ts +11 -0
- package/dist/prompts/iterate-on-feedback.d.ts.map +1 -0
- package/dist/prompts/iterate-on-feedback.js +41 -0
- package/dist/prompts/renovation-from-photos.d.ts +24 -0
- package/dist/prompts/renovation-from-photos.d.ts.map +1 -0
- package/dist/prompts/renovation-from-photos.js +135 -0
- package/dist/prompts/scene-guidance.d.ts +2 -0
- package/dist/prompts/scene-guidance.d.ts.map +1 -0
- package/dist/prompts/scene-guidance.js +24 -0
- package/dist/resources/agent-guide.d.ts +5 -0
- package/dist/resources/agent-guide.d.ts.map +1 -0
- package/dist/resources/agent-guide.js +85 -0
- package/dist/resources/catalog-items.d.ts +10 -0
- package/dist/resources/catalog-items.d.ts.map +1 -0
- package/dist/resources/catalog-items.js +29 -0
- package/dist/resources/constraints.d.ts +9 -0
- package/dist/resources/constraints.d.ts.map +1 -0
- package/dist/resources/constraints.js +69 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +23 -0
- package/dist/resources/scene-current.d.ts +9 -0
- package/dist/resources/scene-current.d.ts.map +1 -0
- package/dist/resources/scene-current.js +20 -0
- package/dist/resources/scene-summary.d.ts +10 -0
- package/dist/resources/scene-summary.d.ts.map +1 -0
- package/dist/resources/scene-summary.js +196 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +18 -0
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +14 -0
- package/dist/storage/slug.d.ts +21 -0
- package/dist/storage/slug.d.ts.map +1 -0
- package/dist/storage/slug.js +62 -0
- package/dist/storage/sqlite-driver.d.ts +18 -0
- package/dist/storage/sqlite-driver.d.ts.map +1 -0
- package/dist/storage/sqlite-driver.js +32 -0
- package/dist/storage/sqlite-scene-store.d.ts +51 -0
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -0
- package/dist/storage/sqlite-scene-store.js +526 -0
- package/dist/storage/types.d.ts +136 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +28 -0
- package/dist/templates/empty-studio.d.ts +8 -0
- package/dist/templates/empty-studio.d.ts.map +1 -0
- package/dist/templates/empty-studio.js +230 -0
- package/dist/templates/garden-house.d.ts +8 -0
- package/dist/templates/garden-house.d.ts.map +1 -0
- package/dist/templates/garden-house.js +242 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +20 -0
- package/dist/templates/two-bedroom.d.ts +8 -0
- package/dist/templates/two-bedroom.d.ts.map +1 -0
- package/dist/templates/two-bedroom.js +270 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/apply-patch.js +59 -0
- package/dist/tools/asset-catalog.d.ts +16 -0
- package/dist/tools/asset-catalog.d.ts.map +1 -0
- package/dist/tools/asset-catalog.js +308 -0
- package/dist/tools/check-collisions.d.ts +15 -0
- package/dist/tools/check-collisions.d.ts.map +1 -0
- package/dist/tools/check-collisions.js +61 -0
- package/dist/tools/construction-tools.d.ts +93 -0
- package/dist/tools/construction-tools.d.ts.map +1 -0
- package/dist/tools/construction-tools.js +369 -0
- package/dist/tools/create-level.d.ts +14 -0
- package/dist/tools/create-level.d.ts.map +1 -0
- package/dist/tools/create-level.js +48 -0
- package/dist/tools/create-wall.d.ts +15 -0
- package/dist/tools/create-wall.d.ts.map +1 -0
- package/dist/tools/create-wall.js +50 -0
- package/dist/tools/cut-opening.d.ts +18 -0
- package/dist/tools/cut-opening.d.ts.map +1 -0
- package/dist/tools/cut-opening.js +57 -0
- package/dist/tools/delete-node.d.ts +12 -0
- package/dist/tools/delete-node.d.ts.map +1 -0
- package/dist/tools/delete-node.js +37 -0
- package/dist/tools/describe-node.d.ts +17 -0
- package/dist/tools/describe-node.d.ts.map +1 -0
- package/dist/tools/describe-node.js +86 -0
- package/dist/tools/duplicate-level.d.ts +12 -0
- package/dist/tools/duplicate-level.d.ts.map +1 -0
- package/dist/tools/duplicate-level.js +56 -0
- package/dist/tools/errors.d.ts +20 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +25 -0
- package/dist/tools/export-glb.d.ts +10 -0
- package/dist/tools/export-glb.d.ts.map +1 -0
- package/dist/tools/export-glb.js +24 -0
- package/dist/tools/export-json.d.ts +11 -0
- package/dist/tools/export-json.d.ts.map +1 -0
- package/dist/tools/export-json.js +23 -0
- package/dist/tools/find-nodes.d.ts +32 -0
- package/dist/tools/find-nodes.d.ts.map +1 -0
- package/dist/tools/find-nodes.js +105 -0
- package/dist/tools/geometry.d.ts +24 -0
- package/dist/tools/geometry.d.ts.map +1 -0
- package/dist/tools/geometry.js +94 -0
- package/dist/tools/get-node.d.ts +11 -0
- package/dist/tools/get-node.d.ts.map +1 -0
- package/dist/tools/get-node.js +27 -0
- package/dist/tools/get-scene.d.ts +11 -0
- package/dist/tools/get-scene.d.ts.map +1 -0
- package/dist/tools/get-scene.js +26 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +64 -0
- package/dist/tools/live-sync.d.ts +11 -0
- package/dist/tools/live-sync.d.ts.map +1 -0
- package/dist/tools/live-sync.js +62 -0
- package/dist/tools/measure.d.ts +14 -0
- package/dist/tools/measure.d.ts.map +1 -0
- package/dist/tools/measure.js +137 -0
- package/dist/tools/photo-to-scene/index.d.ts +11 -0
- package/dist/tools/photo-to-scene/index.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/index.js +11 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts +26 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/photo-to-scene.js +323 -0
- package/dist/tools/place-item.d.ts +15 -0
- package/dist/tools/place-item.d.ts.map +1 -0
- package/dist/tools/place-item.js +84 -0
- package/dist/tools/redo.d.ts +11 -0
- package/dist/tools/redo.d.ts.map +1 -0
- package/dist/tools/redo.js +25 -0
- package/dist/tools/room-tools.d.ts +97 -0
- package/dist/tools/room-tools.d.ts.map +1 -0
- package/dist/tools/room-tools.js +510 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts +33 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/create-project.js +70 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts +12 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/delete-scene.js +42 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts +31 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/get-project-status.js +69 -0
- package/dist/tools/scene-lifecycle/index.d.ts +17 -0
- package/dist/tools/scene-lifecycle/index.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/index.js +29 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts +27 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/list-scenes.js +49 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts +31 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/load-scene.js +55 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts +49 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/metadata.js +68 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts +22 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/rename-scene.js +63 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts +42 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/save-scene.js +130 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts +39 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/test-utils.js +211 -0
- package/dist/tools/scene-query.d.ts +57 -0
- package/dist/tools/scene-query.d.ts.map +1 -0
- package/dist/tools/scene-query.js +645 -0
- package/dist/tools/schemas.d.ts +52 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +42 -0
- package/dist/tools/set-zone.d.ts +14 -0
- package/dist/tools/set-zone.d.ts.map +1 -0
- package/dist/tools/set-zone.js +48 -0
- package/dist/tools/templates/create-from-template.d.ts +54 -0
- package/dist/tools/templates/create-from-template.d.ts.map +1 -0
- package/dist/tools/templates/create-from-template.js +145 -0
- package/dist/tools/templates/create-house-from-brief.d.ts +35 -0
- package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -0
- package/dist/tools/templates/create-house-from-brief.js +170 -0
- package/dist/tools/templates/index.d.ts +14 -0
- package/dist/tools/templates/index.d.ts.map +1 -0
- package/dist/tools/templates/index.js +18 -0
- package/dist/tools/templates/list-templates.d.ts +18 -0
- package/dist/tools/templates/list-templates.d.ts.map +1 -0
- package/dist/tools/templates/list-templates.js +36 -0
- package/dist/tools/undo.d.ts +11 -0
- package/dist/tools/undo.d.ts.map +1 -0
- package/dist/tools/undo.js +25 -0
- package/dist/tools/validate-scene.d.ts +14 -0
- package/dist/tools/validate-scene.d.ts.map +1 -0
- package/dist/tools/validate-scene.js +24 -0
- package/dist/tools/variants/generate-variants.d.ts +30 -0
- package/dist/tools/variants/generate-variants.d.ts.map +1 -0
- package/dist/tools/variants/generate-variants.js +160 -0
- package/dist/tools/variants/index.d.ts +9 -0
- package/dist/tools/variants/index.d.ts.map +1 -0
- package/dist/tools/variants/index.js +9 -0
- package/dist/tools/variants/mutations.d.ts +18 -0
- package/dist/tools/variants/mutations.d.ts.map +1 -0
- package/dist/tools/variants/mutations.js +324 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts +33 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -0
- package/dist/tools/vision/analyze-floorplan-image.js +146 -0
- package/dist/tools/vision/analyze-room-photo.d.ts +29 -0
- package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -0
- package/dist/tools/vision/analyze-room-photo.js +130 -0
- package/dist/tools/vision/index.d.ts +12 -0
- package/dist/tools/vision/index.d.ts.map +1 -0
- package/dist/tools/vision/index.js +14 -0
- package/dist/transports/http.d.ts +36 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +203 -0
- package/dist/transports/stdio.d.ts +14 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +16 -0
- package/package.json +86 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs,
|
|
3
|
+
* but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
|
|
4
|
+
* `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
|
|
5
|
+
* cloned graph validating against `AnyNode`, re-embed the site children from
|
|
6
|
+
* the flat dict.
|
|
7
|
+
*
|
|
8
|
+
* Pure: returns a new graph without mutating the input.
|
|
9
|
+
*/
|
|
10
|
+
export function rehydrateSiteChildren(graph) {
|
|
11
|
+
const out = {
|
|
12
|
+
nodes: { ...graph.nodes },
|
|
13
|
+
rootNodeIds: [...graph.rootNodeIds],
|
|
14
|
+
...(graph.collections ? { collections: graph.collections } : {}),
|
|
15
|
+
};
|
|
16
|
+
for (const [id, node] of Object.entries(out.nodes)) {
|
|
17
|
+
if (node.type !== 'site')
|
|
18
|
+
continue;
|
|
19
|
+
const childrenField = node.children;
|
|
20
|
+
if (!Array.isArray(childrenField))
|
|
21
|
+
continue;
|
|
22
|
+
const rehydrated = [];
|
|
23
|
+
for (const child of childrenField) {
|
|
24
|
+
if (typeof child === 'string') {
|
|
25
|
+
const target = out.nodes[child];
|
|
26
|
+
if (target && (target.type === 'building' || target.type === 'item')) {
|
|
27
|
+
rehydrated.push(target);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (child && typeof child === 'object' && 'id' in child) {
|
|
31
|
+
rehydrated.push(child);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
out.nodes[id] = {
|
|
35
|
+
...node,
|
|
36
|
+
children: rehydrated,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type SafeFetchOptions = {
|
|
2
|
+
maxBytes?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
/** Request `Accept` header to send. */
|
|
5
|
+
accept?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SafeFetchResult = {
|
|
8
|
+
buffer: Buffer;
|
|
9
|
+
contentType: string | null;
|
|
10
|
+
finalUrl: string;
|
|
11
|
+
hops: string[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* SSRF-safe fetch that follows redirects manually, revalidating the host
|
|
15
|
+
* allowlist + private-IP check on every hop. Throws `McpError` for blocked
|
|
16
|
+
* URLs, non-2xx responses, oversize bodies, or timeouts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function safeFetch(urlStr: string, opts?: SafeFetchOptions): Promise<SafeFetchResult>;
|
|
19
|
+
//# sourceMappingURL=safe-fetch.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from 'node:net';
|
|
2
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* SSRF-safe fetch for user-supplied URLs (image URLs in vision tools).
|
|
5
|
+
*
|
|
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)
|
|
14
|
+
*
|
|
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
|
+
*/
|
|
21
|
+
const DEFAULT_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
23
|
+
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
|
|
31
|
+
if (a === 10)
|
|
32
|
+
return true; // 10.0.0.0/8 private
|
|
33
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
34
|
+
return true; // 172.16.0.0/12 private
|
|
35
|
+
if (a === 192 && b === 168)
|
|
36
|
+
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
|
|
41
|
+
if (a >= 224)
|
|
42
|
+
return true; // multicast / reserved
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
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);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
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' ||
|
|
73
|
+
lower.endsWith('.localhost') ||
|
|
74
|
+
lower === 'broadcasthost' ||
|
|
75
|
+
lower.endsWith('.local') ||
|
|
76
|
+
lower.endsWith('.internal') ||
|
|
77
|
+
lower.endsWith('.corp')) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function assertAllowedUrl(url) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(url);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new McpError(ErrorCode.InvalidParams, 'invalid_url', { url });
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
91
|
+
throw new McpError(ErrorCode.InvalidParams, 'url_scheme_not_allowed', {
|
|
92
|
+
url,
|
|
93
|
+
protocol: parsed.protocol,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
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
|
+
const allowEnv = process.env.PASCAL_ALLOWED_ASSET_ORIGINS;
|
|
104
|
+
if (allowEnv) {
|
|
105
|
+
const origins = allowEnv
|
|
106
|
+
.split(',')
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
if (!origins.includes(parsed.origin)) {
|
|
110
|
+
throw new McpError(ErrorCode.InvalidParams, 'url_origin_not_allowlisted', {
|
|
111
|
+
url,
|
|
112
|
+
origin: parsed.origin,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
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.
|
|
122
|
+
*/
|
|
123
|
+
export async function safeFetch(urlStr, opts = {}) {
|
|
124
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
125
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
126
|
+
const hops = [];
|
|
127
|
+
let current = urlStr;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
132
|
+
const parsed = assertAllowedUrl(current);
|
|
133
|
+
hops.push(parsed.toString());
|
|
134
|
+
const res = await fetch(parsed, {
|
|
135
|
+
redirect: 'manual',
|
|
136
|
+
signal: controller.signal,
|
|
137
|
+
headers: opts.accept ? { Accept: opts.accept } : undefined,
|
|
138
|
+
});
|
|
139
|
+
// Manual redirect handling
|
|
140
|
+
if (res.status >= 300 && res.status < 400) {
|
|
141
|
+
const location = res.headers.get('location');
|
|
142
|
+
if (!location) {
|
|
143
|
+
throw new McpError(ErrorCode.InvalidParams, 'redirect_without_location', {
|
|
144
|
+
url: parsed.toString(),
|
|
145
|
+
status: res.status,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
current = new URL(location, parsed).toString();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
throw new McpError(ErrorCode.InvalidParams, 'fetch_failed', {
|
|
153
|
+
url: parsed.toString(),
|
|
154
|
+
status: res.status,
|
|
155
|
+
statusText: res.statusText,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Enforce Content-Length up front if present.
|
|
159
|
+
const declared = Number(res.headers.get('content-length'));
|
|
160
|
+
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
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const chunks = [];
|
|
175
|
+
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);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
buffer: Buffer.concat(chunks.map((c) => Buffer.from(c))),
|
|
200
|
+
contentType: res.headers.get('content-type'),
|
|
201
|
+
finalUrl: parsed.toString(),
|
|
202
|
+
hops,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
throw new McpError(ErrorCode.InvalidParams, 'too_many_redirects', {
|
|
206
|
+
hops: hops.slice(0, MAX_REDIRECTS + 1),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (err instanceof McpError)
|
|
211
|
+
throw err;
|
|
212
|
+
if (err.name === 'AbortError') {
|
|
213
|
+
throw new McpError(ErrorCode.InvalidParams, 'fetch_timeout', { url: urlStr, timeoutMs });
|
|
214
|
+
}
|
|
215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
216
|
+
throw new McpError(ErrorCode.InvalidParams, 'fetch_error', { url: urlStr, message });
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/operations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSceneOperations, } from './scene-operations';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
|
|
2
|
+
import type { AnyNode, AnyNodeId, AnyNodeType } from '@pascal-app/core/schema';
|
|
3
|
+
import type { ActiveSceneMeta, Patch, SceneBridge, ValidationResult } from '../bridge/scene-bridge';
|
|
4
|
+
import type { ProjectCreateOptions, ProjectStatus, SceneEvent, SceneEventAppendOptions, SceneEventListOptions, SceneListOptions, SceneMeta, SceneMutateOptions, SceneSaveOptions, SceneStore, SceneWithGraph } from '../storage/types';
|
|
5
|
+
export type CreateSceneOperationsOptions = {
|
|
6
|
+
bridge?: SceneBridge;
|
|
7
|
+
store?: SceneStore;
|
|
8
|
+
};
|
|
9
|
+
export interface SceneOperations {
|
|
10
|
+
readonly hasBridge: boolean;
|
|
11
|
+
readonly hasStore: boolean;
|
|
12
|
+
readonly hasSceneEvents: boolean;
|
|
13
|
+
readonly canAppendSceneEvents: boolean;
|
|
14
|
+
readonly canListSceneEvents: boolean;
|
|
15
|
+
readonly canCreateProject: boolean;
|
|
16
|
+
readonly canGetProjectStatus: boolean;
|
|
17
|
+
readonly storeBackend: SceneStore['backend'] | null;
|
|
18
|
+
setActiveScene(meta: ActiveSceneMeta): void;
|
|
19
|
+
getActiveScene(): ActiveSceneMeta | null;
|
|
20
|
+
clearActiveScene(): void;
|
|
21
|
+
loadDefault(): void;
|
|
22
|
+
setScene(nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[]): void;
|
|
23
|
+
exportJSON(): SceneGraph & {
|
|
24
|
+
collections: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
exportSceneGraph(): SceneGraph;
|
|
27
|
+
loadJSON(json: string | SceneGraph): void;
|
|
28
|
+
getNode(id: AnyNodeId): AnyNode | null;
|
|
29
|
+
getNodes(): Record<AnyNodeId, AnyNode>;
|
|
30
|
+
getRootNodeIds(): AnyNodeId[];
|
|
31
|
+
getChildren(parentId: AnyNodeId): AnyNode[];
|
|
32
|
+
getAncestry(id: AnyNodeId): AnyNode[];
|
|
33
|
+
findNodes(filter: {
|
|
34
|
+
type?: AnyNodeType;
|
|
35
|
+
parentId?: AnyNodeId | null;
|
|
36
|
+
levelId?: AnyNodeId;
|
|
37
|
+
}): AnyNode[];
|
|
38
|
+
resolveLevelId(id: AnyNodeId): AnyNodeId | null;
|
|
39
|
+
createNode(node: AnyNode, parentId?: AnyNodeId): AnyNodeId;
|
|
40
|
+
updateNode(id: AnyNodeId, data: Partial<AnyNode>): void;
|
|
41
|
+
deleteNode(id: AnyNodeId, cascade?: boolean): string[];
|
|
42
|
+
applyPatch(patches: Patch[]): {
|
|
43
|
+
appliedOps: number;
|
|
44
|
+
deletedIds: AnyNodeId[];
|
|
45
|
+
createdIds: AnyNodeId[];
|
|
46
|
+
};
|
|
47
|
+
undo(steps?: number): number;
|
|
48
|
+
redo(steps?: number): number;
|
|
49
|
+
validateScene(): ValidationResult;
|
|
50
|
+
flushDirty(): string[];
|
|
51
|
+
getHistory(): {
|
|
52
|
+
pastCount: number;
|
|
53
|
+
futureCount: number;
|
|
54
|
+
};
|
|
55
|
+
clearHistory(): void;
|
|
56
|
+
createProject(options: ProjectCreateOptions): Promise<ProjectStatus>;
|
|
57
|
+
getProjectStatus(id: string): Promise<ProjectStatus | null>;
|
|
58
|
+
saveScene(options: SceneSaveOptions): Promise<SceneMeta>;
|
|
59
|
+
loadStoredScene(id: string): Promise<SceneWithGraph | null>;
|
|
60
|
+
listScenes(options?: SceneListOptions): Promise<SceneMeta[]>;
|
|
61
|
+
deleteStoredScene(id: string, options?: SceneMutateOptions): Promise<boolean>;
|
|
62
|
+
renameStoredScene(id: string, newName: string, options?: SceneMutateOptions): Promise<SceneMeta>;
|
|
63
|
+
appendSceneEvent(options: SceneEventAppendOptions): Promise<SceneEvent | null>;
|
|
64
|
+
listSceneEvents(id: string, options?: SceneEventListOptions): Promise<SceneEvent[]>;
|
|
65
|
+
}
|
|
66
|
+
export declare function createSceneOperations(options: CreateSceneOperationsOptions): SceneOperations;
|
|
67
|
+
//# sourceMappingURL=scene-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene-operations.d.ts","sourceRoot":"","sources":["../../src/operations/scene-operations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export function createSceneOperations(options) {
|
|
2
|
+
return new SceneOperationsFacade(options);
|
|
3
|
+
}
|
|
4
|
+
class SceneOperationsFacade {
|
|
5
|
+
#bridge;
|
|
6
|
+
#store;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.#bridge = options.bridge;
|
|
9
|
+
this.#store = options.store;
|
|
10
|
+
}
|
|
11
|
+
get hasBridge() {
|
|
12
|
+
return this.#bridge !== undefined;
|
|
13
|
+
}
|
|
14
|
+
get hasStore() {
|
|
15
|
+
return this.#store !== undefined;
|
|
16
|
+
}
|
|
17
|
+
get hasSceneEvents() {
|
|
18
|
+
return this.canAppendSceneEvents && this.canListSceneEvents;
|
|
19
|
+
}
|
|
20
|
+
get canAppendSceneEvents() {
|
|
21
|
+
return typeof this.#store?.appendSceneEvent === 'function';
|
|
22
|
+
}
|
|
23
|
+
get canListSceneEvents() {
|
|
24
|
+
return typeof this.#store?.listSceneEvents === 'function';
|
|
25
|
+
}
|
|
26
|
+
get canCreateProject() {
|
|
27
|
+
return typeof this.#store?.createProject === 'function';
|
|
28
|
+
}
|
|
29
|
+
get canGetProjectStatus() {
|
|
30
|
+
return typeof this.#store?.getProjectStatus === 'function';
|
|
31
|
+
}
|
|
32
|
+
get storeBackend() {
|
|
33
|
+
return this.#store?.backend ?? null;
|
|
34
|
+
}
|
|
35
|
+
setActiveScene(meta) {
|
|
36
|
+
this.requireBridge().setActiveScene(meta);
|
|
37
|
+
}
|
|
38
|
+
getActiveScene() {
|
|
39
|
+
return this.requireBridge().getActiveScene();
|
|
40
|
+
}
|
|
41
|
+
clearActiveScene() {
|
|
42
|
+
this.requireBridge().clearActiveScene();
|
|
43
|
+
}
|
|
44
|
+
loadDefault() {
|
|
45
|
+
this.requireBridge().loadDefault();
|
|
46
|
+
}
|
|
47
|
+
setScene(nodes, rootNodeIds) {
|
|
48
|
+
this.requireBridge().setScene(nodes, rootNodeIds);
|
|
49
|
+
}
|
|
50
|
+
exportJSON() {
|
|
51
|
+
return this.requireBridge().exportJSON();
|
|
52
|
+
}
|
|
53
|
+
exportSceneGraph() {
|
|
54
|
+
const exported = this.exportJSON();
|
|
55
|
+
return {
|
|
56
|
+
nodes: exported.nodes,
|
|
57
|
+
rootNodeIds: exported.rootNodeIds,
|
|
58
|
+
collections: exported.collections,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
loadJSON(json) {
|
|
62
|
+
this.requireBridge().loadJSON(json);
|
|
63
|
+
}
|
|
64
|
+
getNode(id) {
|
|
65
|
+
return this.requireBridge().getNode(id);
|
|
66
|
+
}
|
|
67
|
+
getNodes() {
|
|
68
|
+
return this.requireBridge().getNodes();
|
|
69
|
+
}
|
|
70
|
+
getRootNodeIds() {
|
|
71
|
+
return this.requireBridge().getRootNodeIds();
|
|
72
|
+
}
|
|
73
|
+
getChildren(parentId) {
|
|
74
|
+
return this.requireBridge().getChildren(parentId);
|
|
75
|
+
}
|
|
76
|
+
getAncestry(id) {
|
|
77
|
+
return this.requireBridge().getAncestry(id);
|
|
78
|
+
}
|
|
79
|
+
findNodes(filter) {
|
|
80
|
+
return this.requireBridge().findNodes(filter);
|
|
81
|
+
}
|
|
82
|
+
resolveLevelId(id) {
|
|
83
|
+
return this.requireBridge().resolveLevelId(id);
|
|
84
|
+
}
|
|
85
|
+
createNode(node, parentId) {
|
|
86
|
+
return this.requireBridge().createNode(node, parentId);
|
|
87
|
+
}
|
|
88
|
+
updateNode(id, data) {
|
|
89
|
+
this.requireBridge().updateNode(id, data);
|
|
90
|
+
}
|
|
91
|
+
deleteNode(id, cascade) {
|
|
92
|
+
return this.requireBridge().deleteNode(id, cascade);
|
|
93
|
+
}
|
|
94
|
+
applyPatch(patches) {
|
|
95
|
+
return this.requireBridge().applyPatch(patches);
|
|
96
|
+
}
|
|
97
|
+
undo(steps) {
|
|
98
|
+
return this.requireBridge().undo(steps);
|
|
99
|
+
}
|
|
100
|
+
redo(steps) {
|
|
101
|
+
return this.requireBridge().redo(steps);
|
|
102
|
+
}
|
|
103
|
+
validateScene() {
|
|
104
|
+
return this.requireBridge().validateScene();
|
|
105
|
+
}
|
|
106
|
+
flushDirty() {
|
|
107
|
+
return this.requireBridge().flushDirty();
|
|
108
|
+
}
|
|
109
|
+
getHistory() {
|
|
110
|
+
return this.requireBridge().getHistory();
|
|
111
|
+
}
|
|
112
|
+
clearHistory() {
|
|
113
|
+
this.requireBridge().clearHistory();
|
|
114
|
+
}
|
|
115
|
+
async createProject(options) {
|
|
116
|
+
const store = this.requireStore();
|
|
117
|
+
if (!store.createProject) {
|
|
118
|
+
throw new Error('create_project_unavailable');
|
|
119
|
+
}
|
|
120
|
+
return store.createProject(options);
|
|
121
|
+
}
|
|
122
|
+
async getProjectStatus(id) {
|
|
123
|
+
const store = this.requireStore();
|
|
124
|
+
if (store.getProjectStatus) {
|
|
125
|
+
return store.getProjectStatus(id);
|
|
126
|
+
}
|
|
127
|
+
const scene = await store.load(id);
|
|
128
|
+
if (!scene)
|
|
129
|
+
return null;
|
|
130
|
+
const editorUrl = scene.editorUrl ?? `/editor/${scene.id}`;
|
|
131
|
+
return {
|
|
132
|
+
id: scene.id,
|
|
133
|
+
projectId: scene.projectId ?? scene.id,
|
|
134
|
+
name: scene.name,
|
|
135
|
+
editorUrl,
|
|
136
|
+
url: editorUrl,
|
|
137
|
+
ownerId: scene.ownerId,
|
|
138
|
+
thumbnailUrl: scene.thumbnailUrl,
|
|
139
|
+
publishedVersion: scene.published === false ? null : scene.version,
|
|
140
|
+
latestVersion: scene.version,
|
|
141
|
+
draftVersion: null,
|
|
142
|
+
browserVisibleVersion: scene.version,
|
|
143
|
+
version: scene.version,
|
|
144
|
+
isEmpty: scene.nodeCount === 0,
|
|
145
|
+
sizeBytes: scene.sizeBytes,
|
|
146
|
+
nodeCount: scene.nodeCount,
|
|
147
|
+
graphHash: scene.graphHash ?? null,
|
|
148
|
+
createdAt: scene.createdAt,
|
|
149
|
+
updatedAt: scene.updatedAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async saveScene(options) {
|
|
153
|
+
return this.requireStore().save(options);
|
|
154
|
+
}
|
|
155
|
+
async loadStoredScene(id) {
|
|
156
|
+
return this.requireStore().load(id);
|
|
157
|
+
}
|
|
158
|
+
async listScenes(options) {
|
|
159
|
+
return this.requireStore().list(options);
|
|
160
|
+
}
|
|
161
|
+
async deleteStoredScene(id, options) {
|
|
162
|
+
return this.requireStore().delete(id, options);
|
|
163
|
+
}
|
|
164
|
+
async renameStoredScene(id, newName, options) {
|
|
165
|
+
return this.requireStore().rename(id, newName, options);
|
|
166
|
+
}
|
|
167
|
+
async appendSceneEvent(options) {
|
|
168
|
+
const append = this.requireStore().appendSceneEvent;
|
|
169
|
+
if (!append)
|
|
170
|
+
return null;
|
|
171
|
+
return append(options);
|
|
172
|
+
}
|
|
173
|
+
async listSceneEvents(id, options) {
|
|
174
|
+
const list = this.requireStore().listSceneEvents;
|
|
175
|
+
if (!list) {
|
|
176
|
+
throw new Error('scene_events_unavailable');
|
|
177
|
+
}
|
|
178
|
+
return list(id, options);
|
|
179
|
+
}
|
|
180
|
+
requireBridge() {
|
|
181
|
+
if (!this.#bridge) {
|
|
182
|
+
throw new Error('scene_bridge_unavailable');
|
|
183
|
+
}
|
|
184
|
+
return this.#bridge;
|
|
185
|
+
}
|
|
186
|
+
requireStore() {
|
|
187
|
+
if (!this.#store) {
|
|
188
|
+
throw new Error('scene_store_unavailable');
|
|
189
|
+
}
|
|
190
|
+
return this.#store;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { SceneOperations } from '../operations';
|
|
3
|
+
/**
|
|
4
|
+
* Build the user-facing prompt text for `from_brief`. Pure function for testability.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildFromBriefPrompt(args: {
|
|
7
|
+
brief: string;
|
|
8
|
+
constraints?: string | undefined;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function registerFromBrief(server: McpServer, _bridge: SceneOperations): void;
|
|
11
|
+
//# sourceMappingURL=from-brief.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-brief.d.ts","sourceRoot":"","sources":["../../src/prompts/from-brief.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAmBpD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACjC,GAAG,MAAM,CAWT;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAwBnF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SCENE_DESIGN_GUIDANCE } from './scene-guidance';
|
|
3
|
+
const PREAMBLE = [
|
|
4
|
+
'You are a Pascal 3D scene designer.',
|
|
5
|
+
'You have access to semantic scene tools and the lower-level `apply_patch` tool. Prefer semantic construction/room/opening/furnishing tools for architectural work, and use `apply_patch` for bulk graph edits that need exact control.',
|
|
6
|
+
'If the user asks for a new project, call `create_project` before building. Use `create_house_from_brief` for a fast starter, then refine with semantic tools. Semantic tools update the browser-visible draft; call `save_scene` with `saveMode: "checkpoint"` only for meaningful milestones, then call `verify_scene` and `get_project_status`, and return the final `editorUrl`.',
|
|
7
|
+
'Build incrementally with visible progress. Starting from an empty scene, first create/load a Site and Building, then create occupied Levels and `create_story_shell` once per story before detailed rooms, openings, furniture, a dedicated roof level via `create_roof`, and landscaping.',
|
|
8
|
+
'Respect these invariants:',
|
|
9
|
+
' - Levels live under a Building.',
|
|
10
|
+
' - Walls, fences, zones, slabs, ceilings, roofs, stairs live under a Level.',
|
|
11
|
+
' - Multi-story exterior walls are per-level story walls; never make lower-level walls taller to stand in for upper-level walls.',
|
|
12
|
+
' - Doors and windows live under a Wall (parentId = wallId).',
|
|
13
|
+
' - Floor items live under a Level; wall/ceiling-attached items live under their target Wall or Ceiling. Do not place items directly under the Site node.',
|
|
14
|
+
'Use realistic dimensions in meters. Keep wall thickness small (0.1–0.3 m) and ceiling height 2.4–3.0 m unless the brief dictates otherwise.',
|
|
15
|
+
SCENE_DESIGN_GUIDANCE,
|
|
16
|
+
'Respond ONLY with tool calls. Do not produce verbose narrative or prose; keep any explanations in short tool-call arguments.',
|
|
17
|
+
].join('\n');
|
|
18
|
+
/**
|
|
19
|
+
* Build the user-facing prompt text for `from_brief`. Pure function for testability.
|
|
20
|
+
*/
|
|
21
|
+
export function buildFromBriefPrompt(args) {
|
|
22
|
+
const parts = [PREAMBLE, '', '## Brief', args.brief.trim()];
|
|
23
|
+
if (args.constraints && args.constraints.trim().length > 0) {
|
|
24
|
+
parts.push('', '## Constraints', args.constraints.trim());
|
|
25
|
+
}
|
|
26
|
+
parts.push('', '## Task', 'Produce tool calls that realise the brief within the stated constraints. For a new project, call create_project first. Use create_house_from_brief for a fast starter or prefer create_story_shell/create_room/add_door/add_window/create_stair_between_levels/create_roof/furnish_room for architectural layout, use apply_patch for exact bulk graph work, call save_scene with saveMode: "checkpoint" only when the design reaches a meaningful milestone, then call validate_scene, verify_scene, and get_project_status.');
|
|
27
|
+
return parts.join('\n');
|
|
28
|
+
}
|
|
29
|
+
export function registerFromBrief(server, _bridge) {
|
|
30
|
+
server.registerPrompt('from_brief', {
|
|
31
|
+
title: 'Generate a Pascal scene from a brief',
|
|
32
|
+
description: 'Produces a plan of apply_patch calls to create a scene from a natural-language brief.',
|
|
33
|
+
argsSchema: {
|
|
34
|
+
brief: z.string(),
|
|
35
|
+
constraints: z.string().optional(),
|
|
36
|
+
},
|
|
37
|
+
}, async ({ brief, constraints }) => ({
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
role: 'user',
|
|
41
|
+
content: {
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: buildFromBriefPrompt({ brief, constraints }),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { SceneOperations } from '../operations';
|
|
3
|
+
/**
|
|
4
|
+
* Registers all MCP prompts exposed by `@pascal-app/mcp`:
|
|
5
|
+
* - `from_brief` — generate a scene from a natural-language brief
|
|
6
|
+
* - `iterate_on_feedback` — minimal-diff patches from user feedback
|
|
7
|
+
* - `renovation_from_photos` — photo-driven renovation plan via vision tools
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerPrompts(server: McpServer, operations: SceneOperations): void;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,GAAG,IAAI,CAIpF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { registerFromBrief } from './from-brief';
|
|
2
|
+
import { registerIterateOnFeedback } from './iterate-on-feedback';
|
|
3
|
+
import { registerRenovationFromPhotos } from './renovation-from-photos';
|
|
4
|
+
/**
|
|
5
|
+
* Registers all MCP prompts exposed by `@pascal-app/mcp`:
|
|
6
|
+
* - `from_brief` — generate a scene from a natural-language brief
|
|
7
|
+
* - `iterate_on_feedback` — minimal-diff patches from user feedback
|
|
8
|
+
* - `renovation_from_photos` — photo-driven renovation plan via vision tools
|
|
9
|
+
*/
|
|
10
|
+
export function registerPrompts(server, operations) {
|
|
11
|
+
registerFromBrief(server, operations);
|
|
12
|
+
registerIterateOnFeedback(server, operations);
|
|
13
|
+
registerRenovationFromPhotos(server, operations);
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { SceneOperations } from '../operations';
|
|
3
|
+
/**
|
|
4
|
+
* Build the user-facing prompt text for `iterate_on_feedback`.
|
|
5
|
+
* Pure function for testability.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildIterateOnFeedbackPrompt(args: {
|
|
8
|
+
feedback: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function registerIterateOnFeedback(server: McpServer, _bridge: SceneOperations): void;
|
|
11
|
+
//# sourceMappingURL=iterate-on-feedback.d.ts.map
|