@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.
- package/README.md +56 -44
- package/dist/bin/pascal-mcp.d.ts +1 -1
- package/dist/bin/pascal-mcp.js +19 -12
- package/dist/bridge/scene-bridge.d.ts +6 -2
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +21 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/operations/scene-operations.js +1 -0
- package/dist/prompts/from-brief.d.ts.map +1 -1
- package/dist/prompts/from-brief.js +4 -3
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
- package/dist/storage/sqlite-scene-store.js +10 -1
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +4 -1
- package/dist/tools/check-collisions.d.ts.map +1 -1
- package/dist/tools/check-collisions.js +23 -35
- package/dist/tools/door-clearance.d.ts +119 -0
- package/dist/tools/door-clearance.d.ts.map +1 -0
- package/dist/tools/door-clearance.js +240 -0
- package/dist/tools/layout-clearance.d.ts +103 -0
- package/dist/tools/layout-clearance.d.ts.map +1 -0
- package/dist/tools/layout-clearance.js +186 -0
- package/dist/tools/measure.d.ts.map +1 -1
- package/dist/tools/measure.js +6 -2
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +90 -34
- package/dist/tools/scene-query.d.ts +1 -0
- package/dist/tools/scene-query.d.ts.map +1 -1
- package/dist/tools/scene-query.js +6 -0
- package/dist/tools/templates/create-from-template.d.ts +2 -0
- package/dist/tools/templates/create-from-template.d.ts.map +1 -1
- package/dist/tools/templates/create-from-template.js +2 -0
- package/dist/transports/http.d.ts +6 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +52 -9
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
and
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
-
##
|
|
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": "
|
|
102
|
-
"args": ["
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
129
|
-
"args": ["
|
|
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
|
|
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": "
|
|
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
|
-
|
|
170
|
+
### Codex CLI
|
|
156
171
|
|
|
157
172
|
Via the CLI:
|
|
158
173
|
|
|
159
174
|
```bash
|
|
160
|
-
|
|
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
|
-
--
|
|
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 = "
|
|
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
|
-
|
|
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": "
|
|
192
|
-
"args": ["
|
|
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
|
package/dist/bin/pascal-mcp.d.ts
CHANGED
package/dist/bin/pascal-mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
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
|
|
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(
|
|
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(
|
|
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":"AACA,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,KAAK,SAAS,EAA4B,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
4
|
+
export { version } from './version';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 +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;
|
|
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
|
-
'
|
|
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.
|
|
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: '
|
|
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,
|
|
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.',
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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 ??
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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) }],
|