@pascal-app/mcp 1.0.0-beta.6 → 1.0.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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  3. package/dist/bridge/scene-bridge.js +2 -2
  4. package/dist/server.d.ts +11 -0
  5. package/dist/server.d.ts.map +1 -1
  6. package/dist/server.js +52 -0
  7. package/dist/tools/annotations.d.ts +28 -0
  8. package/dist/tools/annotations.d.ts.map +1 -0
  9. package/dist/tools/annotations.js +27 -0
  10. package/dist/tools/apply-patch.d.ts +7 -0
  11. package/dist/tools/apply-patch.d.ts.map +1 -1
  12. package/dist/tools/apply-patch.js +6 -2
  13. package/dist/tools/check-collisions.d.ts +59 -0
  14. package/dist/tools/check-collisions.d.ts.map +1 -1
  15. package/dist/tools/check-collisions.js +240 -19
  16. package/dist/tools/construction-tools.d.ts +23 -1
  17. package/dist/tools/construction-tools.d.ts.map +1 -1
  18. package/dist/tools/construction-tools.js +34 -8
  19. package/dist/tools/create-level.d.ts +7 -0
  20. package/dist/tools/create-level.d.ts.map +1 -1
  21. package/dist/tools/create-level.js +6 -3
  22. package/dist/tools/create-wall.d.ts +7 -0
  23. package/dist/tools/create-wall.d.ts.map +1 -1
  24. package/dist/tools/create-wall.js +6 -3
  25. package/dist/tools/cut-opening.d.ts +7 -0
  26. package/dist/tools/cut-opening.d.ts.map +1 -1
  27. package/dist/tools/cut-opening.js +6 -3
  28. package/dist/tools/delete-node.d.ts +7 -0
  29. package/dist/tools/delete-node.d.ts.map +1 -1
  30. package/dist/tools/delete-node.js +6 -3
  31. package/dist/tools/describe-node.d.ts.map +1 -1
  32. package/dist/tools/describe-node.js +2 -0
  33. package/dist/tools/door-clearance.d.ts +18 -2
  34. package/dist/tools/door-clearance.d.ts.map +1 -1
  35. package/dist/tools/door-clearance.js +101 -7
  36. package/dist/tools/duplicate-level.d.ts +7 -0
  37. package/dist/tools/duplicate-level.d.ts.map +1 -1
  38. package/dist/tools/duplicate-level.js +6 -2
  39. package/dist/tools/export-glb.d.ts.map +1 -1
  40. package/dist/tools/export-glb.js +4 -2
  41. package/dist/tools/export-json.d.ts.map +1 -1
  42. package/dist/tools/export-json.js +2 -0
  43. package/dist/tools/find-nodes.d.ts.map +1 -1
  44. package/dist/tools/find-nodes.js +2 -0
  45. package/dist/tools/get-node.d.ts.map +1 -1
  46. package/dist/tools/get-node.js +2 -0
  47. package/dist/tools/get-scene.d.ts.map +1 -1
  48. package/dist/tools/get-scene.js +2 -0
  49. package/dist/tools/layout-clearance.d.ts +2 -0
  50. package/dist/tools/layout-clearance.d.ts.map +1 -1
  51. package/dist/tools/layout-clearance.js +5 -1
  52. package/dist/tools/live-sync.d.ts +33 -3
  53. package/dist/tools/live-sync.d.ts.map +1 -1
  54. package/dist/tools/live-sync.js +36 -4
  55. package/dist/tools/measure.d.ts +1 -0
  56. package/dist/tools/measure.d.ts.map +1 -1
  57. package/dist/tools/measure.js +4 -0
  58. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  59. package/dist/tools/photo-to-scene/photo-to-scene.js +2 -0
  60. package/dist/tools/place-item.d.ts +7 -0
  61. package/dist/tools/place-item.d.ts.map +1 -1
  62. package/dist/tools/place-item.js +6 -2
  63. package/dist/tools/redo.d.ts +7 -0
  64. package/dist/tools/redo.d.ts.map +1 -1
  65. package/dist/tools/redo.js +6 -4
  66. package/dist/tools/room-tools.d.ts +28 -0
  67. package/dist/tools/room-tools.d.ts.map +1 -1
  68. package/dist/tools/room-tools.js +20 -5
  69. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  70. package/dist/tools/scene-lifecycle/create-project.js +2 -0
  71. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  72. package/dist/tools/scene-lifecycle/delete-scene.js +2 -0
  73. package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -1
  74. package/dist/tools/scene-lifecycle/get-project-status.js +2 -0
  75. package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -1
  76. package/dist/tools/scene-lifecycle/list-scenes.js +2 -0
  77. package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -1
  78. package/dist/tools/scene-lifecycle/load-scene.js +2 -0
  79. package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -1
  80. package/dist/tools/scene-lifecycle/rename-scene.js +2 -0
  81. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  82. package/dist/tools/scene-lifecycle/save-scene.js +2 -0
  83. package/dist/tools/scene-lifecycle/test-utils.d.ts +5 -1
  84. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  85. package/dist/tools/scene-lifecycle/test-utils.js +24 -0
  86. package/dist/tools/scene-query.d.ts.map +1 -1
  87. package/dist/tools/scene-query.js +6 -0
  88. package/dist/tools/set-zone.d.ts +7 -0
  89. package/dist/tools/set-zone.d.ts.map +1 -1
  90. package/dist/tools/set-zone.js +6 -3
  91. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  92. package/dist/tools/templates/create-from-template.js +2 -0
  93. package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -1
  94. package/dist/tools/templates/create-house-from-brief.js +2 -0
  95. package/dist/tools/templates/list-templates.d.ts.map +1 -1
  96. package/dist/tools/templates/list-templates.js +2 -0
  97. package/dist/tools/undo.d.ts +7 -0
  98. package/dist/tools/undo.d.ts.map +1 -1
  99. package/dist/tools/undo.js +6 -4
  100. package/dist/tools/validate-scene.d.ts.map +1 -1
  101. package/dist/tools/validate-scene.js +2 -0
  102. package/dist/tools/variants/generate-variants.d.ts.map +1 -1
  103. package/dist/tools/variants/generate-variants.js +2 -0
  104. package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -1
  105. package/dist/tools/vision/analyze-floorplan-image.js +2 -0
  106. package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -1
  107. package/dist/tools/vision/analyze-room-photo.js +2 -0
  108. package/package.json +5 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pascal Group Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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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"}
