@pascal-app/mcp 0.2.0 → 0.3.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.
File without changes
@@ -1,12 +1,10 @@
1
1
  import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
2
2
  /**
3
- * `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs,
4
- * but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
5
- * `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
6
- * cloned graph validating against `AnyNode`, re-embed the site children from
7
- * the flat dict.
8
- *
9
- * Pure: returns a new graph without mutating the input.
3
+ * Previously re-embedded `SiteNode.children` from flat IDs back to full node
4
+ * objects to match the old schema. Since `SiteNode.children` is now
5
+ * `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already
6
+ * produce the correct form — this function is a no-op kept for call-site
7
+ * compatibility.
10
8
  */
11
9
  export declare function rehydrateSiteChildren(graph: SceneGraph): SceneGraph;
12
10
  //# sourceMappingURL=rehydrate-site-children.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rehydrate-site-children.d.ts","sourceRoot":"","sources":["../../src/lib/rehydrate-site-children.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAGpE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA2BnE"}
1
+ {"version":3,"file":"rehydrate-site-children.d.ts","sourceRoot":"","sources":["../../src/lib/rehydrate-site-children.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAEpE;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAEnE"}
@@ -1,40 +1,10 @@
1
1
  /**
2
- * `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs,
3
- * but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
4
- * `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
5
- * cloned graph validating against `AnyNode`, re-embed the site children from
6
- * the flat dict.
7
- *
8
- * Pure: returns a new graph without mutating the input.
2
+ * Previously re-embedded `SiteNode.children` from flat IDs back to full node
3
+ * objects to match the old schema. Since `SiteNode.children` is now
4
+ * `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already
5
+ * produce the correct form — this function is a no-op kept for call-site
6
+ * compatibility.
9
7
  */
10
8
  export function rehydrateSiteChildren(graph) {
11
- const out = {
12
- nodes: { ...graph.nodes },
13
- rootNodeIds: [...graph.rootNodeIds],
14
- ...(graph.collections ? { collections: graph.collections } : {}),
15
- };
16
- for (const [id, node] of Object.entries(out.nodes)) {
17
- if (node.type !== 'site')
18
- continue;
19
- const childrenField = node.children;
20
- if (!Array.isArray(childrenField))
21
- continue;
22
- const rehydrated = [];
23
- for (const child of childrenField) {
24
- if (typeof child === 'string') {
25
- const target = out.nodes[child];
26
- if (target && (target.type === 'building' || target.type === 'item')) {
27
- rehydrated.push(target);
28
- }
29
- }
30
- else if (child && typeof child === 'object' && 'id' in child) {
31
- rehydrated.push(child);
32
- }
33
- }
34
- out.nodes[id] = {
35
- ...node,
36
- children: rehydrated,
37
- };
38
- }
39
- return out;
9
+ return graph;
40
10
  }
@@ -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;AAgQpE,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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAsPpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
@@ -210,13 +210,6 @@ function buildTemplate() {
210
210
  for (const node of [n.site, n.building, n.level, ...n.walls, n.zone, n.door, n.window]) {
211
211
  nodes[node.id] = node;
212
212
  }
213
- // SiteNode.children is a discriminatedUnion of BuildingNode/ItemNode objects
214
- // (not string ids) — so the site must embed the full building node. The
215
- // rest of the tree uses string ids per the BaseNode/LevelNode/WallNode
216
- // schemas. We mutate the flat-dict copy of the site here so the nested
217
- // representation round-trips through AnyNode.safeParse.
218
- const siteInDict = nodes['site_empty'];
219
- siteInDict.children = [nodes['building_empty']];
220
213
  return {
221
214
  nodes,
222
215
  rootNodeIds: ['site_empty'],
@@ -1 +1 @@
1
- {"version":3,"file":"garden-house.d.ts","sourceRoot":"","sources":["../../src/templates/garden-house.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAmRpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
1
+ {"version":3,"file":"garden-house.d.ts","sourceRoot":"","sources":["../../src/templates/garden-house.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AA+QpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
@@ -228,7 +228,6 @@ function buildTemplate() {
228
228
  'fence_w',
229
229
  ],
230
230
  };
231
- nodes.site_garden.children = [nodes.building_garden];
232
231
  return {
233
232
  nodes: nodes,
234
233
  rootNodeIds: ['site_garden'],
@@ -1 +1 @@
1
- {"version":3,"file":"two-bedroom.d.ts","sourceRoot":"","sources":["../../src/templates/two-bedroom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AA+SpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
1
+ {"version":3,"file":"two-bedroom.d.ts","sourceRoot":"","sources":["../../src/templates/two-bedroom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AA2SpE,eAAO,MAAM,QAAQ,EAAE,UAA4B,CAAA;AAEnD,eAAO,MAAM,QAAQ;;;;CAKX,CAAA"}
@@ -256,7 +256,6 @@ function buildTemplate() {
256
256
  'zone_bed2',
257
257
  ],
258
258
  };
259
- nodes.site_2br.children = [nodes.building_2br];
260
259
  return {
261
260
  nodes: nodes,
262
261
  rootNodeIds: ['site_2br'],
@@ -41,7 +41,7 @@ export declare const createRoofInput: {
41
41
  dutch: "dutch";
42
42
  mansard: "mansard";
43
43
  }>>;
44
- roofHeight: z.ZodDefault<z.ZodNumber>;
44
+ pitch: z.ZodDefault<z.ZodNumber>;
45
45
  wallHeight: z.ZodDefault<z.ZodNumber>;
46
46
  wallThickness: z.ZodDefault<z.ZodNumber>;
47
47
  overhang: z.ZodDefault<z.ZodNumber>;
@@ -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;AAYxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAOpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAajC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CAiB3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAqBzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA8S1F"}
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;AAOpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAajC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CAiB3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAqBzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAiT1F"}
@@ -1,4 +1,4 @@
1
- import { CeilingNode, LevelNode, RoofNode, RoofSegmentNode, SlabNode, StairNode, StairSegmentNode, WallNode, } from '@pascal-app/core/schema';
1
+ import { CeilingNode, getActiveRoofHeight, LevelNode, RoofNode, RoofSegmentNode, SlabNode, StairNode, StairSegmentNode, WallNode, } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
3
  import { publishLiveSceneSnapshot } from './live-sync';
4
4
  import { NodeIdSchema, Vec2Schema, Vec3Schema } from './schemas';
@@ -36,7 +36,7 @@ export const createRoofInput = {
36
36
  width: z.number().positive(),
37
37
  depth: z.number().positive(),
38
38
  roofType: z.enum(ROOF_TYPES).default('hip'),
39
- roofHeight: z.number().positive().default(1.8),
39
+ pitch: z.number().min(0).max(85).default(35),
40
40
  wallHeight: z.number().min(0).default(0.35),
41
41
  wallThickness: z.number().positive().default(0.16),
42
42
  overhang: z.number().min(0).default(0.45),
@@ -222,7 +222,10 @@ export function registerConstructionTools(server, bridge) {
222
222
  description: 'Create a roof container with one roof segment. By default creates a dedicated roof level above the reference level so exploded/solo level views can isolate the roof.',
223
223
  inputSchema: createRoofInput,
224
224
  outputSchema: createRoofOutput,
225
- }, async ({ levelId, roofLevelId, useDedicatedRoofLevel, roofLevelLabel, roofLevelElevation, roofLevelHeight, center, width, depth, roofType, roofHeight, wallHeight, wallThickness, overhang, materialPreset, name, }) => {
225
+ }, async ({ levelId, roofLevelId, useDedicatedRoofLevel, roofLevelLabel, roofLevelElevation, roofLevelHeight, center, width, depth, roofType, pitch, wallHeight, wallThickness, overhang, materialPreset, name, }) => {
226
+ // Peak height is derived from pitch + footprint + type; we still
227
+ // need it to size the auto-generated roof level container below.
228
+ const peakHeight = getActiveRoofHeight({ roofType, pitch, width, depth });
226
229
  const referenceLevel = assertNode(bridge, levelId, 'level');
227
230
  const patches = [];
228
231
  let targetRoofLevelId = levelId;
@@ -244,7 +247,7 @@ export function registerConstructionTools(server, bridge) {
244
247
  role: 'roof',
245
248
  label: roofLevelLabel,
246
249
  referenceLevelId: levelId,
247
- height: roofLevelHeight ?? Math.max(wallHeight + roofHeight, 0.2),
250
+ height: roofLevelHeight ?? Math.max(wallHeight + peakHeight, 0.2),
248
251
  },
249
252
  });
250
253
  targetRoofLevelId = roofLevel.id;
@@ -256,7 +259,7 @@ export function registerConstructionTools(server, bridge) {
256
259
  width,
257
260
  depth,
258
261
  wallHeight,
259
- roofHeight,
262
+ pitch,
260
263
  wallThickness,
261
264
  overhang,
262
265
  ...(materialPreset ? { materialPreset } : {}),
@@ -5,10 +5,10 @@ export declare const findNodesInput: {
5
5
  type: z.ZodOptional<z.ZodEnum<{
6
6
  site: "site";
7
7
  building: "building";
8
- item: "item";
9
8
  level: "level";
10
9
  wall: "wall";
11
10
  fence: "fence";
11
+ item: "item";
12
12
  zone: "zone";
13
13
  slab: "slab";
14
14
  stair: "stair";
@@ -164,7 +164,7 @@ function buildSceneGraphFromVision(vision, defaultWallThickness, defaultWallHeig
164
164
  // Build the skeleton: site → building → level.
165
165
  const building = BuildingNode.parse({});
166
166
  const level = LevelNode.parse({ level: 0 });
167
- const site = SiteNode.parse({ children: [building] });
167
+ const site = SiteNode.parse({ children: [building.id] });
168
168
  // Link parent ids so downstream traversal works.
169
169
  const siteId = site.id;
170
170
  const buildingId = building.id;
@@ -1 +1 @@
1
- {"version":3,"file":"generate-variants.d.ts","sourceRoot":"","sources":["../../../src/tools/variants/generate-variants.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,kBAAkB,CAAA;AAcvD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;CAYjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;CAWlC,CAAA;AAmDD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA4FzF"}
1
+ {"version":3,"file":"generate-variants.d.ts","sourceRoot":"","sources":["../../../src/tools/variants/generate-variants.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,kBAAkB,CAAA;AAcvD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;CAYjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;CAWlC,CAAA;AAcD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0FzF"}
@@ -35,45 +35,6 @@ export const generateVariantsOutput = {
35
35
  graph: z.any().optional(),
36
36
  })),
37
37
  };
38
- /**
39
- * `forkSceneGraph` normalises `SiteNode.children` to string IDs, but the
40
- * `SiteNode` schema declares that field as an array of full `BuildingNode` /
41
- * `ItemNode` objects (see CROSS_CUTTING §2). To keep variants validating
42
- * against `AnyNode`, re-embed the site children from the flat dict.
43
- *
44
- * Pure: returns a new graph without mutating the input.
45
- */
46
- function rehydrateSiteChildren(graph) {
47
- const out = {
48
- nodes: { ...graph.nodes },
49
- rootNodeIds: [...graph.rootNodeIds],
50
- ...(graph.collections ? { collections: graph.collections } : {}),
51
- };
52
- for (const [id, node] of Object.entries(out.nodes)) {
53
- if (node.type !== 'site')
54
- continue;
55
- const childrenField = node.children;
56
- if (!Array.isArray(childrenField))
57
- continue;
58
- const rehydrated = [];
59
- for (const child of childrenField) {
60
- if (typeof child === 'string') {
61
- const target = out.nodes[child];
62
- if (target && (target.type === 'building' || target.type === 'item')) {
63
- rehydrated.push(target);
64
- }
65
- }
66
- else if (child && typeof child === 'object' && 'id' in child) {
67
- rehydrated.push(child);
68
- }
69
- }
70
- out.nodes[id] = {
71
- ...node,
72
- children: rehydrated,
73
- };
74
- }
75
- return out;
76
- }
77
38
  /**
78
39
  * Count how many nodes in a graph fail `AnyNode` validation. Used to keep the
79
40
  * tool from returning silently corrupt variants.
@@ -120,8 +81,6 @@ export function registerGenerateVariants(server, bridge) {
120
81
  for (const kind of mutations) {
121
82
  forked = applyMutation(forked, rng, kind);
122
83
  }
123
- // Re-embed site children so variants match the SiteNode schema.
124
- forked = rehydrateSiteChildren(forked);
125
84
  const invalidCount = countInvalidNodes(forked);
126
85
  if (invalidCount > 0) {
127
86
  throwMcpError(ErrorCode.InternalError, `variant_invalid: variant ${i} produced ${invalidCount} invalid node(s)`, { index: i });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Model Context Protocol server for Pascal 3D editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,14 +54,14 @@
54
54
  "prepublishOnly": "bun run build && bun test"
55
55
  },
56
56
  "peerDependencies": {
57
- "@pascal-app/core": "^0.7.0"
57
+ "@pascal-app/core": "^0.8.0"
58
58
  },
59
59
  "dependencies": {
60
60
  "@modelcontextprotocol/sdk": "^1.29.0",
61
61
  "zod": "^4.3.5"
62
62
  },
63
63
  "devDependencies": {
64
- "@pascal-app/core": "^0.7.0",
64
+ "@pascal-app/core": "^0.8.0",
65
65
  "@pascal/typescript-config": "*",
66
66
  "@types/node": "^25.5.0",
67
67
  "typescript": "5.9.3"