@pascal-app/mcp 1.0.6 → 1.1.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.
Files changed (88) hide show
  1. package/README.md +32 -11
  2. package/dist/bin/pascal-mcp.js +2 -0
  3. package/dist/bridge/agent-session.d.ts +2 -1
  4. package/dist/bridge/agent-session.d.ts.map +1 -1
  5. package/dist/bridge/agent-session.js +0 -1
  6. package/dist/bridge/scene-bridge.d.ts +22 -14
  7. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  8. package/dist/bridge/scene-bridge.js +180 -198
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/lib/safe-fetch.d.ts +6 -0
  12. package/dist/lib/safe-fetch.d.ts.map +1 -1
  13. package/dist/lib/safe-fetch.js +135 -41
  14. package/dist/operations/scene-operations.d.ts +7 -0
  15. package/dist/operations/scene-operations.d.ts.map +1 -1
  16. package/dist/operations/scene-operations.js +4 -0
  17. package/dist/prompts/from-brief.js +1 -1
  18. package/dist/prompts/iterate-on-feedback.js +1 -1
  19. package/dist/prompts/renovation-from-photos.js +1 -1
  20. package/dist/prompts/scene-guidance.js +2 -2
  21. package/dist/resources/agent-guide.d.ts.map +1 -1
  22. package/dist/resources/agent-guide.js +8 -3
  23. package/dist/resources/catalog-items.d.ts +4 -1
  24. package/dist/resources/catalog-items.d.ts.map +1 -1
  25. package/dist/resources/catalog-items.js +14 -6
  26. package/dist/server.d.ts +9 -5
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +21 -5
  29. package/dist/templates/empty-studio.d.ts.map +1 -1
  30. package/dist/templates/empty-studio.js +2 -3
  31. package/dist/templates/garden-house.d.ts.map +1 -1
  32. package/dist/templates/garden-house.js +2 -3
  33. package/dist/templates/two-bedroom.d.ts.map +1 -1
  34. package/dist/templates/two-bedroom.js +2 -3
  35. package/dist/tools/add-column.d.ts.map +1 -1
  36. package/dist/tools/add-column.js +9 -6
  37. package/dist/tools/add-object.d.ts +44 -5
  38. package/dist/tools/add-object.d.ts.map +1 -1
  39. package/dist/tools/add-object.js +75 -21
  40. package/dist/tools/apply-patch.js +1 -1
  41. package/dist/tools/asset-catalog.d.ts +5 -6
  42. package/dist/tools/asset-catalog.d.ts.map +1 -1
  43. package/dist/tools/asset-catalog.js +1 -9
  44. package/dist/tools/author-scene.d.ts +8 -5
  45. package/dist/tools/author-scene.d.ts.map +1 -1
  46. package/dist/tools/author-scene.js +302 -51
  47. package/dist/tools/clear-scene.js +2 -2
  48. package/dist/tools/collections.d.ts.map +1 -1
  49. package/dist/tools/collections.js +8 -5
  50. package/dist/tools/honest-patch-guard.js +6 -5
  51. package/dist/tools/hosted-services.d.ts.map +1 -1
  52. package/dist/tools/hosted-services.js +2 -2
  53. package/dist/tools/index.d.ts +7 -2
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +3 -9
  56. package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
  57. package/dist/tools/local-geometry-scripts.js +7 -2
  58. package/dist/tools/patch-guards.d.ts.map +1 -1
  59. package/dist/tools/patch-guards.js +32 -15
  60. package/dist/tools/project-files.d.ts.map +1 -1
  61. package/dist/tools/project-files.js +2 -2
  62. package/dist/tools/room-tools.d.ts +1 -0
  63. package/dist/tools/room-tools.d.ts.map +1 -1
  64. package/dist/tools/room-tools.js +11 -8
  65. package/dist/tools/scene-scope.d.ts +4 -0
  66. package/dist/tools/scene-scope.d.ts.map +1 -0
  67. package/dist/tools/scene-scope.js +4 -0
  68. package/dist/tools/shared-tools.d.ts +0 -2
  69. package/dist/tools/shared-tools.d.ts.map +1 -1
  70. package/dist/tools/shared-tools.js +146 -31
  71. package/dist/tools/templates/create-house-from-brief.js +1 -1
  72. package/dist/tools/view-scene.js +2 -2
  73. package/examples/coordinate-conventions-demo.md +1 -0
  74. package/examples/photo-to-scene.md +1 -0
  75. package/examples/renovate-from-photos.md +2 -0
  76. package/package.json +10 -10
  77. package/dist/tools/create-unit.d.ts +0 -29
  78. package/dist/tools/create-unit.d.ts.map +0 -1
  79. package/dist/tools/create-unit.js +0 -36
  80. package/dist/tools/list-units.d.ts +0 -38
  81. package/dist/tools/list-units.d.ts.map +0 -1
  82. package/dist/tools/list-units.js +0 -61
  83. package/dist/tools/set-unit-members.d.ts +0 -22
  84. package/dist/tools/set-unit-members.d.ts.map +0 -1
  85. package/dist/tools/set-unit-members.js +0 -43
  86. package/dist/tools/unit-members.d.ts +0 -4
  87. package/dist/tools/unit-members.d.ts.map +0 -1
  88. package/dist/tools/unit-members.js +0 -24
