@pascal-app/mcp 0.3.2 → 1.0.0-beta.1

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.
@@ -166,17 +166,17 @@ class SceneOperationsFacade {
166
166
  return this.requireStore().rename(id, newName, options);
167
167
  }
168
168
  async appendSceneEvent(options) {
169
- const append = this.requireStore().appendSceneEvent;
170
- if (!append)
169
+ const store = this.requireStore();
170
+ if (!store.appendSceneEvent)
171
171
  return null;
172
- return append(options);
172
+ return store.appendSceneEvent(options);
173
173
  }
174
174
  async listSceneEvents(id, options) {
175
- const list = this.requireStore().listSceneEvents;
176
- if (!list) {
175
+ const store = this.requireStore();
176
+ if (!store.listSceneEvents) {
177
177
  throw new Error('scene_events_unavailable');
178
178
  }
179
- return list(id, options);
179
+ return store.listSceneEvents(id, options);
180
180
  }
181
181
  requireBridge() {
182
182
  if (!this.#bridge) {
@@ -1 +1 @@
1
- {"version":3,"file":"empty-studio.d.ts","sourceRoot":"","sources":["../../src/templates/empty-studio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAsPpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
1
+ {"version":3,"file":"empty-studio.d.ts","sourceRoot":"","sources":["../../src/templates/empty-studio.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAmPpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LEVEL_HEIGHT } from '@pascal-app/core';
1
2
  /**
2
3
  * 40 m² studio apartment — a single open room with one window, one front door
3
4
  * and a single "Living/Kitchen" zone. Used as a starting point for small unit
@@ -117,7 +118,6 @@ function buildNodes() {
117
118
  metadata: {},
118
119
  children: [],
119
120
  thickness: 0.1,
120
- height: 2.5,
121
121
  start: [-W, -D],
122
122
  end: [W, -D],
123
123
  frontSide: 'unknown',
@@ -132,7 +132,6 @@ function buildNodes() {
132
132
  metadata: {},
133
133
  children: [],
134
134
  thickness: 0.1,
135
- height: 2.5,
136
135
  start: [W, -D],
137
136
  end: [W, D],
138
137
  frontSide: 'unknown',
@@ -147,7 +146,6 @@ function buildNodes() {
147
146
  metadata: {},
148
147
  children: ['door_front'],
149
148
  thickness: 0.1,
150
- height: 2.5,
151
149
  start: [W, D],
152
150
  end: [-W, D],
153
151
  frontSide: 'unknown',
@@ -162,7 +160,6 @@ function buildNodes() {
162
160
  metadata: {},
163
161
  children: ['window_w'],
164
162
  thickness: 0.1,
165
- height: 2.5,
166
163
  start: [-W, D],
167
164
  end: [-W, -D],
168
165
  frontSide: 'unknown',
@@ -192,6 +189,7 @@ function buildNodes() {
192
189
  visible: true,
193
190
  metadata: {},
194
191
  level: 0,
192
+ height: DEFAULT_LEVEL_HEIGHT,
195
193
  children: [...wallIds, 'zone_living'],
196
194
  };
197
195
  return {
@@ -28,7 +28,6 @@ function wall(id, start, end, children = []) {
28
28
  metadata: {},
29
29
  children,
30
30
  thickness: WALL_THICKNESS,
31
- height: WALL_HEIGHT,
32
31
  start,
33
32
  end,
34
33
  frontSide: 'unknown',
@@ -216,6 +215,7 @@ function buildTemplate() {
216
215
  visible: true,
217
216
  metadata: {},
218
217
  level: 0,
218
+ height: WALL_HEIGHT,
219
219
  children: [
220
220
  'wall_n',
221
221
  'wall_e',
@@ -31,7 +31,6 @@ function wall(id, start, end, children = []) {
31
31
  metadata: {},
32
32
  children,
33
33
  thickness: WALL_THICKNESS,
34
- height: WALL_HEIGHT,
35
34
  start,
36
35
  end,
37
36
  frontSide: 'unknown',
@@ -240,6 +239,7 @@ function buildTemplate() {
240
239
  visible: true,
241
240
  metadata: {},
242
241
  level: 0,
242
+ height: WALL_HEIGHT,
243
243
  children: [
244
244
  'wall_n',
245
245
  'wall_e',
@@ -4,7 +4,7 @@ import type { SceneOperations } from '../operations';
4
4
  export declare const createStoryShellInput: {
5
5
  levelId: z.ZodString;
6
6
  footprint: z.ZodArray<z.ZodArray<z.ZodNumber>>;
7
- wallHeight: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
7
+ wallHeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
8
8
  wallThickness: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
9
9
  createSlab: z.ZodDefault<z.ZodBoolean>;
10
10
  createCeiling: z.ZodDefault<z.ZodBoolean>;
@@ -62,7 +62,7 @@ export declare const createStairBetweenLevelsInput: {
62
62
  rotation: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
63
63
  width: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
64
64
  runLength: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
65
- totalRise: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
65
+ totalRise: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
66
66
  stepCount: z.ZodDefault<z.ZodNumber>;
67
67
  railingMode: z.ZodDefault<z.ZodEnum<{
68
68
  none: "none";
@@ -1 +1 @@
1
- {"version":3,"file":"construction-tools.d.ts","sourceRoot":"","sources":["../../src/tools/construction-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAaxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAqBjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CA0B3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAiT1F"}
1
+ {"version":3,"file":"construction-tools.d.ts","sourceRoot":"","sources":["../../src/tools/construction-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAcxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAsBjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CA0B3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmU1F"}
@@ -1,3 +1,4 @@
1
+ import { resolveStairTotalRise } from '@pascal-app/core';
1
2
  import { CeilingNode, getActiveRoofHeight, LevelNode, RoofNode, RoofSegmentNode, SlabNode, StairNode, StairSegmentNode, WallNode, } from '@pascal-app/core/schema';
2
3
  import { z } from 'zod';
3
4
  import { publishLiveSceneSnapshot } from './live-sync';
@@ -8,7 +9,10 @@ const RAILING_MODES = ['none', 'left', 'right', 'both'];
8
9
  export const createStoryShellInput = {
9
10
  levelId: NodeIdSchema,
10
11
  footprint: z.array(Vec2Schema).min(3),
11
- wallHeight: measurement('length', 'm', { positive: true, description: 'Wall height.' }).default(2.8),
12
+ wallHeight: measurement('length', 'm', {
13
+ positive: true,
14
+ description: 'Explicit wall height override. Omit for level-plane-bound walls.',
15
+ }).optional(),
12
16
  wallThickness: measurement('length', 'm', {
13
17
  positive: true,
14
18
  description: 'Wall thickness.',
@@ -77,7 +81,7 @@ export const createStairBetweenLevelsInput = {
77
81
  totalRise: measurement('length', 'm', {
78
82
  positive: true,
79
83
  description: 'Total vertical rise.',
80
- }).default(2.8),
84
+ }).optional(),
81
85
  stepCount: z.number().int().positive().default(14),
82
86
  railingMode: z.enum(RAILING_MODES).default('both'),
83
87
  destinationSlabId: NodeIdSchema.optional(),
@@ -200,7 +204,7 @@ export function registerConstructionTools(server, bridge) {
200
204
  start: points[i],
201
205
  end: points[(i + 1) % points.length],
202
206
  thickness: wallThickness,
203
- height: wallHeight,
207
+ ...(wallHeight !== undefined ? { height: wallHeight } : {}),
204
208
  frontSide: 'exterior',
205
209
  backSide: 'interior',
206
210
  ...(wallMaterialPreset ? { materialPreset: wallMaterialPreset } : {}),
@@ -215,6 +219,11 @@ export function registerConstructionTools(server, bridge) {
215
219
  name: namePrefix ? `${namePrefix} Slab` : undefined,
216
220
  polygon: points,
217
221
  elevation: slabElevation,
222
+ // Grounded solid: underside on the level plane, so the created
223
+ // story slab occupies [0, slabElevation] like the legacy
224
+ // extrude-from-zero model.
225
+ thickness: Math.max(slabElevation, 0),
226
+ recessed: slabElevation < 0,
218
227
  ...(slabMaterialPreset ? { materialPreset: slabMaterialPreset } : {}),
219
228
  metadata: { role: 'story-slab' },
220
229
  });
@@ -223,10 +232,13 @@ export function registerConstructionTools(server, bridge) {
223
232
  }
224
233
  let ceilingId = null;
225
234
  if (createCeiling) {
235
+ // Height-less unless the caller pinned one: a new story ceiling
236
+ // follows the level top automatically.
237
+ const explicitCeilingHeight = ceilingHeight ?? wallHeight;
226
238
  const ceiling = CeilingNode.parse({
227
239
  name: namePrefix ? `${namePrefix} Ceiling` : undefined,
228
240
  polygon: points,
229
- height: ceilingHeight ?? wallHeight,
241
+ ...(explicitCeilingHeight !== undefined ? { height: explicitCeilingHeight } : {}),
230
242
  ...(ceilingMaterialPreset ? { materialPreset: ceilingMaterialPreset } : {}),
231
243
  metadata: { role: 'story-ceiling' },
232
244
  });
@@ -268,12 +280,12 @@ export function registerConstructionTools(server, bridge) {
268
280
  const roofLevel = LevelNode.parse({
269
281
  name: roofLevelLabel,
270
282
  level: roofLevelElevation ?? nextLevelIndex(bridge, buildingId, referenceLevel),
283
+ height: roofLevelHeight ?? Math.max(wallHeight + peakHeight, 0.2),
271
284
  children: [],
272
285
  metadata: {
273
286
  role: 'roof',
274
287
  label: roofLevelLabel,
275
288
  referenceLevelId: levelId,
276
- height: roofLevelHeight ?? Math.max(wallHeight + peakHeight, 0.2),
277
289
  },
278
290
  });
279
291
  targetRoofLevelId = roofLevel.id;
@@ -325,15 +337,7 @@ export function registerConstructionTools(server, bridge) {
325
337
  if (isRoofLevel(fromLevel) || isRoofLevel(toLevel)) {
326
338
  throw new Error('Roof support levels are not occupied stories; create a separate occupied attic/story level if a stair-accessible attic is required');
327
339
  }
328
- const segment = StairSegmentNode.parse({
329
- segmentType: 'stair',
330
- width,
331
- length: runLength,
332
- height: totalRise,
333
- stepCount,
334
- ...(materialPreset ? { materialPreset } : {}),
335
- });
336
- const stair = StairNode.parse({
340
+ const stairDraft = StairNode.parse({
337
341
  name: name ?? 'Stair',
338
342
  position: position,
339
343
  rotation,
@@ -343,15 +347,33 @@ export function registerConstructionTools(server, bridge) {
343
347
  slabOpeningMode: 'none',
344
348
  openingOffset,
345
349
  width,
346
- totalRise,
350
+ ...(totalRise !== undefined ? { totalRise } : {}),
347
351
  stepCount,
348
352
  railingMode,
349
- children: [segment.id],
353
+ children: [],
350
354
  ...(materialPreset ? { materialPreset } : {}),
351
355
  metadata: {
352
356
  openingManaged: 'manual-rectangular',
353
357
  },
354
358
  });
359
+ const riseNodes = {
360
+ ...bridge.getNodes(),
361
+ [fromLevel.id]: {
362
+ ...fromLevel,
363
+ children: [...fromLevel.children, stairDraft.id],
364
+ },
365
+ [stairDraft.id]: stairDraft,
366
+ };
367
+ const resolvedTotalRise = resolveStairTotalRise(stairDraft, riseNodes);
368
+ const segment = StairSegmentNode.parse({
369
+ segmentType: 'stair',
370
+ width,
371
+ length: runLength,
372
+ height: resolvedTotalRise,
373
+ stepCount,
374
+ ...(materialPreset ? { materialPreset } : {}),
375
+ });
376
+ const stair = { ...stairDraft, children: [segment.id] };
355
377
  const openingPolygon = rectangularOpening({
356
378
  position: position,
357
379
  rotation,
@@ -1 +1 @@
1
- {"version":3,"file":"create-level.d.ts","sourceRoot":"","sources":["../../src/tools/create-level.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,gBAAgB;;;;;CAQ5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0CpF"}
1
+ {"version":3,"file":"create-level.d.ts","sourceRoot":"","sources":["../../src/tools/create-level.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,gBAAgB;;;;;CAW5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA+CpF"}
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LEVEL_HEIGHT } from '@pascal-app/core';
1
2
  import { LevelNode } from '@pascal-app/core/schema';
2
3
  import { z } from 'zod';
3
4
  import { ErrorCode, throwMcpError } from './errors';
@@ -6,10 +7,13 @@ import { measurement } from './measurement';
6
7
  import { NodeIdSchema } from './schemas';
7
8
  export const createLevelInput = {
8
9
  buildingId: NodeIdSchema,
9
- elevation: z.number().optional(),
10
+ elevation: z
11
+ .number()
12
+ .optional()
13
+ .describe("Legacy parameter; new levels are appended above the building's current top level."),
10
14
  height: measurement('length', 'm', {
11
15
  min: 0,
12
- description: 'Level height (stored in metadata).',
16
+ description: 'Stored floor-to-floor storey height.',
13
17
  }).optional(),
14
18
  label: z.string().optional(),
15
19
  };
@@ -19,10 +23,10 @@ export const createLevelOutput = {
19
23
  export function registerCreateLevel(server, bridge) {
20
24
  server.registerTool('create_level', {
21
25
  title: 'Create level',
22
- description: 'Create a new level node attached to the given building. height and label are stored in metadata.',
26
+ description: "Append a new level above the given building's current top level. height is stored as the level's floor-to-floor storey height.",
23
27
  inputSchema: createLevelInput,
24
28
  outputSchema: createLevelOutput,
25
- }, async ({ buildingId, elevation, height, label }) => {
29
+ }, async ({ buildingId, height, label }) => {
26
30
  const parent = bridge.getNode(buildingId);
27
31
  if (!parent) {
28
32
  throwMcpError(ErrorCode.InvalidParams, `Building not found: ${buildingId}`);
@@ -31,12 +35,16 @@ export function registerCreateLevel(server, bridge) {
31
35
  throwMcpError(ErrorCode.InvalidParams, `Node ${buildingId} is a ${parent.type}, expected building`);
32
36
  }
33
37
  const metadata = {};
34
- if (height !== undefined)
35
- metadata.height = height;
36
38
  if (label !== undefined)
37
39
  metadata.label = label;
40
+ const existingOrdinals = bridge
41
+ .getChildren(buildingId)
42
+ .filter((node) => node.type === 'level')
43
+ .map((node) => node.level);
44
+ const nextOrdinal = Math.max(-1, ...existingOrdinals) + 1;
38
45
  const levelNode = LevelNode.parse({
39
- level: elevation ?? 0,
46
+ level: nextOrdinal,
47
+ height: height ?? DEFAULT_LEVEL_HEIGHT,
40
48
  children: [],
41
49
  ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
42
50
  ...(label !== undefined ? { name: label } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"describe-node.d.ts","sourceRoot":"","sources":["../../src/tools/describe-node.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,iBAAiB;;CAE7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AA0CD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAwCrF"}
1
+ {"version":3,"file":"describe-node.d.ts","sourceRoot":"","sources":["../../src/tools/describe-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AA0CD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDrF"}
@@ -1,5 +1,7 @@
1
+ import { resolveCeilingHeight } from '@pascal-app/core';
1
2
  import { z } from 'zod';
2
3
  import { ErrorCode, throwMcpError } from './errors';
4
+ import { resolveReportedWallHeight } from './scene-query';
3
5
  import { NodeIdSchema } from './schemas';
4
6
  export const describeNodeInput = {
5
7
  id: NodeIdSchema,
@@ -17,13 +19,13 @@ export const describeNodeOutput = {
17
19
  * Build a short, human-readable one-liner describing the node.
18
20
  * Covers the common shapes; falls back to a generic sentence otherwise.
19
21
  */
20
- function describe(node) {
22
+ function describe(node, bridge) {
21
23
  switch (node.type) {
22
24
  case 'wall': {
23
25
  const [x1, z1] = node.start;
24
26
  const [x2, z2] = node.end;
25
27
  const t = node.thickness ?? 0.1;
26
- const h = node.height ?? 2.5;
28
+ const h = resolveReportedWallHeight(bridge, node);
27
29
  return `Wall from (${x1},${z1}) to (${x2},${z2}), thickness ${t.toFixed(2)}m, height ${h.toFixed(2)}m`;
28
30
  }
29
31
  case 'level':
@@ -39,7 +41,7 @@ function describe(node) {
39
41
  case 'slab':
40
42
  return `Slab with ${node.polygon.length} vertices`;
41
43
  case 'ceiling':
42
- return `Ceiling with ${node.polygon.length} vertices, height ${node.height.toFixed(2)}m`;
44
+ return `Ceiling with ${node.polygon.length} vertices, height ${resolveCeilingHeight(node, bridge.getNodes()).toFixed(2)}m`;
43
45
  case 'door':
44
46
  return `Door (${node.width.toFixed(2)}m x ${node.height.toFixed(2)}m)`;
45
47
  case 'window':
@@ -69,14 +71,21 @@ export function registerDescribeNode(server, bridge) {
69
71
  const children = bridge.getChildren(id);
70
72
  const childrenIds = children.map((n) => n.id);
71
73
  const n = node;
74
+ const properties = n.type === 'wall'
75
+ ? {
76
+ ...n,
77
+ resolvedHeight: resolveReportedWallHeight(bridge, n),
78
+ heightIsExplicit: n.height !== undefined,
79
+ }
80
+ : n;
72
81
  const payload = {
73
82
  id: n.id,
74
83
  type: n.type,
75
84
  parentId: (n.parentId ?? null),
76
85
  ancestryIds,
77
86
  childrenIds,
78
- properties: n,
79
- description: describe(n),
87
+ properties: properties,
88
+ description: describe(n, bridge),
80
89
  };
81
90
  return {
82
91
  content: [{ type: 'text', text: JSON.stringify(payload) }],
@@ -1 +1 @@
1
- {"version":3,"file":"photo-to-scene.d.ts","sourceRoot":"","sources":["../../../src/tools/photo-to-scene/photo-to-scene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAYxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAa7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AAiTD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6ErF"}
1
+ {"version":3,"file":"photo-to-scene.d.ts","sourceRoot":"","sources":["../../../src/tools/photo-to-scene/photo-to-scene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAYxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAa7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AAmTD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6ErF"}
@@ -40,6 +40,7 @@ const VisionResponseSchema = z.object({
40
40
  start: z.tuple([z.number(), z.number()]),
41
41
  end: z.tuple([z.number(), z.number()]),
42
42
  thickness: z.number().optional(),
43
+ height: z.number().positive().optional(),
43
44
  })),
44
45
  rooms: z.array(z.object({
45
46
  name: z.string(),
@@ -61,13 +62,14 @@ const SYSTEM_PROMPT = `You are a vision assistant that extracts structured floor
61
62
  Your ONLY job: return a JSON object that exactly matches this schema — no prose, no markdown fences.
62
63
 
63
64
  {
64
- "walls": [{ "start": [x, z], "end": [x, z], "thickness": number? }, ...],
65
+ "walls": [{ "start": [x, z], "end": [x, z], "thickness": number?, "height": number? }, ...],
65
66
  "rooms": [{ "name": string, "polygon": [[x,z], ...], "approximateAreaSqM": number? }, ...],
66
67
  "approximateDimensions": { "widthM": number, "depthM": number },
67
68
  "confidence": number 0..1
68
69
  }
69
70
 
70
71
  Coordinates are in metres. Origin can be the floor plan's centre or bottom-left — be consistent.
72
+ Only include a wall height when it is visibly measured or annotated in the image.
71
73
  If the image is unclear, lower the confidence score but still produce your best attempt.
72
74
  DO NOT wrap the JSON in markdown. DO NOT explain. Just output the raw JSON.`;
73
75
  const DATA_URI_RE = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i;
@@ -170,7 +172,7 @@ function buildSceneGraphFromVision(vision, defaultWallThickness, defaultWallHeig
170
172
  const warnings = [];
171
173
  // Build the skeleton: site → building → level.
172
174
  const building = BuildingNode.parse({});
173
- const level = LevelNode.parse({ level: 0 });
175
+ const level = LevelNode.parse({ level: 0, height: defaultWallHeight });
174
176
  const site = SiteNode.parse({ children: [building.id] });
175
177
  // Link parent ids so downstream traversal works.
176
178
  const siteId = site.id;
@@ -208,7 +210,7 @@ function buildSceneGraphFromVision(vision, defaultWallThickness, defaultWallHeig
208
210
  start: w.start,
209
211
  end: w.end,
210
212
  thickness: w.thickness ?? defaultWallThickness,
211
- height: defaultWallHeight,
213
+ ...(w.height !== undefined ? { height: w.height } : {}),
212
214
  });
213
215
  const linkedWall = {
214
216
  ...wall,
@@ -1,4 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { AnyNode } from '@pascal-app/core/schema';
2
3
  import { z } from 'zod';
3
4
  import type { SceneOperations } from '../operations';
4
5
  export declare const levelScopedInput: {
@@ -48,6 +49,9 @@ export declare const verifySceneOutput: {
48
49
  issues: z.ZodArray<z.ZodString>;
49
50
  hasIssues: z.ZodBoolean;
50
51
  };
52
+ export declare function resolveReportedWallHeight(bridge: SceneOperations, wall: Extract<AnyNode, {
53
+ type: 'wall';
54
+ }>): number;
51
55
  export declare function registerListLevels(server: McpServer, bridge: SceneOperations): void;
52
56
  export declare function registerGetLevelSummary(server: McpServer, bridge: SceneOperations): void;
53
57
  export declare function registerGetWalls(server: McpServer, bridge: SceneOperations): void;
@@ -1 +1 @@
1
- {"version":3,"file":"scene-query.d.ts","sourceRoot":"","sources":["../../src/tools/scene-query.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;AAWpD,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAID,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;CAcjC,CAAA;AAED,eAAO,MAAM,cAAc;;;CAG1B,CAAA;AAED,eAAO,MAAM,cAAc;;;CAG1B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;CAY7B,CAAA;AA2YD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAyCnF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgBxF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmBjF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmBjF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAkQpF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAMxF"}
1
+ {"version":3,"file":"scene-query.d.ts","sourceRoot":"","sources":["../../src/tools/scene-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAQxE,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAWpD,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAID,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;CAcjC,CAAA;AAED,eAAO,MAAM,cAAc;;;CAG1B,CAAA;AAED,eAAO,MAAM,cAAc;;;CAG1B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;CAY7B,CAAA;AA8CD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACvC,MAAM,CAgBR;AAkWD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAyCnF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgBxF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmBjF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmBjF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA4PpF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAMxF"}
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_LEVEL_HEIGHT, getStoredLevelHeight, getWallPlaneTop, resolveStairTotalRise, resolveWallEffectiveHeight, } from '@pascal-app/core';
2
+ import { computeWallSlabSupport } from '@pascal-app/core/spatial-grid';
1
3
  import { z } from 'zod';
2
4
  import { distance2D, pointInPolygon, polygonArea, polygonContainsPolygon, wallLength, } from './geometry';
3
5
  import { NodeIdSchema } from './schemas';
@@ -71,6 +73,19 @@ function getDefaultLevelId(bridge, requested) {
71
73
  function nodesOnLevel(bridge, levelId) {
72
74
  return Object.values(bridge.getNodes()).filter((node) => node.id !== levelId && bridge.resolveLevelId(node.id) === levelId);
73
75
  }
76
+ export function resolveReportedWallHeight(bridge, wall) {
77
+ const levelId = bridge.resolveLevelId(wall.id);
78
+ // Covering-clamped plane for plane-bound walls; explicit heights pass
79
+ // through resolveWallEffectiveHeight untouched.
80
+ const planeTop = levelId
81
+ ? getWallPlaneTop(wall, levelId, bridge.getNodes())
82
+ : DEFAULT_LEVEL_HEIGHT;
83
+ const levelNodes = levelId ? nodesOnLevel(bridge, levelId) : [];
84
+ const slabs = levelNodes.filter((node) => node.type === 'slab');
85
+ const walls = levelNodes.filter((node) => node.type === 'wall');
86
+ const support = computeWallSlabSupport(wall, slabs, walls, wall.supportSlabId);
87
+ return resolveWallEffectiveHeight(wall, planeTop, support.elevation);
88
+ }
74
89
  function metadataRecord(node) {
75
90
  return typeof node.metadata === 'object' && node.metadata !== null
76
91
  ? node.metadata
@@ -116,6 +131,7 @@ function wallSummary(bridge, wall) {
116
131
  if (wall.type !== 'wall')
117
132
  return null;
118
133
  const length = distance2D(wall.start, wall.end);
134
+ const resolvedHeight = resolveReportedWallHeight(bridge, wall);
119
135
  return {
120
136
  id: wall.id,
121
137
  name: wall.name,
@@ -123,6 +139,8 @@ function wallSummary(bridge, wall) {
123
139
  end: wall.end,
124
140
  length: Math.round(length * 100) / 100,
125
141
  height: wall.height,
142
+ resolvedHeight,
143
+ heightIsExplicit: wall.height !== undefined,
126
144
  thickness: wall.thickness,
127
145
  openings: openingSummaries(bridge, wall.id),
128
146
  };
@@ -235,7 +253,7 @@ function stairFootprintPolygons(bridge, stair) {
235
253
  {
236
254
  width: stair.width ?? 1,
237
255
  length: 3,
238
- height: stair.totalRise ?? 2.5,
256
+ height: resolveStairTotalRise(stair, nodes),
239
257
  stepCount: stair.stepCount ?? 10,
240
258
  attachmentSide: 'front',
241
259
  },
@@ -524,14 +542,9 @@ export function registerVerifyScene(server, bridge) {
524
542
  const summary = levels.find((entry) => entry.levelId === level.id);
525
543
  if (!summary?.isOccupiedStory)
526
544
  continue;
527
- const expectedHeight = typeof level.metadata === 'object' &&
528
- level.metadata !== null &&
529
- 'height' in level.metadata &&
530
- typeof level.metadata.height === 'number'
531
- ? level.metadata.height
532
- : 3.2;
545
+ const expectedHeight = getStoredLevelHeight(level);
533
546
  for (const wall of nodesOnLevel(bridge, level.id).filter((node) => node.type === 'wall')) {
534
- const wallHeight = wall.height ?? 2.5;
547
+ const wallHeight = resolveReportedWallHeight(bridge, wall);
535
548
  if (wallHeight > expectedHeight + 0.25) {
536
549
  issues.push(`Wall ${wall.name ?? wall.id} on ${level.name ?? level.id} is ${wallHeight}m high; multi-story exterior walls should be split into level-owned story walls`);
537
550
  }
@@ -558,7 +571,7 @@ export function registerVerifyScene(server, bridge) {
558
571
  if (localX - width / 2 < -0.01 || localX + width / 2 > length + 0.01) {
559
572
  issues.push(`${node.type} ${node.id} extends outside wall ${parent.id}`);
560
573
  }
561
- const wallHeight = parent.height ?? 2.5;
574
+ const wallHeight = resolveReportedWallHeight(bridge, parent);
562
575
  const bottom = node.position[1] - height / 2;
563
576
  const top = node.position[1] + height / 2;
564
577
  if (bottom < -0.01 || top > wallHeight + 0.01) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/mcp",
3
- "version": "0.3.2",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Model Context Protocol server for Pascal 3D editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "prepublishOnly": "bun run build && bun test"
56
56
  },
57
57
  "peerDependencies": {
58
- "@pascal-app/core": "^0.9.2"
58
+ "@pascal-app/core": "^1.0.0-beta.1"
59
59
  },
60
60
  "dependencies": {
61
61
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -63,7 +63,7 @@
63
63
  "zod": "^4.3.5"
64
64
  },
65
65
  "devDependencies": {
66
- "@pascal-app/core": "^0.9.2",
66
+ "@pascal-app/core": "^1.0.0-beta.1",
67
67
  "@pascal/typescript-config": "*",
68
68
  "@types/node": "^22.19.20",
69
69
  "typescript": "6.0.3"