@@ -1,6 +1,6 @@
1
1
  // Side-effect import MUST come first: installs RAF polyfill before core loads.
2
2
  import './node-shims';
3
- import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema';
3
+ import { AnyNode as AnyNodeSchema, parseNode, } from '@pascal-app/core/schema';
4
4
  // Per PLAN §0.6: `useScene` is the DEFAULT export from `@pascal-app/core/store`.
5
5
  import useScene from '@pascal-app/core/store';
6
6
  /**
@@ -294,7 +294,7 @@ export class SceneBridge {
294
294
  if (!p)
295
295
  throw new Error(`invalid patch: patches[${i}] is undefined`);
296
296
  if (p.op === 'create') {
297
- const res = AnyNodeSchema.safeParse(p.node);
297
+ const res = parseNode(p.node);
298
298
  if (!res.success) {
299
299
  throw new Error(`invalid patch: patches[${i}] create node failed schema: ${res.error.message}`);
300
300
  }
package/dist/server.d.ts CHANGED
@@ -2,6 +2,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { SceneBridge } from './bridge/scene-bridge';
3
3
  import { type SceneOperations } from './operations';
4
4
  import type { SceneStore } from './storage/types';
5
+ export type PascalMcpToolExecutor = <Result>(input: {
6
+ name: string;
7
+ signal: AbortSignal;
8
+ execute: () => Promise<Result>;
9
+ }) => Promise<Result>;
5
10
  export type CreatePascalMcpServerOptions = {
6
11
  bridge: SceneBridge;
7
12
  operations?: SceneOperations;
@@ -9,6 +14,12 @@ export type CreatePascalMcpServerOptions = {
9
14
  store?: SceneStore;
10
15
  name?: string;
11
16
  version?: string;
17
+ /**
18
+ * Wrap every regular tool handler, including callback updates.
19
+ * Tool renames fail closed because the SDK registration lifecycle cannot safely rename twice.
20
+ * Experimental task-based tool registrations are outside this hook.
21
+ */
22
+ executeTool?: PascalMcpToolExecutor;
12
23
  };
