@pascal-app/mcp 1.0.0-beta.4 → 1.0.0-beta.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 (45) hide show
  1. package/README.md +56 -44
  2. package/dist/bin/pascal-mcp.d.ts +1 -1
  3. package/dist/bin/pascal-mcp.js +19 -12
  4. package/dist/bridge/scene-bridge.d.ts +6 -2
  5. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  6. package/dist/bridge/scene-bridge.js +21 -7
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/operations/scene-operations.js +1 -0
  11. package/dist/prompts/from-brief.d.ts.map +1 -1
  12. package/dist/prompts/from-brief.js +4 -3
  13. package/dist/resources/agent-guide.d.ts.map +1 -1
  14. package/dist/resources/agent-guide.js +2 -0
  15. package/dist/server.d.ts.map +1 -1
  16. package/dist/server.js +3 -2
  17. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  18. package/dist/storage/sqlite-scene-store.js +10 -1
  19. package/dist/templates/garden-house.d.ts.map +1 -1
  20. package/dist/templates/garden-house.js +4 -1
  21. package/dist/tools/check-collisions.d.ts.map +1 -1
  22. package/dist/tools/check-collisions.js +23 -35
  23. package/dist/tools/door-clearance.d.ts +119 -0
  24. package/dist/tools/door-clearance.d.ts.map +1 -0
  25. package/dist/tools/door-clearance.js +240 -0
  26. package/dist/tools/layout-clearance.d.ts +103 -0
  27. package/dist/tools/layout-clearance.d.ts.map +1 -0
  28. package/dist/tools/layout-clearance.js +186 -0
  29. package/dist/tools/measure.d.ts.map +1 -1
  30. package/dist/tools/measure.js +6 -2
  31. package/dist/tools/room-tools.d.ts.map +1 -1
  32. package/dist/tools/room-tools.js +90 -34
  33. package/dist/tools/scene-query.d.ts +1 -0
  34. package/dist/tools/scene-query.d.ts.map +1 -1
  35. package/dist/tools/scene-query.js +6 -0
  36. package/dist/tools/templates/create-from-template.d.ts +2 -0
  37. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  38. package/dist/tools/templates/create-from-template.js +2 -0
  39. package/dist/transports/http.d.ts +6 -1
  40. package/dist/transports/http.d.ts.map +1 -1
  41. package/dist/transports/http.js +52 -9
  42. package/dist/version.d.ts +2 -0
  43. package/dist/version.d.ts.map +1 -0
  44. package/dist/version.js +3 -0
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -3,47 +3,68 @@
3
3
  Model Context Protocol server for the Pascal 3D editor. Drives the
4
4
  `@pascal-app/core` scene graph from any MCP-compatible AI host.
5
5
 