@@ -1,5 +1,6 @@
1
- import { achievedChanges, verifyScene, } from '@pascal-app/core/agent-operations';
2
- import { authorSceneTool, isAgentRefusal, } from '@pascal-app/core/agent-tools';
1
+ import { filterDerivedNodeWrites } from '@pascal-app/core';
2
+ import { achievedChanges, authorSceneChecks, verifyScene, } from '@pascal-app/core/agent-operations';
3
+ import { authorScenePayload, authorSceneTool, isAgentRefusal, refuse, surfaceDescription, } from '@pascal-app/core/agent-tools';
3
4
  import { GEOMETRY_SCRIPT_MIME_TYPES } from '@pascal-app/core/schema';
4
5
  import { compileAuthorScene } from '@pascal-app/geometry-script/author-scene';
5
6
  import { z } from 'zod';
@@ -8,15 +9,21 @@ import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations.js';
8
9
  import { refusalResult, toolError } from './errors.js';
9
10
  import { persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
10
11
  import { toPatches } from './shared-tools.js';
12
+ /** The largest payload file read: a house with its modules inline runs to a few MiB. */
13
+ const PAYLOAD_FILE_MAX_BYTES = 32 * 1024 * 1024;
14
+ /** Payload keys holding a module's text; stored as `<key>Script`, the script's hash. */
15
+ const CODE_KEYS = new Set(['code', 'floorCode', 'wallCode']);
11
16
  /**
12
- * `author_scene` on the MCP: every item's module compiled and stored through the host, the
13
- * module and the adapter kept as the scene's provenance, the operation applied, Pascal's checks
14
- * run, and the gate report answered. Replay is the same payload through the same operation.
17
+ * `author_scene` on the MCP: the payload given, or the one the adapter answers on the module (the
18
+ * stored module and adapter, changed by edits, when left out), or the stored conversion again;
19
+ * items naming a piece written with its module, those still being made left for the host to write
20
+ * when they land; every module compiled and stored through the host, the operation applied,
21
+ * Pascal's checks run, and the gate report answered.
15
22
  */
16
- export function registerAuthorScene(server, bridge, host) {
23
+ export function registerAuthorScene(server, bridge, host, makers, readFile) {
17
24
  server.registerTool(authorSceneTool.name, {
18
25
  title: authorSceneTool.title,
19
- description: authorSceneTool.description,
26
+ description: surfaceDescription(authorSceneTool, 'mcp'),
20
27
  inputSchema: z.object(authorSceneTool.input),
21
28
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
22
29
  }, async (input) => {
@@ -46,9 +53,39 @@ export function registerAuthorScene(server, bridge, host) {
46
53
  ? { materials: bridge.getMaterials() }
47
54
  : {}),
48
55
  };
56
+ const readText = async (sha256) => {
57
+ const bytes = await host.readArtifact({ sceneId: scene.id, sha256 });
58
+ if (!bytes)
59
+ throw new Error(`The stored script ${sha256} could not be read`);
60
+ return new TextDecoder().decode(bytes);
61
+ };
62
+ let source;
63
+ try {
64
+ const readPayloadFile = async (fileId) => {
65
+ if (!readFile)
66
+ refuse('files_unavailable', 'This Pascal server keeps no project files: pass the payload itself.');
67
+ return new TextDecoder().decode(await readFile({
68
+ projectId: scene.projectId ?? undefined,
69
+ fileId,
70
+ maxBytes: PAYLOAD_FILE_MAX_BYTES,
71
+ signal: host.signal,
72
+ }));
73
+ };
74
+ source = await sourceOf(args, storedConversion(before, args.buildingId), host, readText, readPayloadFile);
75
+ }
76
+ catch (error) {
77
+ if (isAgentRefusal(error))
78
+ return refusalResult(error);
79
+ return toolError(error instanceof Error ? error.message : String(error), {
80
+ code: 'conversion_failed',
81
+ });
82
+ }
83
+ if ('failed' in source)
84
+ return source.failed;
85
+ const pieces = await withPieces(source.payload, scene.id, makers);
49
86
  let outcome;
50
87
  try {
51
- outcome = await compileAuthorScene(before, args, context, ({ code, kind, inputs, nodeId }) => compileAndStore(host, scene.id, { code, language: 'three', params: undefined, inputs }, kind, { nodeId }), host.signal);
88
+ outcome = await compileAuthorScene(before, { payload: pieces.payload, replace: args.replace, buildingId: args.buildingId }, context, ({ code, kind, inputs, nodeId }) => compileAndStore(host, scene.id, { code, language: 'three', params: undefined, inputs }, kind, { nodeId }), host.signal);
52
89
  }
53
90
  catch (error) {
54
91
  if (isAgentRefusal(error))
@@ -59,54 +96,76 @@ export function registerAuthorScene(server, bridge, host) {
59
96
  }
60
97
  // Provenance: the design's module and the program that read it, kept as scripts of the scene.
61
98
  const provenance = {};
62
- for (const [kind, text] of [
63
- ['module', args.module],
64
- ['adapter', args.adapter],
65
- ]) {
66
- if (!text)
67
- continue;
68
- provenance[kind] = await host.storeArtifact({
69
- sceneId: scene.id,
70
- sha256: await sha256(text),
71
- bytes: new TextEncoder().encode(text),
72
- mimeType: GEOMETRY_SCRIPT_MIME_TYPES.three,
73
- metadata: { kind: 'item' },
74
- });
99
+ for (const kind of ['module', 'adapter']) {
100
+ const text = source.texts[kind];
101
+ const kept = source.changed[kind] && text !== undefined
102
+ ? await host.storeArtifact({
103
+ sceneId: scene.id,
104
+ sha256: await sha256(text),
105
+ bytes: new TextEncoder().encode(text),
106
+ mimeType: GEOMETRY_SCRIPT_MIME_TYPES.three,
107
+ metadata: { kind: 'item' },
108
+ })
109
+ : source.stored[kind];
110
+ if (kept)
111
+ provenance[kind] = kept;
75
112
  }
76
113
  host.signal?.throwIfAborted();
77
114
  await host.assertActive?.();
78
115
  if (JSON.stringify(bridge.getNodes()) !== beforeJson) {
79
116
  return toolError('The scene changed while its candidate was compiling. Read the current scene and retry the conversion.', { code: 'scene_changed', mutationApplied: false });
80
117
  }
118
+ const report = {
119
+ ...outcome.result,
120
+ defects: [...pieces.defects, ...outcome.result.defects],
121
+ };
122
+ const payloadText = storedPayloadText(await withScripts(source.payload));
123
+ const payloadScript = await host.storeArtifact({
124
+ sceneId: scene.id,
125
+ sha256: await sha256(payloadText),
126
+ bytes: new TextEncoder().encode(payloadText),
127
+ mimeType: GEOMETRY_SCRIPT_MIME_TYPES.three,
128
+ metadata: { kind: 'item' },
129
+ });
81
130
  const buildingUpdate = outcome.changes.update.find((change) => change.id === outcome.buildingId);
82
- buildingUpdate.data.metadata = {
83
- ...buildingUpdate.data.metadata,
84
- authorScene: {
85
- ...provenance,
86
- payload: withoutCode(args.payload),
87
- report: outcome.result,
88
- at: new Date().toISOString(),
89
- },
131
+ const stored = {
132
+ ...provenance,
133
+ payload: payloadScript,
134
+ pending: pieces.pending,
135
+ at: new Date().toISOString(),
90
136
  };
91
- const patches = toPatches(outcome.changes);
137
+ buildingUpdate.data.metadata = JSON.parse(JSON.stringify({ ...buildingUpdate.data.metadata, authorScene: stored }));
138
+ // A revision's plates and ceilings follow its walls: what only the reconciler may change on
139
+ // one that stays (its outline, its room) is left to the bridge's reconciliation after the
140
+ // write, as in the editor; a new room's arrive with it.
141
+ const patches = toPatches(filterDerivedNodeWrites(bridge.getNodes(), outcome.changes));
92
142
  if (patches.length)
93
143
  bridge.applyPatch(patches);
94
- const checks = verifyScene(bridge.getNodes(), undefined, {
144
+ const verified = verifyScene(bridge.getNodes(), undefined, {
95
145
  activeLevelId: null,
96
146
  }).result;
97
147
  const byType = new Map();
98
- for (const issue of checks.issues)
148
+ for (const issue of verified.issues)
99
149
  byType.set(issue.type ?? 'unknown', (byType.get(issue.type ?? 'unknown') ?? 0) + 1);
150
+ const checks = authorSceneChecks({
151
+ payload: source.payload,
152
+ report,
153
+ pending: pieces.pending,
154
+ issues: verified.issues.filter((issue) => issue.severity !== 'info'),
155
+ });
100
156
  const payload = {
101
- ...outcome.result,
157
+ ...report,
158
+ pending: pieces.pending,
159
+ checks,
160
+ complete: pieces.pending.length === 0 && checks.every((check) => check.passed),
102
161
  itemTriangles: outcome.itemTriangles,
103
162
  itemsCompiled: outcome.itemsCompiled,
104
- green: outcome.result.defects.length === 0,
163
+ green: report.defects.length === 0,
105
164
  verify: {
106
- valid: checks.valid,
107
- issues: checks.issues.length,
165
+ valid: verified.valid,
166
+ issues: verified.issues.length,
108
167
  byType: Object.fromEntries(byType),
109
- first: checks.issues.slice(0, 40).map((i) => i.message),
168
+ first: verified.issues.slice(0, 40).map((i) => i.message),
110
169
  },
111
170
  achieved: achievedChanges(before, outcome.changes ?? {}),
112
171
  ...persistencePayload(await publishLiveSceneSnapshot(bridge, authorSceneTool.name)),
@@ -117,23 +176,215 @@ export function registerAuthorScene(server, bridge, host) {
117
176
  };
118
177
  });
119
178
  }
120
- /** The payload as kept with the scene: every module is already its own artifact. */
121
- function withoutCode(payload) {
179
+ /** The building a conversion revises: the one named, else the one holding a conversion. */
180
+ function storedConversion(nodes, buildingId) {
181
+ const holds = (node) => node?.type === 'building'
182
+ ? (node.metadata?.authorScene ?? null)
183
+ : null;
184
+ if (buildingId)
185
+ return holds(nodes[buildingId]) ?? undefined;
186
+ return Object.values(nodes).map(holds).find(Boolean) ?? undefined;
187
+ }
188
+ /**
189
+ * The payload to write, with the module and adapter it came from: given, or read from its project
190
+ * file; answered by the adapter on the module (each the one given or stored, changed by the edits);
191
+ * or the stored conversion again.
192
+ * An adapter that throws, or answers no valid payload, is answered with why, and nothing is written.
193
+ */
194
+ async function sourceOf(args, stored, host, readText, readPayloadFile) {
195
+ const edits = args.edits ?? [];
196
+ const changed = {
197
+ module: args.module !== undefined || edits.some((edit) => edit.in === 'module'),
198
+ adapter: args.adapter !== undefined || edits.some((edit) => edit.in === 'adapter'),
199
+ };
200
+ const result = (payload, texts) => ({
201
+ payload,
202
+ texts,
203
+ changed,
204
+ stored: { module: stored?.module, adapter: stored?.adapter },
205
+ });
206
+ if (args.payload && args.payloadFile)
207
+ refuse('payload_twice', 'Pass the payload or its payloadFile, not both.');
208
+ if (args.payload || args.payloadFile) {
209
+ if (edits.length)
210
+ refuse('edits_with_payload', 'Edits change the module or the adapter Pascal runs: leave the payload out to have it run.');
211
+ const texts = { module: args.module, adapter: args.adapter };
212
+ if (args.payload)
213
+ return result(args.payload, texts);
214
+ let answered;
215
+ try {
216
+ answered = JSON.parse(await readPayloadFile(args.payloadFile));
217
+ }
218
+ catch (error) {
219
+ if (!(error instanceof SyntaxError))
220
+ throw error;
221
+ return {
222
+ failed: toolError(`The payload file is not JSON: ${error.message}`, {
223
+ code: 'payload_invalid',
224
+ mutationApplied: false,
225
+ }),
226
+ };
227
+ }
228
+ const parsed = parsedPayload(answered, 'The payload file');
229
+ return 'failed' in parsed ? parsed : result(parsed.payload, texts);
230
+ }
231
+ if (args.module !== undefined || args.adapter !== undefined || edits.length) {
232
+ const texts = {
233
+ module: args.module ?? (stored?.module ? await readText(stored.module) : undefined),
234
+ adapter: args.adapter ?? (stored?.adapter ? await readText(stored.adapter) : undefined),
235
+ };
236
+ edits.forEach((edit, index) => {
237
+ const text = texts[edit.in];
238
+ if (text === undefined)
239
+ refuse('edit_without_source', `Edit ${index}: there is no ${edit.in} to change. Pass it, or convert with one first.`, { edit: index });
240
+ const at = text.indexOf(edit.find);
241
+ if (at < 0)
242
+ refuse('edit_not_found', `Edit ${index}: its find text is not in the ${edit.in}.`, {
243
+ edit: index,
244
+ });
245
+ if (text.indexOf(edit.find, at + 1) >= 0)
246
+ refuse('edit_ambiguous', `Edit ${index}: its find text occurs more than once in the ${edit.in}; give more of it.`, { edit: index });
247
+ texts[edit.in] = text.slice(0, at) + edit.replace + text.slice(at + edit.find.length);
248
+ });
249
+ if (texts.adapter === undefined)
250
+ refuse('adapter_required', 'This scene stores no adapter: pass the payload, or the module and its adapter together in one call.');
251
+ if (texts.module === undefined)
252
+ refuse('module_required', 'This scene stores no module: pass the module and its adapter together in one call.');
253
+ if (!host.runAdapter)
254
+ refuse('adapter_unavailable', 'This Pascal server cannot run an adapter: pass the payload.');
255
+ let answered;
256
+ try {
257
+ answered = (await host.runAdapter({ adapter: texts.adapter, design: texts.module })).payload;
258
+ }
259
+ catch (error) {
260
+ return {
261
+ failed: toolError(`Your adapter failed: ${error instanceof Error ? error.message : String(error)}`, { code: 'adapter_failed', mutationApplied: false }),
262
+ };
263
+ }
264
+ const parsed = parsedPayload(answered, "Your adapter's payload");
265
+ return 'failed' in parsed ? parsed : result(parsed.payload, texts);
266
+ }
267
+ if (typeof stored?.payload !== 'string')
268
+ refuse('nothing_to_write', 'Pass a payload, or a module and its adapter: this scene holds no stored conversion to write again.');
269
+ const rehydrated = (await withCode(readStoredPayload(await readText(stored.payload)), readText));
270
+ const parsed = authorScenePayload.safeParse(rehydrated);
271
+ if (!parsed.success)
272
+ refuse('stored_conversion_unreadable', 'The stored conversion could not be read back: pass the payload, or the module and its adapter.');
273
+ return result(parsed.data, {});
274
+ }
275
+ /** A payload read from an adapter or a file, or why it does not match author_scene's. */
276
+ function parsedPayload(value, what) {
277
+ const parsed = authorScenePayload.safeParse(value);
278
+ if (parsed.success)
279
+ return { payload: parsed.data };
122
280
  return {
123
- ...payload,
124
- levels: payload.levels.map((level) => ({
125
- ...level,
126
- walls: (level.walls ?? []).map(({ faceLeft, faceRight, ...wall }) => ({
127
- ...wall,
128
- openings: (wall.openings ?? []).map(({ code: _, ...opening }) => opening),
129
- ...(faceLeft ? { faceLeft: { toHeight: faceLeft.toHeight } } : {}),
130
- ...(faceRight ? { faceRight: { toHeight: faceRight.toHeight } } : {}),
131
- })),
132
- items: (level.items ?? []).map(({ code: _, ...item }) => item),
133
- rooms: level.rooms.map(({ floorCode: _, wallCode: _w, ...room }) => room),
134
- })),
281
+ failed: toolError(`${what} does not match author_scene's: ${parsed.error.issues
282
+ .slice(0, 12)
283
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
284
+ .join('; ')}`, {
285
+ code: 'payload_invalid',
286
+ mutationApplied: false,
287
+ issues: parsed.error.issues
288
+ .slice(0, 40)
289
+ .map((issue) => ({ path: issue.path.join('.'), message: issue.message })),
290
+ }),
135
291
  };
136
292
  }
293
+ /**
294
+ * Items naming a piece, with its module once made; those still being made left out and listed as
295
+ * pending (the host writes them when they land); one that failed, is unknown or has no module, a
296
+ * defect by its id.
297
+ */
298
+ async function withPieces(payload, sceneId, makers) {
299
+ const items = [
300
+ ...payload.levels.flatMap((level) => level.items ?? []),
301
+ ...(payload.roof?.items ?? []),
302
+ ];
303
+ const ids = [...new Set(items.flatMap((item) => (item.piece ? [item.piece] : [])))];
304
+ const states = ids.length && makers ? await makers.pieces({ sceneId, ids }) : {};
305
+ const pending = [];
306
+ const defects = [];
307
+ const resolve = (list) => list?.flatMap(({ piece, ...item }) => {
308
+ if (!piece) {
309
+ if (item.code)
310
+ return [item];
311
+ defects.push({ id: item.id, code: 'no_module', message: 'It has neither code nor piece.' });
312
+ return [];
313
+ }
314
+ const state = states[piece];
315
+ if (state?.status === 'made')
316
+ return [{ ...item, code: state.code }];
317
+ if (state?.status === 'making')
318
+ pending.push({ id: item.id, piece });
319
+ else
320
+ defects.push(!makers
321
+ ? {
322
+ id: item.id,
323
+ code: 'pieces_unavailable',
324
+ message: 'This Pascal server makes no pieces: give the item its code.',
325
+ }
326
+ : state
327
+ ? { id: item.id, code: 'piece_failed', message: state.error }
328
+ : {
329
+ id: item.id,
330
+ code: 'piece_unknown',
331
+ message: `No piece "${piece}": make it with add_object's piece first.`,
332
+ });
333
+ return [];
334
+ });
335
+ return {
336
+ payload: {
337
+ ...payload,
338
+ levels: payload.levels.map((level) => ({ ...level, items: resolve(level.items) })),
339
+ ...(payload.roof ? { roof: { ...payload.roof, items: resolve(payload.roof.items) } } : {}),
340
+ },
341
+ pending,
342
+ defects,
343
+ };
344
+ }
345
+ const STORED_PAYLOAD_PREFIX = 'export default ';
346
+ /** The payload as a module of the scene, the one script type its files take. */
347
+ const storedPayloadText = (payload) => `${STORED_PAYLOAD_PREFIX}${JSON.stringify(payload)}\n`;
348
+ function readStoredPayload(text) {
349
+ if (!text.startsWith(STORED_PAYLOAD_PREFIX))
350
+ return undefined;
351
+ try {
352
+ return JSON.parse(text.slice(STORED_PAYLOAD_PREFIX.length));
353
+ }
354
+ catch {
355
+ return undefined;
356
+ }
357
+ }
358
+ /** The payload as kept with the scene: every module as its stored script's hash, pieces by id. */
359
+ async function withScripts(value) {
360
+ if (Array.isArray(value))
361
+ return Promise.all(value.map(withScripts));
362
+ if (!value || typeof value !== 'object')
363
+ return value;
364
+ const out = {};
365
+ for (const [key, entry] of Object.entries(value))
366
+ if (CODE_KEYS.has(key) && typeof entry === 'string')
367
+ out[`${key}Script`] = await sha256(entry);
368
+ else
369
+ out[key] = await withScripts(entry);
370
+ return out;
371
+ }
372
+ /** The stored payload with its modules read back; one whose script is gone is left without it. */
373
+ async function withCode(value, readText) {
374
+ if (Array.isArray(value))
375
+ return Promise.all(value.map((entry) => withCode(entry, readText)));
376
+ if (!value || typeof value !== 'object')
377
+ return value;
378
+ const out = {};
379
+ for (const [key, entry] of Object.entries(value)) {
380
+ const base = key.endsWith('Script') ? key.slice(0, -'Script'.length) : null;
381
+ if (base && CODE_KEYS.has(base) && typeof entry === 'string')
382
+ out[base] = await readText(entry).catch(() => undefined);
383
+ else
384
+ out[key] = await withCode(entry, readText);
385
+ }
386
+ return out;
387
+ }
137
388
  async function sha256(text) {
138
389
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
139
390
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
@@ -1,4 +1,4 @@
1
- import { clearSceneTool } from '@pascal-app/core/agent-tools';
1
+ import { clearSceneTool, surfaceDescription } from '@pascal-app/core/agent-tools';
2
2
  import { z } from 'zod';
3
3
  import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations.js';
4
4
  import { refusalResult } from './errors.js';
@@ -11,7 +11,7 @@ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './
11
11
  export function registerClearScene(server, operations) {
12
12
  server.registerTool(clearSceneTool.name, {
13
13
  title: clearSceneTool.title,
14
- description: clearSceneTool.description,
14
+ description: surfaceDescription(clearSceneTool, 'mcp'),
15
15
  inputSchema: z.object(clearSceneTool.input),
16
16
  outputSchema: z.object({
17
17
  cleared: z.object({ removed: z.number() }),
@@ -1 +1 @@
1
- {"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/tools/collections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAY7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAgBpD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA+CxF"}
1
+ {"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/tools/collections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAiB7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAiBpD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDxF"}
@@ -1,9 +1,11 @@
1
1
  import { editCollection, listCollections, writeCollections, } from '@pascal-app/core/agent-operations';
2
- import { editCollectionTool, listCollectionsTool } from '@pascal-app/core/agent-tools';
2
+ import { editCollectionTool, listCollectionsTool, surfaceDescription, } from '@pascal-app/core/agent-tools';
3
+ import { untracked } from '@pascal-app/core/runtime';
3
4
  import { z } from 'zod';
4
5
  import { DESTRUCTIVE_TOOL_ANNOTATIONS, READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
5
6
  import { refusalResult } from './errors.js';
6
7
  import { persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
8
+ import { inScene } from './scene-scope.js';
7
9
  // The collection tools share contracts and operations with the chat; they read the scene's
8
10
  // collections beside its nodes, which the node-only shared operations never see.
9
11
  function sceneOf(bridge) {
@@ -15,7 +17,7 @@ function reply(payload) {
15
17
  export function registerCollectionTools(server, bridge) {
16
18
  server.registerTool(listCollectionsTool.name, {
17
19
  title: listCollectionsTool.title,
18
- description: listCollectionsTool.description,
20
+ description: surfaceDescription(listCollectionsTool, 'mcp'),
19
21
  inputSchema: z.object(listCollectionsTool.input),
20
22
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
21
23
  }, async (input) => {
@@ -28,13 +30,13 @@ export function registerCollectionTools(server, bridge) {
28
30
  });
29
31
  server.registerTool(editCollectionTool.name, {
30
32
  title: editCollectionTool.title,
31
- description: editCollectionTool.description,
33
+ description: surfaceDescription(editCollectionTool, 'mcp'),
32
34
  inputSchema: z.object(editCollectionTool.input),
33
35
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
34
36
  }, async (input) => {
35
37
  let outcome;
36
38
  try {
37
- outcome = editCollection(sceneOf(bridge), input);
39
+ outcome = inScene(bridge, () => editCollection(sceneOf(bridge), input));
38
40
  }
39
41
  catch (error) {
40
42
  return refusalResult(error);
@@ -43,7 +45,8 @@ export function registerCollectionTools(server, bridge) {
43
45
  bridge.runAsSingleHistoryStep(() => {
44
46
  if (update?.length)
45
47
  bridge.applyPatch(update.map(({ id, data }) => ({ op: 'update', id: id, data })));
46
- bridge.setCollections(writeCollections(bridge.getCollections(), collections));
48
+ // Writing the other collections back is a copy, not a read of them.
49
+ bridge.setCollections(writeCollections(untracked(bridge.getCollections()), collections));
47
50
  });
48
51
  return reply({
49
52
  ...outcome.result,
@@ -7,14 +7,15 @@ import { PatchRefusedError, registerPatchGuard } from './patch-guards.js';
7
7
  * one hides is refused, naming it; `null` clears. The cleaned data is what is written.
8
8
  */
9
9
  function honestUpdates(patches, scene) {
10
- const nodes = { ...scene };
10
+ // The batch over the scene, read id by id.
11
+ const written = new Map();
11
12
  for (const [index, patch] of patches.entries()) {
12
13
  if (patch.op === 'create')
13
- nodes[patch.node.id] = patch.node;
14
+ written.set(patch.node.id, patch.node);
14
15
  else if (patch.op === 'delete')
15
- delete nodes[patch.id];
16
+ written.set(patch.id, undefined);
16
17
  else if (patch.op === 'update') {
17
- const current = nodes[patch.id];
18
+ const current = written.has(patch.id) ? written.get(patch.id) : scene[patch.id];
18
19
  if (!current)
19
20
  continue;
20
21
  try {
@@ -25,7 +26,7 @@ function honestUpdates(patches, scene) {
25
26
  throw error;
26
27
  throw new PatchRefusedError(error.code, index, patch.id, error.message);
27
28
  }
28
- nodes[patch.id] = { ...current, ...patch.data };
29
+ written.set(patch.id, { ...current, ...patch.data });
29
30
  }
30
31
  }
31
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hosted-services.d.ts","sourceRoot":"","sources":["../../src/tools/hosted-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAA;AAQ1C,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,qBAAqB,GAC7B,IAAI,CA8CN"}
1
+ {"version":3,"file":"hosted-services.d.ts","sourceRoot":"","sources":["../../src/tools/hosted-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAA;AAS1C,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,qBAAqB,GAC7B,IAAI,CA8CN"}
@@ -1,11 +1,11 @@
1
1
  import { executeHostedServiceTool, } from '@pascal-app/core/agent-operations';
2
- import { HOSTED_SERVICE_TOOL_CONTRACTS, isAgentRefusal, refusalPayload, } from '@pascal-app/core/agent-tools';
2
+ import { HOSTED_SERVICE_TOOL_CONTRACTS, isAgentRefusal, refusalPayload, surfaceDescription, } from '@pascal-app/core/agent-tools';
3
3
  import { z } from 'zod';
4
4
  export function registerHostedServiceTools(server, execute) {
5
5
  for (const contract of HOSTED_SERVICE_TOOL_CONTRACTS) {
6
6
  server.registerTool(contract.name, {
7
7
  title: contract.title,
8
- description: contract.description,
8
+ description: surfaceDescription(contract, 'mcp'),
9
9
  inputSchema: z.object(contract.input).strict(),
10
10
  annotations: contract.annotations,
11
11
  }, async (input, ctx) => {
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/server';
2
+ import type { ProjectFileReader } from '@pascal-app/core/agent-operations';
2
3
  import type { SceneOperations } from '../operations/index.js';
3
- import { type GeometryScriptHost } from './add-object.js';
4
+ import { type GeometryScriptHost, type ObjectMakerHost } from './add-object.js';
4
5
  import type { AssetCatalog } from './asset-catalog.js';
5
6
  import { type PlanReadingHosts } from './shared-tools.js';
6
7
  import { type SceneViewHost } from './view-scene.js';
@@ -16,7 +17,11 @@ import { type SceneViewHost } from './view-scene.js';
16
17
  export type ToolHosts = PlanReadingHosts & {
17
18
  catalog?: AssetCatalog;
18
19
  geometryScripts?: GeometryScriptHost;
20
+ /** Makes add_object's pieces from words; without it a piece is refused `pieces_unavailable`. */
21
+ objectMakers?: ObjectMakerHost;
22
+ /** Reads author_scene's payloadFile; without it the tool refuses one `files_unavailable`. */
23
+ readFile?: ProjectFileReader;
19
24
  sceneViews?: SceneViewHost;
20
25
  };
21
- export declare function registerTools(server: McpServer, operations: SceneOperations, { catalog, geometryScripts, sceneViews, assetViews }?: ToolHosts): void;
26
+ export declare function registerTools(server: McpServer, operations: SceneOperations, { catalog, geometryScripts, objectMakers, readFile, sceneViews, assetViews }?: ToolHosts): void;
22
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,OAAO,EAEL,KAAK,kBAAkB,EAGxB,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAiBnD,OAAO,EAAE,KAAK,gBAAgB,EAAuB,MAAM,gBAAgB,CAAA;AAI3E,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEpE;;;;;;;GAOG;AACH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG;IACzC,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,UAAU,CAAC,EAAE,aAAa,CAAA;CAC3B,CAAA;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,eAAe,EAC3B,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,GAAE,SAAc,GACnE,IAAI,CA+BN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAGrB,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAcnD,OAAO,EAAE,KAAK,gBAAgB,EAAuB,MAAM,gBAAgB,CAAA;AAI3E,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEpE;;;;;;;GAOG;AACH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG;IACzC,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,gGAAgG;IAChG,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,UAAU,CAAC,EAAE,aAAa,CAAA;CAC3B,CAAA;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,eAAe,EAC3B,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,GAAE,SAAc,GAC3F,IAAI,CA4BN"}
@@ -5,32 +5,29 @@ import { registerAuthorScene } from './author-scene.js';
5
5
  import { registerCheckCollisions } from './check-collisions.js';
6
6
  import { registerClearScene } from './clear-scene.js';
7
7
  import { registerConstructionTools } from './construction-tools.js';
8
- import { registerCreateUnit } from './create-unit.js';
9
8
  import { registerExportGlb } from './export-glb.js';
10
9
  import { registerExportJson } from './export-json.js';
11
10
  import { registerGetScene } from './get-scene.js';
12
- import { registerListUnits } from './list-units.js';
13
11
  import { registerMeasure } from './measure.js';
14
12
  import { registerPhotoToSceneTool } from './photo-to-scene/index.js';
15
13
  import { registerRedo } from './redo.js';
16
14
  import { registerRoomTools } from './room-tools.js';
17
15
  import { registerSceneLifecycleTools } from './scene-lifecycle/index.js';
18
- import { registerSetUnitMembers } from './set-unit-members.js';
19
16
  import { registerSetZone } from './set-zone.js';
20
17
  import { registerSharedTools } from './shared-tools.js';
21
18
  import { registerTemplateTools } from './templates/index.js';
22
19
  import { registerUndo } from './undo.js';
23
20
  import { registerVariantTools } from './variants/index.js';
24
21
  import { registerViewScene } from './view-scene.js';
25
- export function registerTools(server, operations, { catalog, geometryScripts, sceneViews, assetViews } = {}) {
22
+ export function registerTools(server, operations, { catalog, geometryScripts, objectMakers, readFile, sceneViews, assetViews } = {}) {
26
23
  registerGetScene(server, operations);
27
24
  registerSharedTools(server, operations, catalog, {
28
25
  assetViews,
29
26
  buildObject: (input) => compileObjectOnHost(geometryScripts, operations, input),
30
27
  });
31
28
  registerAddColumn(server, operations, geometryScripts);
32
- registerAddObject(server, operations, geometryScripts);
33
- registerAuthorScene(server, operations, geometryScripts);
29
+ registerAddObject(server, operations, geometryScripts, objectMakers);
30
+ registerAuthorScene(server, operations, geometryScripts, objectMakers, readFile);
34
31
  registerGetSource(server, operations, geometryScripts);
35
32
  registerMeasure(server, operations);
36
33
  registerViewScene(server, operations, sceneViews);
@@ -38,9 +35,6 @@ export function registerTools(server, operations, { catalog, geometryScripts, sc
38
35
  registerRoomTools(server, operations, geometryScripts);
39
36
  registerApplyPatch(server, operations);
40
37
  registerClearScene(server, operations);
41
- registerCreateUnit(server, operations);
42
- registerSetUnitMembers(server, operations);
43
- registerListUnits(server, operations);
44
38
  registerSetZone(server, operations);
45
39
  registerUndo(server, operations);
46
40
  registerRedo(server, operations);
@@ -1 +1 @@
1
- {"version":3,"file":"local-geometry-scripts.d.ts","sourceRoot":"","sources":["../../src/tools/local-geometry-scripts.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CA4D5E"}
1
+ {"version":3,"file":"local-geometry-scripts.d.ts","sourceRoot":"","sources":["../../src/tools/local-geometry-scripts.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAkE5E"}
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { sdfChecksOn } from '@pascal-app/core/usage-field';
3
4
  import { compileBlenderBuild } from '@pascal-app/geometry-script/blender';
4
5
  import { runBlenderLocally } from '@pascal-app/geometry-script/blender-local';
5
6
  import { DiskArtifactStore } from '../storage/disk-artifact-store.js';
@@ -17,7 +18,11 @@ export function localGeometryScripts(projectPath) {
17
18
  if (!blender)
18
19
  throw new Error('bpy scripts need PASCAL_BLENDER, the path to a Blender binary');
19
20
  const run = await runBlenderLocally({ blender, code: input.code, params: input.params });
20
- const { glb, ...compiled } = await compileBlenderBuild({ ...input, run });
21
+ const { glb, ...compiled } = await compileBlenderBuild({
22
+ ...input,
23
+ run,
24
+ ...(sdfChecksOn() ? { usageShape: true } : {}),
25
+ });
21
26
  return { ...compiled, glb: new Uint8Array(glb) };
22
27
  }
23
28
  if (process.env.PASCAL_SERVER_SCRIPT_COMPILE !== '1') {
@@ -55,7 +60,7 @@ export function localGeometryScripts(projectPath) {
55
60
  reject(new Error(`Geometry script compilation failed: ${stderr}`));
56
61
  }
57
62
  });
58
- child.stdin.end(JSON.stringify(input));
63
+ child.stdin.end(JSON.stringify({ ...input, ...(sdfChecksOn() ? { usageShape: true } : {}) }));
59
64
  });
60
65
  },
61
66
  storeArtifact: async ({ sha256, bytes }) => {