13
24
  export declare function createPascalMcpServer(opts: CreatePascalMcpServerOptions): McpServer;
14
25
  //# sourceMappingURL=server.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,yCAAyC,CAAA;AACxF,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,qBAAqB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/B,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAErB,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;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAA;CACpC,CAAA;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,SAAS,CAanF"}
package/dist/server.js CHANGED
@@ -10,6 +10,8 @@ export function createPascalMcpServer(opts) {
10
10
  name: opts.name ?? 'pascal-mcp-server',
11
11
  version: opts.version ?? version,
12
12
  });
13
+ if (opts.executeTool)
14
+ installToolExecutor(server, opts.executeTool);
13
15
  const operations = opts.operations ?? createSceneOperations({ bridge: opts.bridge, store: opts.store });
14
16
  registerTools(server, operations);
15
17
  registerVisionTools(server, operations);
@@ -17,3 +19,53 @@ export function createPascalMcpServer(opts) {
17
19
  registerPrompts(server, operations);
18
20
  return server;
19
21
  }
22
+ function installToolExecutor(server, executeTool) {
23
+ const registerTool = server.registerTool.bind(server);
24
+ const wrappedRegisterTool = (name, config, callback) => {
25
+ const runtimeCallback = callback;
26
+ const registration = registerTool(name, config, wrapToolCallback(name, runtimeCallback, executeTool));
27
+ return wrapRegisteredTool(registration, name, runtimeCallback, executeTool);
28
+ };
29
+ server.registerTool = wrappedRegisterTool;
30
+ const tool = server.tool.bind(server);
31
+ server.tool = ((name, ...args) => {
32
+ const callback = args.at(-1);
33
+ if (typeof callback !== 'function') {
34
+ return Reflect.apply(tool, undefined, [name, ...args]);
35
+ }
36
+ const runtimeCallback = callback;
37
+ args[args.length - 1] = wrapToolCallback(name, runtimeCallback, executeTool);
38
+ const registration = Reflect.apply(tool, undefined, [name, ...args]);
39
+ return wrapRegisteredTool(registration, name, runtimeCallback, executeTool);
40
+ });
41
+ }
42
+ function wrapToolCallback(name, callback, executeTool) {
43
+ return (...args) => executeTool({
44
+ name,
45
+ signal: toolRequestSignal(args),
46
+ execute: () => Promise.resolve(Reflect.apply(callback, undefined, args)),
47
+ });
48
+ }
49
+ function wrapRegisteredTool(registration, initialName, initialCallback, executeTool) {
50
+ let currentCallback = initialCallback;
51
+ const update = registration.update.bind(registration);
52
+ registration.update = ((updates) => {
53
+ if (typeof updates.name === 'string') {
54
+ throw new Error('MCP tool renaming is unsupported when executeTool is configured');
55
+ }
56
+ const callbackUpdate = updates.callback;
57
+ if (typeof callbackUpdate === 'function') {
58
+ currentCallback = callbackUpdate;
59
+ }
60
+ update({
61
+ ...updates,
62
+ ...(typeof callbackUpdate === 'function'
63
+ ? { callback: wrapToolCallback(initialName, currentCallback, executeTool) }
64
+ : {}),
65
+ });
66
+ });
67
+ return registration;
68
+ }
69
+ function toolRequestSignal(args) {
70
+ return args.at(-1).signal;
71
+ }
@@ -0,0 +1,28 @@
1
+ export declare const READ_ONLY_TOOL_ANNOTATIONS: {
2
+ readonly readOnlyHint: true;
3
+ readonly destructiveHint: false;
4
+ readonly idempotentHint: true;
5
+ readonly openWorldHint: false;
6
+ };
7
+ export declare const READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS: {
8
+ readonly readOnlyHint: true;
9
+ readonly destructiveHint: false;
10
+ readonly idempotentHint: true;
11
+ readonly openWorldHint: true;
12
+ };
13
+ export declare const ADDITIVE_TOOL_ANNOTATIONS: {
14
+ readonly readOnlyHint: false;
15
+ readonly destructiveHint: false;
16
+ readonly openWorldHint: false;
17
+ };
18
+ export declare const DESTRUCTIVE_TOOL_ANNOTATIONS: {
19
+ readonly readOnlyHint: false;
20
+ readonly destructiveHint: true;
21
+ readonly openWorldHint: false;
22
+ };
23
+ export declare const DESTRUCTIVE_OPEN_WORLD_TOOL_ANNOTATIONS: {
24
+ readonly readOnlyHint: false;
25
+ readonly destructiveHint: true;
26
+ readonly openWorldHint: true;
27
+ };
28
+ //# sourceMappingURL=annotations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/tools/annotations.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B;;;;;CAK7B,CAAA;AAEV,eAAO,MAAM,qCAAqC;;;;;CAKxC,CAAA;AAEV,eAAO,MAAM,yBAAyB;;;;CAI5B,CAAA;AAEV,eAAO,MAAM,4BAA4B;;;;CAI/B,CAAA;AAEV,eAAO,MAAM,uCAAuC;;;;CAI1C,CAAA"}
@@ -0,0 +1,27 @@
1
+ export const READ_ONLY_TOOL_ANNOTATIONS = {
2
+ readOnlyHint: true,
3
+ destructiveHint: false,
4
+ idempotentHint: true,
5
+ openWorldHint: false,
6
+ };
7
+ export const READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS = {
8
+ readOnlyHint: true,
9
+ destructiveHint: false,
10
+ idempotentHint: true,
11
+ openWorldHint: true,
12
+ };
13
+ export const ADDITIVE_TOOL_ANNOTATIONS = {
14
+ readOnlyHint: false,
15
+ destructiveHint: false,
16
+ openWorldHint: false,
17
+ };
18
+ export const DESTRUCTIVE_TOOL_ANNOTATIONS = {
19
+ readOnlyHint: false,
20
+ destructiveHint: true,
21
+ openWorldHint: false,
22
+ };
23
+ export const DESTRUCTIVE_OPEN_WORLD_TOOL_ANNOTATIONS = {
24
+ readOnlyHint: false,
25
+ destructiveHint: true,
26
+ openWorldHint: true,
27
+ };
@@ -17,6 +17,13 @@ export declare const applyPatchInput: {
17
17
  }, z.core.$strip>], "op">>;