6
- The server runs headlessly in Bun with no browser, WebGPU, React, or external
7
- database service. It exposes the same scene mutations used by the editor UI
8
- (create walls, place items, cut openings, undo, etc.) as MCP tools, resources,
9
- and prompts.
6
+ For the hosted Pascal MCP endpoint and copy-ready setup for Claude Code, Codex,
7
+ Cursor, and OpenClaw, read [Connect an AI agent](https://editor.pascal.app/docs/developers/mcp).
8
+ The hosted endpoint edits projects in a Pascal account; this package is the
9
+ open-source, local server for custom hosts and local scene storage.
10
10
 
11
- ## Install
11
+ The server runs headlessly in Node.js 22.13 or newer or Bun, with no browser,
12
+ WebGPU, React, or external database service. It exposes the same scene mutations used
13
+ by the editor UI (create walls, place items, cut openings, undo, etc.) as MCP tools,
14
+ resources, and prompts.
15
+
16
+ ## Recommended local setup
17
+
18
+ For a local editor and MCP that share projects automatically, install the Pascal CLI:
19
+
20
+ ```bash
21
+ npx @pascal-app/cli editor
22
+ pascal mcp setup codex
23
+ ```
24
+
25
+ `pascal editor` starts the editor and an authenticated MCP service together.
26
+ `pascal mcp connect` is a stable stdio connector that discovers the dynamic loopback
27
+ port, so MCP client configuration contains neither a changing port nor a secret.
28
+
29
+ Use this package directly when embedding the MCP server, supplying a custom store, or
30
+ running MCP without the Pascal editor.
31
+
32
+ ## Install the package directly
12
33
 
13
34
  ```bash
14
35
  bun add @pascal-app/mcp
15
36
  ```
16
37
 
17
- `@pascal-app/core` is a peer dependency; Bun workspaces resolve it automatically.
18
- The MCP CLI is intended to run with Bun. When the storage package is consumed by
19
- the Next.js editor server, it opens the same local database through Node's
20
- built-in SQLite driver.
38
+ `@pascal-app/core` is a peer dependency. The local store uses Bun SQLite under Bun and
39
+ Node's built-in SQLite driver under Node.js.
21
40
 
22
41
  ## Quick start
23
42
 
24
43
  Launch the server over stdio in one line:
25
44
 
26
45
  ```bash
27
- bunx pascal-mcp
46
+ bunx @pascal-app/mcp
47
+ # or
48
+ npm exec --package=@pascal-app/mcp -- pascal-mcp
28
49
  ```
29
50
 
30
51
  Load an initial scene from disk:
31
52
 
32
53
  ```bash
33
- pascal-mcp --stdio --scene ./my-scene.json
54
+ bunx @pascal-app/mcp --stdio --scene ./my-scene.json
34
55
  ```
35
56
 
36
57
  Expose it over loopback HTTP:
37
58
 
38
59
  ```bash
39
- pascal-mcp --http --port 8787
60
+ bunx @pascal-app/mcp --http --port 8787
40
61
  ```
41
62
 
42
63
  Binding a non-loopback host requires a bearer token:
43
64
 
44
65
  ```bash
45
66
  PASCAL_MCP_HTTP_TOKEN="$(openssl rand -hex 32)" \
46
- pascal-mcp --http --host 0.0.0.0 --port 8787 --cors-origin https://editor.example
67
+ bunx @pascal-app/mcp --http --host 0.0.0.0 --port 8787 --cors-origin https://editor.example
47
68
  ```
48
69
 
49
70
  ## Local scene storage
@@ -89,7 +110,10 @@ another MCP process saved a newer version first, the MCP tool returns
89
110
  `live_sync_version_conflict`; reload the scene with `load_scene` before
90
111
  continuing.
91
112
 
92
- ## Claude Desktop config
113
+ ## Managed CLI client configuration
114
+
115
+ The recommended JSON configuration for Claude Desktop, Cursor, and compatible clients
116
+ is:
93
117
 
94
118
  Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
95
119
  (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
@@ -98,25 +122,19 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
98
122
  {
99
123
  "mcpServers": {
100
124
  "pascal": {
101
- "command": "bunx",
102
- "args": ["pascal-mcp"],
103
- "env": {
104
- "PASCAL_DATA_DIR": "/Users/you/.pascal/data"
105
- }
125
+ "command": "pascal",
126
+ "args": ["mcp", "connect"]
106
127
  }
107
128
  }
108
129
  }
109
130
  ```
110
131
 
111
- If `bunx` is not on your PATH, point `command` at the absolute path to `bun`
112
- and pass the built `dist/bin/pascal-mcp.js` file as the first arg.
113
-
114
- ## Claude Code config
132
+ ### Claude Code
115
133
 
116
134
  Via the CLI:
117
135
 
118
136
  ```bash
119
- claude mcp add pascal bunx pascal-mcp
137
+ pascal mcp setup claude
120
138
  ```
121
139
 
122
140
  Or add to `.mcp.json` at the repo root:
@@ -125,24 +143,21 @@ Or add to `.mcp.json` at the repo root:
125
143
  {
126
144
  "mcpServers": {
127
145
  "pascal": {
128
- "command": "bunx",
129
- "args": ["pascal-mcp"],
130
- "env": {
131
- "PASCAL_DATA_DIR": "/Users/you/.pascal/data"
132
- }
146
+ "command": "pascal",
147
+ "args": ["mcp", "connect"]
133
148
  }
134
149
  }
135
150
  }
136
151
  ```
137
152
 
138
- For local workspace testing before publish, build first and point Claude Code at
139
- the built binary:
153
+ For package-development testing without the managed CLI, build first and point Claude
154
+ Code at the built binary:
140
155
 
141
156
  ```json
142
157
  {
143
158
  "mcpServers": {
144
159
  "pascal": {
145
- "command": "bun",
160
+ "command": "node",
146
161
  "args": ["/absolute/path/to/editor/packages/mcp/dist/bin/pascal-mcp.js"],
147
162
  "env": {
148
163
  "PASCAL_DATA_DIR": "/Users/you/.pascal/data"
@@ -152,12 +167,12 @@ the built binary:
152
167
  }
153
168
  ```
154
169
 
155
- ## Codex CLI config
170
+ ### Codex CLI
156
171
 
157
172
  Via the CLI:
158
173
 
159
174
  ```bash
160
- codex mcp add pascal --env PASCAL_DATA_DIR="$HOME/.pascal/data" -- bunx pascal-mcp
175
+ pascal mcp setup codex
161
176
  ```
162
177
 
163
178
  For local workspace testing before publish:
@@ -166,21 +181,21 @@ For local workspace testing before publish:
166
181
  bun run --cwd packages/mcp build
167
182
  codex mcp add pascal-dev \
168
183
  --env PASCAL_DATA_DIR="$HOME/.pascal/data" \
169
- -- bun "$PWD/packages/mcp/dist/bin/pascal-mcp.js"
184
+ -- node "$PWD/packages/mcp/dist/bin/pascal-mcp.js"
170
185
  ```
171
186
 
172
187
  This writes an entry like this to `~/.codex/config.toml`:
173
188
 
174
189
  ```toml
175
190
  [mcp_servers.pascal-dev]
176
- command = "bun"
191
+ command = "node"
177
192
  args = ["/absolute/path/to/editor/packages/mcp/dist/bin/pascal-mcp.js"]
178
193
 
179
194
  [mcp_servers.pascal-dev.env]
180
195
  PASCAL_DATA_DIR = "/Users/you/.pascal/data"
181
196
  ```
182
197
 
183
- ## Cursor config
198
+ ### Cursor config
184
199
 
185
200
  In Cursor settings (`settings.json`):
186
201
 
@@ -188,11 +203,8 @@ In Cursor settings (`settings.json`):
188
203
  {
189
204
  "mcp.servers": {
190
205
  "pascal": {
191
- "command": "bunx",
192
- "args": ["pascal-mcp"],
193
- "env": {
194
- "PASCAL_DATA_DIR": "/Users/you/.pascal/data"
195
- }
206
+ "command": "pascal",
207
+ "args": ["mcp", "connect"]
196
208
  }
197
209
  }
198
210
  }
@@ -200,7 +212,7 @@ In Cursor settings (`settings.json`):
200
212
 
201
213
  ## Programmatic use
202
214
 
203
- Embed the server in your own Bun process using the in-memory transport. The
215
+ Embed the server in your own Node.js or Bun process using the in-memory transport. The
204
216
  example below runs a full client/server pair inside a single script — useful
205
217
  for agent frameworks and tests.
206
218
 
@@ -272,7 +284,7 @@ external-coordinate gotcha — lives in
272
284
  [`examples/coordinate-conventions-demo.md`](./examples/coordinate-conventions-demo.md)
273
285
  and [`examples/coordinate-conventions-demo.json`](./examples/coordinate-conventions-demo.json).
274
286
  Load the JSON with
275
- `pascal-mcp --stdio --scene examples/coordinate-conventions-demo.json`.
287
+ `bunx @pascal-app/mcp --stdio --scene examples/coordinate-conventions-demo.json`.
276
288
 
277
289
  **Example — a 6 × 4 m slab rotated 30° about its first corner** (coordinates
278
290
  rounded to 3 dp; sides ≈ 6 m / 4 m; not axis-aligned, so the mapping is
@@ -1,3 +1,3 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  import '../bridge/node-shims';
3
3
  //# sourceMappingURL=pascal-mcp.d.ts.map
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  // Load shims FIRST so any subsequent core import sees the RAF polyfill.
3
3
  import '../bridge/node-shims';
4
4
  import { readFileSync } from 'node:fs';
@@ -47,25 +47,32 @@ async function main() {
47
47
  console.log(version);
48
48
  process.exit(0);
49
49
  }
50
- const bridge = new SceneBridge();
51
- if (values.scene) {
52
- const raw = readFileSync(values.scene, 'utf8');
53
- bridge.loadJSON(raw);
54
- }
55
- else {
56
- bridge.loadDefault();
57
- }
58
50
  const store = await createSceneStore();
59
- const server = createPascalMcpServer({ bridge, store });
51
+ const createServer = () => {
52
+ const bridge = new SceneBridge();
53
+ if (values.scene)
54
+ bridge.loadJSON(readFileSync(values.scene, 'utf8'));
55
+ else
56
+ bridge.loadDefault();
57
+ return createPascalMcpServer({ bridge, store });
58
+ };
60
59
  if (values.http) {
61
60
  const portNum = Number.parseInt(values.port ?? '3917', 10);
62
61
  if (!Number.isFinite(portNum) || portNum < 0 || portNum > 65_535) {
63
62
  throw new Error(`invalid --port value: ${values.port}`);
64
63
  }
65
- const handle = await connectHttp(server, portNum, {
64
+ const handle = await connectHttp(createServer, portNum, {
66
65
  host: values.host,
67
66
  authToken: values['auth-token'],
68
67
  allowedOrigins: values['cors-origin'],
68
+ ...(process.env.PASCAL_INSTANCE_ID
69
+ ? {
70
+ health: {
71
+ version: process.env.PASCAL_RUNTIME_VERSION ?? version,
72
+ instanceId: process.env.PASCAL_INSTANCE_ID,
73
+ },
74
+ }
75
+ : {}),
69
76
  });
70
77
  console.error(`[pascal-mcp] HTTP server listening on ${handle.host}:${handle.port}`);
71
78
  const shutdown = async () => {
@@ -81,7 +88,7 @@ async function main() {
81
88
  }
82
89
  else {
83
90
  // --stdio is the default when no transport flag is passed.
84
- await connectStdio(server);
91
+ await connectStdio(createServer());
85
92
  console.error('[pascal-mcp] stdio server running');
86
93
  }
87
94
  }
@@ -2,6 +2,7 @@ import './node-shims';
2
2
  import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
3
3
  import type { AnyNode } from '@pascal-app/core/schema';
4
4
  import { type AnyNodeId, type AnyNodeType } from '@pascal-app/core/schema';
5
+ import useScene from '@pascal-app/core/store';
5
6
  import type { SceneMeta } from '../storage/types';
6
7
  export type ValidationError = {
7
8
  nodeId: string;
@@ -29,6 +30,8 @@ export type DeletePatch = {
29
30
  };
30
31
  export type Patch = CreatePatch | UpdatePatch | DeletePatch;
31
32
  export type ActiveSceneMeta = Pick<SceneMeta, 'id' | 'name' | 'projectId' | 'ownerId' | 'thumbnailUrl' | 'version'>;
33
+ /** The `extra` bag `setScene` accepts — collections, materials, plugin state. */
34
+ type SetSceneExtra = Parameters<ReturnType<typeof useScene.getState>['setScene']>[2];
32
35
  /**
33
36
  * Headless bridge to the `@pascal-app/core` Zustand store.
34
37
  *
@@ -48,8 +51,8 @@ export declare class SceneBridge {
48
51
  /** Load initial state; if empty, creates default Site → Building → Level. */
49
52
  loadDefault(): void;
50
53
  /** Replace entire scene (undoable via Zundo). */
51
- setScene(nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[]): void;
52
- /** Full snapshot for export, including collections. */
54
+ setScene(nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[], extra?: SetSceneExtra): void;
55
+ /** Full snapshot for export, including collections and the material palette. */
53
56
  exportJSON(): SceneGraph & {
54
57
  collections: Record<string, unknown>;
55
58
  };
@@ -155,4 +158,5 @@ export declare class SceneBridge {
155
158
  */
156
159
  private _collectDescendants;
157
160
  }
161
+ export {};
158
162
  //# sourceMappingURL=scene-bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/scene-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scene-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/scene-bridge.ts"],"names":[],"mappings":"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"}
@@ -37,10 +37,10 @@ export class SceneBridge {
37
37
  useScene.getState().loadScene();
38
38
  }
39
39
  /** Replace entire scene (undoable via Zundo). */
40
- setScene(nodes, rootNodeIds) {
41
- useScene.getState().setScene(nodes, rootNodeIds);
40
+ setScene(nodes, rootNodeIds, extra) {
41
+ useScene.getState().setScene(nodes, rootNodeIds, extra);
42
42
  }
43
- /** Full snapshot for export, including collections. */
43
+ /** Full snapshot for export, including collections and the material palette. */
44
44
  exportJSON() {
45
45
  const state = useScene.getState();
46
46
  // Deep-clone so callers can't mutate store state directly.
@@ -48,6 +48,7 @@ export class SceneBridge {
48
48
  nodes: state.nodes,
49
49
  rootNodeIds: state.rootNodeIds,
50
50
  collections: state.collections ?? {},
51
+ materials: state.materials ?? {},
51
52
  ...(state.hasExplicitPluginInstallState || state.installedPlugins.length > 0
52
53
  ? { installedPlugins: state.installedPlugins }
53
54
  : {}),
@@ -90,10 +91,23 @@ export class SceneBridge {
90
91
  throw new Error(`invalid scene: forbidden key "${key}" in nodes`);
91
92
  }
92
93
  }
93
- this.setScene(nodes, rootNodeIds);
94
- if (Array.isArray(obj.installedPlugins)) {
95
- useScene.getState().setInstalledPlugins(obj.installedPlugins.filter((id) => typeof id === 'string'), { explicit: true });
96
- }
94
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
95
+ const collections = record(obj.collections);
96
+ const materials = record(obj.materials);
97
+ const installedPlugins = Array.isArray(obj.installedPlugins)
98
+ ? obj.installedPlugins.filter((id) => typeof id === 'string')
99
+ : undefined;
100
+ // One `setScene` call rather than a follow-up `setInstalledPlugins`:
101
+ // `setScene` overwrites `collections` and `materials` with `{}` whenever
102
+ // they aren't in the `extra` bag, so anything applied afterwards is lost.
103
+ // It also marks every node dirty at the end, and `markDirty` skips nodes
104
+ // whose plugin isn't installed — so plugin state has to be in place by
105
+ // then or plugin-owned nodes never get validated.
106
+ this.setScene(nodes, rootNodeIds, {
107
+ ...(collections && { collections }),
108
+ ...(materials && { materials }),
109
+ ...(installedPlugins && { installedPlugins, hasExplicitPluginInstallState: true }),
110
+ });
97
111
  }
98
112
  /** Read a single node, or `null` if not present. */
99
113
  getNode(id) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SceneBridge } from './bridge/scene-bridge';
2
2
  export { createSceneOperations, type SceneOperations } from './operations';
3
3
  export { type CreatePascalMcpServerOptions, createPascalMcpServer } from './server';
4
- export declare const version = "0.1.0";
4
+ export { version } from './version';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,KAAK,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAEnF,eAAO,MAAM,OAAO,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,KAAK,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { SceneBridge } from './bridge/scene-bridge';
2
2
  export { createSceneOperations } from './operations';
3
3
  export { createPascalMcpServer } from './server';
4
- export const version = '0.1.0';
4
+ export { version } from './version';
@@ -56,6 +56,7 @@ class SceneOperationsFacade {
56
56
  nodes: exported.nodes,
57
57
  rootNodeIds: exported.rootNodeIds,
58
58
  collections: exported.collections,
59
+ materials: exported.materials,
59
60
  installedPlugins: exported.installedPlugins,
60
61
  };
61
62
  }
@@ -1 +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"}
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;AAoBpD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACjC,GAAG,MAAM,CAeT;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAwBnF"}
@@ -3,8 +3,9 @@ import { SCENE_DESIGN_GUIDANCE } from './scene-guidance';
3
3
  const PREAMBLE = [
4
4
  'You are a Pascal 3D scene designer.',
5
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`.',
6
+ 'Bind an active scene before mutating anything. Call `create_project` for a new project, `list_scenes` then `load_scene` for an existing one, or `create_house_from_brief` to create and load a starter in one step. Without a bound scene, mutations apply in memory only — they are not persisted and never appear in the browser.',
7
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
+ '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`.',
8
9
  'Respect these invariants:',
9
10
  ' - Levels live under a Building.',
10
11
  ' - Walls, fences, zones, slabs, ceilings, roofs, stairs live under a Level.',
@@ -23,13 +24,13 @@ export function buildFromBriefPrompt(args) {
23
24
  if (args.constraints && args.constraints.trim().length > 0) {
24
25
  parts.push('', '## Constraints', args.constraints.trim());
25
26
  }
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
+ parts.push('', '## Task', 'Produce tool calls that realise the brief within the stated constraints.', '1. Bind a scene — `create_project` (new), `list_scenes` then `load_scene` (existing), or `create_house_from_brief` (starter template).', '2. Build the design — `create_story_shell` for exterior shells, `create_room`/`add_door`/`add_window`/`create_stair_between_levels`/`furnish_room` for interior layout, `create_roof` for roofing, `apply_patch` for exact bulk graph work.', '3. Finish — call `validate_scene`, `verify_scene`, and `get_project_status`, then return the `editorUrl`.', 'Call `save_scene` with `saveMode: "checkpoint"` only when the design reaches a meaningful milestone.');
27
28
  return parts.join('\n');
28
29
  }
29
30
  export function registerFromBrief(server, _bridge) {
30
31
  server.registerPrompt('from_brief', {
31
32
  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
+ description: 'Generate a Pascal 3D scene from a natural-language brief. Binds an active scene, then produces semantic tool calls to realise the design.',
33
34
  argsSchema: {
34
35
  brief: z.string(),
35
36
  constraints: z.string().optional(),
@@ -1 +1 @@
1
- {"version":3,"file":"agent-guide.d.ts","sourceRoot":"","sources":["../../src/resources/agent-guide.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,eAAO,MAAM,WAAW,QAwDZ,CAAA;AAEZ,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAuCpF"}
1
+ {"version":3,"file":"agent-guide.d.ts","sourceRoot":"","sources":["../../src/resources/agent-guide.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,eAAO,MAAM,WAAW,QA0DZ,CAAA;AAEZ,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAuCpF"}
@@ -32,6 +32,8 @@ export const AGENT_GUIDE = [
32
32
  '- Prefer semantic tools over raw graph patches.',
33
33
  '- Do not hand-write node graphs unless no semantic tool exists.',
34
34
  '- For rooms, use `create_room` -> `add_door` -> `add_window` -> `furnish_room`.',
35
+ '- `furnish_room` skips or nudges poses that block door clear zones or overlap other items; `verify_scene` and `check_collisions` report remaining issues.',
36
+ '- Between adjacent rooms, prefer one shared wall (or only cut openings that line up). Leave ~0.65 m clear on both sides of each door; do not stack furniture footprints.',
35
37
  '- For complete homes, create exterior shell, interior rooms, openings, roof, furniture, then landscaping.',
36
38
  '- For doors/windows, use `t` or `position` from 0 to 1 along the wall unless a tool explicitly says otherwise.',
37
39
  '- X/Z are floor-plan axes and Y is vertical; dimensions are meters.',
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAG1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAIjD,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,SAAS,CAYnF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAG1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAKjD,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,SAAS,CAYnF"}
package/dist/server.js CHANGED
@@ -4,10 +4,11 @@ import { registerPrompts } from './prompts';
4
4
  import { registerResources } from './resources';
5
5
  import { registerTools } from './tools';
6
6
  import { registerVisionTools } from './tools/vision';
7
+ import { version } from './version';
7
8
  export function createPascalMcpServer(opts) {
8
9
  const server = new McpServer({
9
- name: opts.name ?? 'pascal-mcp',
10
- version: opts.version ?? '0.1.0',
10
+ name: opts.name ?? 'pascal-mcp-server',
11
+ version: opts.version ?? version,
11
12
  });
12
13
  const operations = opts.operations ?? createSceneOperations({ bridge: opts.bridge, store: opts.store });
13
14
  registerTools(server, operations);
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-scene-store.d.ts","sourceRoot":"","sources":["../../src/storage/sqlite-scene-store.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAE1B,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAGf,KAAK,cAAc,EACpB,MAAM,SAAS,CAAA;AAOhB,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAmBvF;AAiKD;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,UAAU;IACjD,QAAQ,CAAC,OAAO,EAAG,QAAQ,CAAS;IAEpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAwC;IAC5E,OAAO,CAAC,EAAE,CAA8B;IACxC,OAAO,CAAC,SAAS,CAAuC;gBAE5C,IAAI,GAAE,uBAA4B;IAMxC,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBjE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAS3D,IAAI,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC;IAyHhD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAUhD,IAAI,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiCvD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAenE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;IAsCtF,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,UAAU,CAAC;IA6BpE,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAmB/F,KAAK,IAAI,IAAI;YAMC,QAAQ;IAiBtB,OAAO,CAAC,OAAO;YAgDD,oBAAoB;IAiBlC,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,gBAAgB;CAOzB"}
1
+ {"version":3,"file":"sqlite-scene-store.d.ts","sourceRoot":"","sources":["../../src/storage/sqlite-scene-store.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAE1B,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAGf,KAAK,cAAc,EACpB,MAAM,SAAS,CAAA;AAOhB,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAgDD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAmBvF;AAkKD;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,UAAU;IACjD,QAAQ,CAAC,OAAO,EAAG,QAAQ,CAAS;IAEpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAwC;IAC5E,OAAO,CAAC,EAAE,CAA8B;IACxC,OAAO,CAAC,SAAS,CAAuC;gBAE5C,IAAI,GAAE,uBAA4B;IAMxC,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBjE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAS3D,IAAI,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC;IAyHhD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAUhD,IAAI,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiCvD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IAenE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;IAsCtF,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,UAAU,CAAC;IA6BpE,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAmB/F,KAAK,IAAI,IAAI;YAMC,QAAQ;IAiBtB,OAAO,CAAC,OAAO;YAgDD,oBAAoB;IAiBlC,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,gBAAgB;CAOzB"}
@@ -10,10 +10,18 @@ const DEFAULT_MAX_SCENE_BYTES = 10 * 1024 * 1024;
10
10
  const DEFAULT_LIST_LIMIT = 100;
11
11
  const MAX_NAME_LENGTH = 200;
12
12
  const MIN_NAME_LENGTH = 1;
13
+ // `z.object()` strips keys it doesn't name, so every field that must survive a
14
+ // save→load round trip has to be listed here. Values stay `unknown` rather than
15
+ // being validated against `SceneMaterial`/`Collection`: nothing validates on the
16
+ // way in, and `parseGraph` throws, so a strict shape here would let one odd
17
+ // stored value make a saved scene permanently unloadable. Validation belongs on
18
+ // the write path, where the caller can still react to it.
13
19
  const GraphSchema = z.object({
14
20
  nodes: z.record(z.string(), z.unknown()),
15
21
  rootNodeIds: z.array(z.string()),
16
22
  collections: z.record(z.string(), z.unknown()).optional(),
23
+ materials: z.record(z.string(), z.unknown()).optional(),
24
+ installedPlugins: z.array(z.string()).optional(),
17
25
  });
18
26
  /**
19
27
  * Resolves Pascal's local SQLite database path.
@@ -81,7 +89,8 @@ function rowToMeta(row) {
81
89
  };
82
90
  }
83
91
  function editorUrlForScene(id) {
84
- return `/editor/${id}`;
92
+ const origin = process.env.PASCAL_EDITOR_ORIGIN?.replace(/\/$/, '');
93
+ return origin ? `${origin}/scene/${encodeURIComponent(id)}` : `/editor/${id}`;
85
94
  }
86
95
  function hashGraphJson(graphJson) {
87
96
  return createHash('sha256').update(graphJson).digest('hex');
@@ -1 +1 @@
1
- {"version":3,"file":"garden-house.d.ts","sourceRoot":"","sources":["../../src/templates/garden-house.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AA+QpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
1
+ {"version":3,"file":"garden-house.d.ts","sourceRoot":"","sources":["../../src/templates/garden-house.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAkRpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
@@ -12,10 +12,13 @@
12
12
  * - 2 windows on the south wall, 1 window on each of east and west
13
13
  * - 1 indoor "living" zone, 1 outdoor "garden" zone
14
14
  * - 3 fence segments bounding the north/east/west of the garden
15
+ *
16
+ * Coordinate system: `[x, z]` on the XZ plane, with `x` running east/west
17
+ * and `z` running north/south (positive z points south).
15
18
  */
16
19
  const HOUSE_W = 6; // half-width of the house (12 m total)
17
20
  const HOUSE_D = 4; // half-depth of the house (8 m total)
18
- const GARDEN_DEPTH = 6; // depth of the back-garden zone along +z direction
21
+ const GARDEN_DEPTH = 6; // depth of the back-garden zone along -z (north)
19
22
  const WALL_THICKNESS = 0.15;
20
23
  const WALL_HEIGHT = 2.7;
21
24
  function wall(id, start, end, children = []) {
@@ -1 +1 @@
1
- {"version":3,"file":"check-collisions.d.ts","sourceRoot":"","sources":["../../src/tools/check-collisions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,eAAO,MAAM,oBAAoB;;CAEhC,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;CAQjC,CAAA;AAqBD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAsCxF"}
1
+ {"version":3,"file":"check-collisions.d.ts","sourceRoot":"","sources":["../../src/tools/check-collisions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAIpD,eAAO,MAAM,oBAAoB;;CAEhC,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;CAQjC,CAAA;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA4CxF"}
@@ -1,5 +1,5 @@
1
- import { getScaledDimensions } from '@pascal-app/core/schema';
2
1
  import { z } from 'zod';
2
+ import { findItemItemCollisions } from './layout-clearance';
3
3
  import { NodeIdSchema } from './schemas';
4
4
  export const checkCollisionsInput = {
5
5
  levelId: NodeIdSchema.optional(),
@@ -11,47 +11,35 @@ export const checkCollisionsOutput = {
11
11
  kind: z.string(),
12
12
  })),
13
13
  };
14
- function itemAabb(item) {
15
- const [x, , z] = item.position;
16
- const [w, , d] = getScaledDimensions(item);
17
- const halfW = w / 2;
18
- const halfD = d / 2;
19
- return {
20
- minX: x - halfW,
21
- maxX: x + halfW,
22
- minZ: z - halfD,
23
- maxZ: z + halfD,
24
- };
25
- }
26
- function aabbOverlap(a, b) {
27
- return a.minX < b.maxX && a.maxX > b.minX && a.minZ < b.maxZ && a.maxZ > b.minZ;
28
- }
29
14
  export function registerCheckCollisions(server, bridge) {
30
15
  server.registerTool('check_collisions', {
31
16
  title: 'Check collisions',
32
- description: 'Detect overlapping item footprints via an axis-aligned 2D bounding-box test. Optionally scoped to a single level.',
17
+ description: 'Detect overlapping item footprints via a rotation-aware plan AABB test. Optionally scoped to a single level (items parented to that level).',
33
18
  inputSchema: checkCollisionsInput,
34
19
  outputSchema: checkCollisionsOutput,
35
20
  }, async ({ levelId }) => {
36
- const filter = { type: 'item' };
37
- if (levelId)
38
- filter.levelId = levelId;
39
- const items = bridge.findNodes(filter);
40
- const boxes = items.map((i) => ({ item: i, aabb: itemAabb(i) }));
41
- const collisions = [];
42
- for (let i = 0; i < boxes.length; i++) {
43
- for (let j = i + 1; j < boxes.length; j++) {
44
- const a = boxes[i];
45
- const b = boxes[j];
46
- if (aabbOverlap(a.aabb, b.aabb)) {
47
- collisions.push({
48
- aId: a.item.id,
49
- bId: b.item.id,
50
- kind: 'item-aabb',
51
- });
52
- }
53
- }
21
+ const nodes = Object.values(bridge.getNodes());
22
+ let scoped = nodes;
23
+ if (levelId) {
24
+ const levelItems = new Set(bridge
25
+ .findNodes({ type: 'item', levelId: levelId })
26
+ .map((n) => n.id));
27
+ scoped = nodes.filter((n) => n.type !== 'item' || levelItems.has(n.id));
54
28
  }
29
+ // gap: 0 keeps this tool's contract — it reports *actual* overlap.
30
+ // findItemItemCollisions defaults to DEFAULT_ITEM_GAP (8cm), which is the
31
+ // breathing room furnish_room wants when placing new items; applied here
32
+ // it would report items merely standing close together as colliding.
33
+ const found = findItemItemCollisions({
34
+ nodes: scoped,
35
+ levelId: levelId,
36
+ gap: 0,
37
+ });
38
+ const collisions = found.map((c) => ({
39
+ aId: c.aId,
40
+ bId: c.bId,
41
+ kind: c.kind,
42
+ }));
55
43
  const payload = { collisions };
56
44
  return {
57
45
  content: [{ type: 'text', text: JSON.stringify(payload) }],