18
18
  };
19
19
  export declare const applyPatchOutput: {
20
+ persistence: z.ZodOptional<z.ZodObject<{
21
+ status: z.ZodEnum<{
22
+ unbound: "unbound";
23
+ events_unsupported: "events_unsupported";
24
+ }>;
25
+ warning: z.ZodString;
26
+ }, z.core.$strip>>;
20
27
  appliedOps: z.ZodNumber;
21
28
  deletedIds: z.ZodArray<z.ZodString>;
22
29
  createdIds: z.ZodArray<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"apply-patch.d.ts","sourceRoot":"","sources":["../../src/tools/apply-patch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;CAE3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;CAI5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmDnF"}
1
+ {"version":3,"file":"apply-patch.d.ts","sourceRoot":"","sources":["../../src/tools/apply-patch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;CAE3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;CAK5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAqDnF"}
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations';
2
3
  import { ErrorCode, throwMcpError } from './errors';
3
- import { publishLiveSceneSnapshot } from './live-sync';
4
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
4
5
  import { PatchSchema } from './schemas';
5
6
  export const applyPatchInput = {
6
7
  patches: z.array(PatchSchema),
@@ -9,6 +10,7 @@ export const applyPatchOutput = {
9
10
  appliedOps: z.number(),
10
11
  deletedIds: z.array(z.string()),
11
12
  createdIds: z.array(z.string()),
13
+ ...liveSyncOutput,
12
14
  };
13
15
  export function registerApplyPatch(server, bridge) {
14
16
  server.registerTool('apply_patch', {
@@ -16,6 +18,7 @@ export function registerApplyPatch(server, bridge) {
16
18
  description: 'Apply a batch of create/update/delete operations atomically. All patches are validated before any are applied; the entire batch forms a single undo step.',
17
19
  inputSchema: applyPatchInput,
18
20
  outputSchema: applyPatchOutput,
21
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
19
22
  }, async ({ patches }) => {
20
23
  const bridgePatches = patches.map((p) => {
21
24
  if (p.op === 'create') {
@@ -40,11 +43,12 @@ export function registerApplyPatch(server, bridge) {
40
43
  });
41
44
  try {
42
45
  const result = bridge.applyPatch(bridgePatches);
43
- await publishLiveSceneSnapshot(bridge, 'apply_patch');
46
+ const persistence = await publishLiveSceneSnapshot(bridge, 'apply_patch');
44
47
  const payload = {
45
48
  appliedOps: result.appliedOps,
46
49
  deletedIds: result.deletedIds,
47
50
  createdIds: result.createdIds,
51
+ ...persistencePayload(persistence),
48
52
  };
49
53
  return {
50
54
  content: [{ type: 'text', text: JSON.stringify(payload) }],
@@ -3,12 +3,71 @@ import { z } from 'zod';
3
3
  import type { SceneOperations } from '../operations';
4
4
  export declare const checkCollisionsInput: {
5
5
  levelId: z.ZodOptional<z.ZodString>;
6
+ minimumClearance: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
7
+ floorOnly: z.ZodDefault<z.ZodBoolean>;
8
+ candidate: z.ZodOptional<z.ZodObject<{
9
+ id: z.ZodDefault<z.ZodString>;
10
+ name: z.ZodDefault<z.ZodString>;
11
+ levelId: z.ZodString;
12
+ position: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>, z.ZodTransform<[number, number, number], number[]>>;
13
+ dimensions: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>, z.ZodTransform<[number, number, number], number[]>>;
14
+ rotationY: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
15
+ source: z.ZodOptional<z.ZodObject<{
16
+ assetId: z.ZodOptional<z.ZodString>;
17
+ uri: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>>;
19
+ }, z.core.$strip>>;
6
20
  };
7
21
  export declare const checkCollisionsOutput: {
22
+ status: z.ZodEnum<{
23
+ checked: "checked";
24
+ partial: "partial";
25
+ insufficient_evidence: "insufficient_evidence";
26
+ }>;
27
+ units: z.ZodLiteral<"meters">;
28
+ method: z.ZodLiteral<"rotation-aware-plan-aabb">;
29
+ assessmentGraphHash: z.ZodString;
30
+ minimumClearanceMeters: z.ZodNumber;
31
+ floorOnly: z.ZodBoolean;
32
+ candidateItemId: z.ZodNullable<z.ZodString>;
33
+ checkedItems: z.ZodArray<z.ZodObject<{
34
+ id: z.ZodString;
35
+ name: z.ZodString;
36
+ levelId: z.ZodNullable<z.ZodString>;
37
+ positionMeters: z.ZodArray<z.ZodNumber>;
38
+ rotationYRadians: z.ZodNumber;
39
+ source: z.ZodObject<{
40
+ assetId: z.ZodString;
41
+ uri: z.ZodString;
42
+ catalog: z.ZodString;
43
+ }, z.core.$strip>;
44
+ sourceDimensionsMeters: z.ZodArray<z.ZodNumber>;
45
+ effectiveDimensionsMeters: z.ZodArray<z.ZodNumber>;
46
+ footprintBoundsMeters: z.ZodObject<{
47
+ minX: z.ZodNumber;
48
+ maxX: z.ZodNumber;
49
+ minZ: z.ZodNumber;
50
+ maxZ: z.ZodNumber;
51
+ }, z.core.$strip>;
52
+ }, z.core.$strip>>;
53
+ skippedItems: z.ZodArray<z.ZodObject<{
54
+ id: z.ZodString;
55
+ name: z.ZodString;
56
+ reason: z.ZodString;
57
+ }, z.core.$strip>>;
58
+ unsupportedChecks: z.ZodArray<z.ZodObject<{
59
+ check: z.ZodString;
60
+ reason: z.ZodString;
61
+ }, z.core.$strip>>;
8
62
  collisions: z.ZodArray<z.ZodObject<{
9
63
  aId: z.ZodString;
10
64
  bId: z.ZodString;
11
65
  kind: z.ZodString;
66
+ violation: z.ZodEnum<{
67
+ overlap: "overlap";
68
+ clearance: "clearance";
69
+ }>;
70
+ minimumClearanceMeters: z.ZodNumber;
12
71
  }, z.core.$strip>>;
13
72
  };
14
73
  export declare function registerCheckCollisions(server: McpServer, bridge: SceneOperations): void;
@@ -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;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
+ {"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;AA8BpD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;CA+BhC,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDjC,CAAA;AA6BD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAyKxF"}
@@ -1,46 +1,267 @@
1
+ import { ItemNode } from '@pascal-app/core/schema';
1
2
  import { z } from 'zod';
3
+ import { READ_ONLY_TOOL_ANNOTATIONS } from './annotations';
4
+ import { inspectItemPlanFootprint, resolveNodeLevelId } from './door-clearance';
5
+ import { ErrorCode, throwMcpError } from './errors';
2
6
  import { findItemItemCollisions } from './layout-clearance';
7
+ import { measurement } from './measurement';
8
+ import { computeGraphHash } from './scene-lifecycle/metadata';
3
9
  import { NodeIdSchema } from './schemas';
10
+ const candidatePosition = measurement('length', 'm', {
11
+ description: 'Candidate position coordinate.',
12
+ });
13
+ const candidateDimension = measurement('length', 'm', {
14
+ positive: true,
15
+ description: 'Candidate declared size.',
16
+ });
17
+ const vector3 = (component) => z
18
+ .array(component)
19
+ .length(3)
20
+ .transform((values, ctx) => {
21
+ const [x, y, zValue] = values;
22
+ if (x === undefined || y === undefined || zValue === undefined) {
23
+ ctx.addIssue({ code: 'custom', message: 'Expected exactly three values.' });
24
+ return z.NEVER;
25
+ }
26
+ return [x, y, zValue];
27
+ });
28
+ const outputVector3 = z.array(z.number()).length(3);
4
29
  export const checkCollisionsInput = {
5
30
  levelId: NodeIdSchema.optional(),
31
+ minimumClearance: measurement('length', 'm', {
32
+ min: 0,
33
+ description: 'Minimum free plan-space required between item footprints.',
34
+ }).default(0),
35
+ floorOnly: z
36
+ .boolean()
37
+ .default(false)
38
+ .describe('When true, wall-, wall-side-, and ceiling-attached items are excluded.'),
39
+ candidate: z
40
+ .object({
41
+ id: z.string().min(1).max(120).default('candidate'),
42
+ name: z.string().min(1).max(200).default('Candidate item'),
43
+ levelId: NodeIdSchema,
44
+ position: vector3(candidatePosition),
45
+ dimensions: vector3(candidateDimension),
46
+ rotationY: measurement('angle', 'rad', {
47
+ description: 'Candidate yaw around the vertical axis.',
48
+ }).default(0),
49
+ source: z
50
+ .object({
51
+ assetId: z.string().min(1).optional(),
52
+ uri: z.string().min(1).optional(),
53
+ })
54
+ .optional(),
55
+ })
56
+ .optional()
57
+ .describe('Read-only prospective furniture item. It is checked against the target level but never added to the scene.'),
6
58
  };
7
59
  export const checkCollisionsOutput = {
60
+ status: z.enum(['checked', 'partial', 'insufficient_evidence']),
61
+ units: z.literal('meters'),
62
+ method: z.literal('rotation-aware-plan-aabb'),
63
+ assessmentGraphHash: z.string(),
64
+ minimumClearanceMeters: z.number(),
65
+ floorOnly: z.boolean(),
66
+ candidateItemId: z.string().nullable(),
67
+ checkedItems: z.array(z.object({
68
+ id: z.string(),
69
+ name: z.string(),
70
+ levelId: z.string().nullable(),
71
+ positionMeters: outputVector3,
72
+ rotationYRadians: z.number(),
73
+ source: z.object({
74
+ assetId: z.string(),
75
+ uri: z.string(),
76
+ catalog: z.string(),
77
+ }),
78
+ sourceDimensionsMeters: outputVector3,
79
+ effectiveDimensionsMeters: outputVector3,
80
+ footprintBoundsMeters: z.object({
81
+ minX: z.number(),
82
+ maxX: z.number(),
83
+ minZ: z.number(),
84
+ maxZ: z.number(),
85
+ }),
86
+ })),
87
+ skippedItems: z.array(z.object({
88
+ id: z.string(),
89
+ name: z.string(),
90
+ reason: z.string(),
91
+ })),
92
+ unsupportedChecks: z.array(z.object({
93
+ check: z.string(),
94
+ reason: z.string(),
95
+ })),
8
96
  collisions: z.array(z.object({
9
97
  aId: z.string(),
10
98
  bId: z.string(),
11
99
  kind: z.string(),
100
+ violation: z.enum(['overlap', 'clearance']),
101
+ minimumClearanceMeters: z.number(),
12
102
  })),
13
103
  };
104
+ const unsupportedChecks = [
105
+ {
106
+ check: 'vertical_clearance',
107
+ reason: 'This check uses the X/Z plan footprint only and does not prove height clearance.',
108
+ },
109
+ {
110
+ check: 'room_boundary_clearance',
111
+ reason: 'The tool compares items with other items and does not prove containment in a room.',
112
+ },
113
+ {
114
+ check: 'door_swing_envelope',
115
+ reason: 'Door swing geometry is not evaluated by check_collisions.',
116
+ },
117
+ {
118
+ check: 'delivery_path',
119
+ reason: 'No route, turning-radius, stair, or opening traversal is evaluated.',
120
+ },
121
+ {
122
+ check: 'mesh_geometry',
123
+ reason: 'Asset meshes are not loaded; the check uses declared rectangular dimensions.',
124
+ },
125
+ {
126
+ check: 'hosted_item_world_transform',
127
+ reason: 'Items positioned in a non-level parent frame are skipped instead of approximated.',
128
+ },
129
+ ];
14
130
  export function registerCheckCollisions(server, bridge) {
15
131
  server.registerTool('check_collisions', {
16
132
  title: 'Check collisions',
17
- description: 'Detect overlapping item footprints via a rotation-aware plan AABB test. Optionally scoped to a single level (items parented to that level).',
133
+ description: 'Assess declared item footprints with a rotation-aware plan AABB test. Supports an explicit minimum clearance and reports units, source dimensions, skipped evidence, and unsupported checks. Optionally scope to one level or floor-standing items.',
18
134
  inputSchema: checkCollisionsInput,
19
135
  outputSchema: checkCollisionsOutput,
20
- }, async ({ levelId }) => {
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));
136
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
137
+ }, async ({ levelId, minimumClearance, floorOnly, candidate }) => {
138
+ const sceneNodes = Object.values(bridge.getNodes());
139
+ if (candidate && levelId && candidate.levelId !== levelId) {
140
+ throwMcpError(ErrorCode.InvalidParams, 'candidate.levelId must match levelId when both are provided');
141
+ }
142
+ const scopeLevelId = candidate?.levelId ?? levelId;
143
+ if (scopeLevelId) {
144
+ const target = sceneNodes.find((node) => node.id === scopeLevelId);
145
+ if (target?.type !== 'level') {
146
+ throwMcpError(ErrorCode.InvalidParams, `Level not found: ${scopeLevelId}`);
147
+ }
28
148
  }
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.
149
+ if (candidate) {
150
+ if (sceneNodes.some((node) => node.id === candidate.id)) {
151
+ throwMcpError(ErrorCode.InvalidParams, `Candidate id already exists: ${candidate.id}`);
152
+ }
153
+ }
154
+ const candidateNode = candidate
155
+ ? ItemNode.parse({
156
+ object: 'node',
157
+ type: 'item',
158
+ parentId: candidate.levelId,
159
+ visible: true,
160
+ metadata: {},
161
+ name: candidate.name,
162
+ position: candidate.position,
163
+ rotation: [0, candidate.rotationY, 0],
164
+ scale: [1, 1, 1],
165
+ children: [],
166
+ asset: {
167
+ id: candidate.source?.assetId ?? 'supplied-dimensions',
168
+ name: candidate.name,
169
+ category: 'furniture',
170
+ thumbnail: '',
171
+ source: 'library',
172
+ src: 'asset://supplied-dimensions',
173
+ dimensions: candidate.dimensions,
174
+ offset: [0, 0, 0],
175
+ rotation: [0, 0, 0],
176
+ scale: [1, 1, 1],
177
+ },
178
+ })
179
+ : null;
180
+ const nodes = candidateNode ? [...sceneNodes, candidateNode] : sceneNodes;
181
+ const byId = new Map(nodes.map((node) => [node.id, node]));
182
+ const scoped = nodes.filter((node) => {
183
+ if (node.type !== 'item' || !scopeLevelId)
184
+ return true;
185
+ return resolveNodeLevelId(node.id, byId) === scopeLevelId;
186
+ });
187
+ const checkedItems = [];
188
+ const skippedItems = [];
189
+ for (const node of scoped) {
190
+ if (node.type !== 'item')
191
+ continue;
192
+ const resolvedLevelId = node === candidateNode ? candidate.levelId : bridge.resolveLevelId(node.id);
193
+ if (!resolvedLevelId) {
194
+ skippedItems.push({
195
+ id: node.id,
196
+ name: node.name ?? node.asset.name ?? node.id,
197
+ reason: 'unresolved_level',
198
+ });
199
+ continue;
200
+ }
201
+ const immediateParent = node === candidateNode ? byId.get(candidate.levelId) : bridge.getAncestry(node.id)[1];
202
+ if (immediateParent && immediateParent.type !== 'level') {
203
+ skippedItems.push({
204
+ id: node.id,
205
+ name: node.name ?? node.asset.name ?? node.id,
206
+ reason: floorOnly ? 'unsupported_attachment' : 'parent_local_coordinates',
207
+ });
208
+ continue;
209
+ }
210
+ const inspected = inspectItemPlanFootprint(node, { floorOnly });
211
+ const name = node.name ?? node.asset.name ?? node.id;
212
+ if (!inspected.ok) {
213
+ skippedItems.push({ id: node.id, name, reason: inspected.reason });
214
+ continue;
215
+ }
216
+ checkedItems.push({
217
+ id: node === candidateNode ? candidate.id : node.id,
218
+ name,
219
+ levelId: resolvedLevelId,
220
+ positionMeters: node.position,
221
+ rotationYRadians: inspected.rotationY,
222
+ source: {
223
+ assetId: node === candidateNode
224
+ ? (candidate.source?.assetId ?? 'supplied-dimensions')
225
+ : node.asset.id,
226
+ uri: node === candidateNode
227
+ ? (candidate.source?.uri ?? 'supplied://dimensions')
228
+ : node.asset.src,
229
+ catalog: node === candidateNode ? 'supplied' : (node.asset.source ?? 'unknown'),
230
+ },
231
+ sourceDimensionsMeters: inspected.sourceDimensions,
232
+ effectiveDimensionsMeters: inspected.effectiveDimensions,
233
+ footprintBoundsMeters: inspected.aabb,
234
+ });
235
+ }
236
+ const checkedItemIds = new Set(checkedItems.map((item) => (item.id === candidate?.id ? candidateNode.id : item.id)));
33
237
  const found = findItemItemCollisions({
34
- nodes: scoped,
35
- levelId: levelId,
36
- gap: 0,
238
+ nodes: scoped.filter((node) => (node.type === 'item' ? checkedItemIds.has(node.id) : true)),
239
+ gap: minimumClearance,
37
240
  });
38
241
  const collisions = found.map((c) => ({
39
- aId: c.aId,
40
- bId: c.bId,
242
+ aId: c.aId === candidateNode?.id ? candidate.id : c.aId,
243
+ bId: c.bId === candidateNode?.id ? candidate.id : c.bId,
41
244
  kind: c.kind,
245
+ violation: c.violation,
246
+ minimumClearanceMeters: c.minimumClearanceMeters,
42
247
  }));
43
- const payload = { collisions };
248
+ const payload = {
249
+ status: checkedItems.length === 0 && skippedItems.length > 0
250
+ ? 'insufficient_evidence'
251
+ : skippedItems.length > 0
252
+ ? 'partial'
253
+ : 'checked',
254
+ units: 'meters',
255
+ method: 'rotation-aware-plan-aabb',
256
+ assessmentGraphHash: computeGraphHash(bridge.exportJSON()),
257
+ minimumClearanceMeters: minimumClearance,
258
+ floorOnly,
259
+ candidateItemId: candidate?.id ?? null,
260
+ checkedItems,
261
+ skippedItems,
262
+ unsupportedChecks: [...unsupportedChecks],
263
+ collisions,
264
+ };
44
265
  return {
45
266
  content: [{ type: 'text', text: JSON.stringify(payload) }],
46
267
  structuredContent: payload,