@pascal-app/mcp 1.0.0-beta.4 → 1.0.0-beta.6

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 (45) hide show
  1. package/README.md +56 -44
  2. package/dist/bin/pascal-mcp.d.ts +1 -1
  3. package/dist/bin/pascal-mcp.js +19 -12
  4. package/dist/bridge/scene-bridge.d.ts +6 -2
  5. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  6. package/dist/bridge/scene-bridge.js +21 -7
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/operations/scene-operations.js +1 -0
  11. package/dist/prompts/from-brief.d.ts.map +1 -1
  12. package/dist/prompts/from-brief.js +4 -3
  13. package/dist/resources/agent-guide.d.ts.map +1 -1
  14. package/dist/resources/agent-guide.js +2 -0
  15. package/dist/server.d.ts.map +1 -1
  16. package/dist/server.js +3 -2
  17. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  18. package/dist/storage/sqlite-scene-store.js +10 -1
  19. package/dist/templates/garden-house.d.ts.map +1 -1
  20. package/dist/templates/garden-house.js +4 -1
  21. package/dist/tools/check-collisions.d.ts.map +1 -1
  22. package/dist/tools/check-collisions.js +23 -35
  23. package/dist/tools/door-clearance.d.ts +119 -0
  24. package/dist/tools/door-clearance.d.ts.map +1 -0
  25. package/dist/tools/door-clearance.js +240 -0
  26. package/dist/tools/layout-clearance.d.ts +103 -0
  27. package/dist/tools/layout-clearance.d.ts.map +1 -0
  28. package/dist/tools/layout-clearance.js +186 -0
  29. package/dist/tools/measure.d.ts.map +1 -1
  30. package/dist/tools/measure.js +6 -2
  31. package/dist/tools/room-tools.d.ts.map +1 -1
  32. package/dist/tools/room-tools.js +90 -34
  33. package/dist/tools/scene-query.d.ts +1 -0
  34. package/dist/tools/scene-query.d.ts.map +1 -1
  35. package/dist/tools/scene-query.js +6 -0
  36. package/dist/tools/templates/create-from-template.d.ts +2 -0
  37. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  38. package/dist/tools/templates/create-from-template.js +2 -0
  39. package/dist/transports/http.d.ts +6 -1
  40. package/dist/transports/http.d.ts.map +1 -1
  41. package/dist/transports/http.js +52 -9
  42. package/dist/version.d.ts +2 -0
  43. package/dist/version.d.ts.map +1 -0
  44. package/dist/version.js +3 -0
  45. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"room-tools.d.ts","sourceRoot":"","sources":["../../src/tools/room-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAWxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AA2BpD,eAAO,MAAM,iBAAiB;;;CAG7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;CAG9B,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;CAa3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAQxB,CAAA;AAED,eAAO,MAAM,aAAa;;;;;;;;CAQzB,CAAA;AAED,eAAO,MAAM,cAAc;;;;;;;CAU1B,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;CAS3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;CAI7B,CAAA;AA+PD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsB5D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAqDnF;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA2ChF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0ClF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAsEpF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAMlF"}
1
+ {"version":3,"file":"room-tools.d.ts","sourceRoot":"","sources":["../../src/tools/room-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAWxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AA4BpD,eAAO,MAAM,iBAAiB;;;CAG7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;CAG9B,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;CAa3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAQxB,CAAA;AAED,eAAO,MAAM,aAAa;;;;;;;;CAQzB,CAAA;AAED,eAAO,MAAM,cAAc;;;;;;;CAU1B,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;CAS3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;CAI7B,CAAA;AA4QD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsB5D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAqDnF;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA2ChF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0ClF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAuHpF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAMlF"}
@@ -1,8 +1,10 @@
1
1
  import { CeilingNode, DoorNode, ItemNode, SlabNode, WallNode, WindowNode, ZoneNode, } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
3
  import { findCatalogItem, searchCatalogItems } from './asset-catalog';
4
+ import { keepoutCoversPlanned, keepoutForPolygonEdge } from './door-clearance';
4
5
  import { ErrorCode, throwMcpError } from './errors';
5
- import { pointInBoundsWithPadding, polygonArea, polygonBounds, wallLength, wallLocalXFromT, } from './geometry';
6
+ import { polygonArea, polygonBounds, wallLength, wallLocalXFromT } from './geometry';
7
+ import { collectDoorKeepouts, collectOccupiedFootprints, findValidPlacement, itemPlanAabb, } from './layout-clearance';
6
8
  import { publishLiveSceneSnapshot } from './live-sync';
7
9
  import { measurement } from './measurement';
8
10
  import { NodeIdSchema, Vec2Schema } from './schemas';
@@ -172,19 +174,6 @@ function makeItemAsset(asset) {
172
174
  ...(asset.interactive ? { interactive: asset.interactive } : {}),
173
175
  };
174
176
  }
175
- function itemFootprint(asset, x, z, rotationDeg = 0) {
176
- const [w = 1, , d = 1] = asset.dimensions ?? [1, 1, 1];
177
- const rot = (rotationDeg * Math.PI) / 180;
178
- const cos = Math.abs(Math.cos(rot));
179
- const sin = Math.abs(Math.sin(rot));
180
- const halfW = (w * cos + d * sin) / 2;
181
- const halfD = (w * sin + d * cos) / 2;
182
- return { minX: x - halfW, maxX: x + halfW, minZ: z - halfD, maxZ: z + halfD };
183
- }
184
- function footprintsOverlap(a, b) {
185
- const gap = 0.08;
186
- return (a.maxX - gap > b.minX && a.minX + gap < b.maxX && a.maxZ - gap > b.minZ && a.minZ + gap < b.maxZ);
187
- }
188
177
  function buildRoomPlacements(roomType, polygon, doorWallIndex = 0) {
189
178
  const bounds = polygonBounds(polygon);
190
179
  const n = polygon.length;
@@ -232,11 +221,25 @@ function buildRoomPlacements(roomType, polygon, doorWallIndex = 0) {
232
221
  const area = polygonArea(polygon);
233
222
  const addBack = (assetId, inset, lateral = 0, rotationDeg = facingRot) => {
234
223
  const [x, z] = backPos(inset, lateral);
235
- placements.push({ assetId, x, z, rotationDeg });
224
+ placements.push({
225
+ assetId,
226
+ x,
227
+ z,
228
+ rotationDeg,
229
+ along: { x: ax, z: az },
230
+ inward: { x: inX, z: inZ },
231
+ });
236
232
  };
237
233
  const addSide = (assetId, inset, lateral = 0, rotationDeg = sideRot) => {
238
234
  const [x, z] = sidePos(inset, lateral);
239
- placements.push({ assetId, x, z, rotationDeg });
235
+ placements.push({
236
+ assetId,
237
+ x,
238
+ z,
239
+ rotationDeg,
240
+ along: { x: sax, z: saz },
241
+ inward: { x: snX, z: snZ },
242
+ });
240
243
  };
241
244
  switch (roomType) {
242
245
  case 'bedroom': {
@@ -272,14 +275,23 @@ function buildRoomPlacements(roomType, polygon, doorWallIndex = 0) {
272
275
  addBack('sofa', 0.9);
273
276
  addBack('coffee-table', 2.1);
274
277
  addSide('livingroom-chair', 0.85, -sideAlongLen * 0.18);
278
+ // TV faces the sofa from the door wall: use door-wall axes so smart
279
+ // re-place nudges into the room (along wall / inward), not world X/Z.
275
280
  const doorIdx = doorWallIndex % n;
276
281
  const doorStart = polygon[doorIdx];
277
282
  const doorEnd = polygon[(doorIdx + 1) % n];
283
+ const doorMidX = (doorStart[0] + doorEnd[0]) / 2;
284
+ const doorMidZ = (doorStart[1] + doorEnd[1]) / 2;
285
+ // Door-wall inward is opposite of "back wall" inward (into room from door).
286
+ const doorInX = -inX;
287
+ const doorInZ = -inZ;
278
288
  placements.push({
279
289
  assetId: 'tv-stand',
280
- x: (doorStart[0] + doorEnd[0]) / 2 - inX * 0.35,
281
- z: (doorStart[1] + doorEnd[1]) / 2 - inZ * 0.35,
290
+ x: doorMidX + doorInX * 0.35,
291
+ z: doorMidZ + doorInZ * 0.35,
282
292
  rotationDeg: facingRot + 180,
293
+ along: { x: ax, z: az },
294
+ inward: { x: doorInX, z: doorInZ },
283
295
  });
284
296
  break;
285
297
  }
@@ -459,41 +471,85 @@ export function registerAddWindow(server, bridge) {
459
471
  export function registerFurnishRoom(server, bridge) {
460
472
  server.registerTool('furnish_room', {
461
473
  title: 'Furnish room',
462
- description: 'Place realistic furniture for a room type using levelId + polygon, or infer both from zoneId. Parent floor items to the level so they render and validate.',
474
+ description: 'Place furniture for a room type (levelId+polygon or zoneId). Skips or nudges poses that block door clear zones or overlap existing items (rotation-aware). Parent floor items to the level.',
463
475
  inputSchema: furnishRoomInput,
464
476
  outputSchema: furnishRoomOutput,
465
477
  }, async ({ levelId, zoneId, roomType, polygon, doorWallIndex }) => {
466
478
  const room = inferRoomGeometry(bridge, levelId, polygon, zoneId);
467
479
  assertLevel(bridge, room.levelId);
468
480
  const points = room.polygon;
469
- const { placements, bounds } = buildRoomPlacements(roomType, points, doorWallIndex ?? 0);
470
- const footprints = [];
481
+ const resolvedDoorWallIndex = doorWallIndex ?? 0;
482
+ const { placements, bounds } = buildRoomPlacements(roomType, points, resolvedDoorWallIndex);
471
483
  const skipped = [];
472
484
  const items = [];
485
+ const allNodes = Object.values(bridge.getNodes());
486
+ // Doors on THIS level only (stacked floors must not interact in plan).
487
+ const existingKeepouts = collectDoorKeepouts(allNodes, { levelId: room.levelId });
488
+ const doorKeepoutAabbs = existingKeepouts.map((k) => k.aabb);
489
+ // Always protect this room's door-wall edge when no keep-out already covers it
490
+ // (other rooms may already have doors elsewhere on the same level).
491
+ const planned = keepoutForPolygonEdge(points, resolvedDoorWallIndex, {
492
+ t: 0.5,
493
+ width: 0.9,
494
+ });
495
+ if (planned &&
496
+ !doorKeepoutAabbs.some((existing) => keepoutCoversPlanned(existing, planned))) {
497
+ doorKeepoutAabbs.push(planned);
498
+ }
499
+ // Existing floor items on this level + footprints we place in this batch.
500
+ const occupied = collectOccupiedFootprints(allNodes, {
501
+ levelId: room.levelId,
502
+ floorOnly: true,
503
+ }).map((f) => f.aabb);
504
+ const roomBounds = {
505
+ minX: bounds.minX,
506
+ maxX: bounds.maxX,
507
+ minZ: bounds.minZ,
508
+ maxZ: bounds.maxZ,
509
+ };
473
510
  for (const placement of placements) {
474
511
  const asset = findCatalogItem(placement.assetId);
475
512
  if (!asset) {
476
513
  skipped.push(`${placement.assetId}: asset not found`);
477
514
  continue;
478
515
  }
479
- const fp = itemFootprint(asset, placement.x, placement.z, placement.rotationDeg ?? 0);
480
- const padding = 0.05;
481
- if (!(pointInBoundsWithPadding(fp.minX, fp.minZ, bounds, -padding) &&
482
- pointInBoundsWithPadding(fp.maxX, fp.maxZ, bounds, -padding))) {
483
- skipped.push(`${asset.id}: outside room bounds`);
484
- continue;
485
- }
486
- if (footprints.some((existing) => footprintsOverlap(fp, existing))) {
487
- skipped.push(`${asset.id}: overlaps another item`);
516
+ const primary = {
517
+ x: placement.x,
518
+ z: placement.z,
519
+ rotationDeg: placement.rotationDeg ?? 0,
520
+ };
521
+ const resolved = findValidPlacement({
522
+ primary,
523
+ dimensions: asset.dimensions,
524
+ doorKeepouts: doorKeepoutAabbs,
525
+ occupied,
526
+ roomBounds,
527
+ along: placement.along,
528
+ inward: placement.inward,
529
+ });
530
+ if (!resolved.candidate) {
531
+ const reason = resolved.reason === 'blocks_door_clearance'
532
+ ? 'blocks door clearance'
533
+ : resolved.reason === 'outside_bounds'
534
+ ? 'outside room bounds'
535
+ : 'overlaps another item';
536
+ skipped.push(`${asset.id}: ${reason}`);
488
537
  continue;
489
538
  }
490
- footprints.push(fp);
539
+ const { x, z, rotationDeg } = resolved.candidate;
540
+ const rotRad = (rotationDeg * Math.PI) / 180;
541
+ const planAabb = itemPlanAabb([x, 0, z], asset.dimensions, rotRad);
542
+ occupied.push(planAabb);
491
543
  items.push(ItemNode.parse({
492
544
  name: asset.name,
493
- position: [placement.x, 0, placement.z],
494
- rotation: [0, ((placement.rotationDeg ?? 0) * Math.PI) / 180, 0],
545
+ position: [x, 0, z],
546
+ rotation: [0, rotRad, 0],
495
547
  asset: makeItemAsset(asset),
496
- metadata: { mcpTool: 'furnish_room', roomType },
548
+ metadata: {
549
+ mcpTool: 'furnish_room',
550
+ roomType,
551
+ ...(x !== primary.x || z !== primary.z ? { placementAdjusted: true } : {}),
552
+ },
497
553
  }));
498
554
  }
499
555
  if (items.length > 0) {
@@ -16,6 +16,7 @@ export declare const listLevelsOutput: {
16
16
  export declare const getLevelSummaryOutput: {
17
17
  levelId: z.ZodString;
18
18
  levelName: z.ZodOptional<z.ZodString>;
19
+ floorIndex: z.ZodNumber;
19
20
  role: z.ZodString;
20
21
  metadataRole: z.ZodNullable<z.ZodString>;
21
22
  isOccupiedStory: z.ZodBoolean;
@@ -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;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
+ {"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;AAYpD,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAID,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAA;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;CAejC,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,CAiQpF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAMxF"}
@@ -2,6 +2,7 @@ import { DEFAULT_LEVEL_HEIGHT, getStoredLevelHeight, getWallPlaneTop, resolveSta
2
2
  import { computeWallSlabSupport } from '@pascal-app/core/spatial-grid';
3
3
  import { z } from 'zod';
4
4
  import { distance2D, pointInPolygon, polygonArea, polygonContainsPolygon, wallLength, } from './geometry';
5
+ import { layoutIssuesFromScene } from './layout-clearance';
5
6
  import { NodeIdSchema } from './schemas';
6
7
  export const levelScopedInput = {
7
8
  levelId: NodeIdSchema.optional(),
@@ -18,6 +19,7 @@ export const listLevelsOutput = {
18
19
  export const getLevelSummaryOutput = {
19
20
  levelId: z.string(),
20
21
  levelName: z.string().optional(),
22
+ floorIndex: z.number(),
21
23
  role: z.string(),
22
24
  metadataRole: z.string().nullable(),
23
25
  isOccupiedStory: z.boolean(),
@@ -633,6 +635,10 @@ export function registerVerifyScene(server, bridge) {
633
635
  issues.push(`Schema: ${validation.errors.length - 5} additional validation errors`);
634
636
  }
635
637
  }
638
+ // Door keep-outs + item–item footprint overlaps (rotation-aware).
639
+ for (const layoutIssue of layoutIssuesFromScene(Object.values(bridge.getNodes()))) {
640
+ issues.push(layoutIssue);
641
+ }
636
642
  const payload = {
637
643
  ok: true,
638
644
  valid: validation.valid,
@@ -17,6 +17,8 @@ export declare const createFromTemplateOutput: {
17
17
  templateId: z.ZodString;
18
18
  rootNodeIds: z.ZodArray<z.ZodString>;
19
19
  nodeCount: z.ZodNumber;
20
+ /** Present when `save: true` was requested but no store was attached. */
21
+ saveSkipped: z.ZodOptional<z.ZodBoolean>;
20
22
  /** Present when `save: true` (and a store was available). */
21
23
  scene: z.ZodOptional<z.ZodObject<{
22
24
  id: z.ZodString;
@@ -1 +1 @@
1
- {"version":3,"file":"create-from-template.d.ts","sourceRoot":"","sources":["../../../src/tools/templates/create-from-template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAMvD,eAAO,MAAM,uBAAuB;;;IAYlC;;;;;OAKG;;;CAGJ,CAAA;AAED,eAAO,MAAM,wBAAwB;;;;IAInC,6DAA6D;;;;;;;;;;;;;;;;;;;;;;;;CAuB9D,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA8F3F"}
1
+ {"version":3,"file":"create-from-template.d.ts","sourceRoot":"","sources":["../../../src/tools/templates/create-from-template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAMvD,eAAO,MAAM,uBAAuB;;;IAYlC;;;;;OAKG;;;CAGJ,CAAA;AAED,eAAO,MAAM,wBAAwB;;;;IAInC,yEAAyE;;IAEzE,6DAA6D;;;;;;;;;;;;;;;;;;;;;;;;CAuB9D,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA8F3F"}
@@ -28,6 +28,8 @@ export const createFromTemplateOutput = {
28
28
  templateId: z.string(),
29
29
  rootNodeIds: z.array(z.string()),
30
30
  nodeCount: z.number(),
31
+ /** Present when `save: true` was requested but no store was attached. */
32
+ saveSkipped: z.boolean().optional(),
31
33
  /** Present when `save: true` (and a store was available). */
32
34
  scene: z
33
35
  .object({
@@ -19,6 +19,11 @@ export type HttpTransportOptions = {
19
19
  allowedOrigins?: string[];
20
20
  /** Per-client request cap per minute. Set <= 0 to disable. */
21
21
  rateLimitPerMinute?: number;
22
+ /** Authenticated identity returned from GET /health for local supervisors. */
23
+ health?: {
24
+ version: string;
25
+ instanceId: string;
26
+ };
22
27
  };
23
28
  /**
24
29
  * Attach an `McpServer` to a Streamable HTTP transport bound to a local port.
@@ -32,5 +37,5 @@ export type HttpTransportOptions = {
32
37
  * the underlying Node HTTP server. To bind a public interface, pass `host` and
33
38
  * configure an auth token.
34
39
  */
35
- export declare function connectHttp(server: McpServer, port: number, options?: HttpTransportOptions): Promise<HttpTransportHandle>;
40
+ export declare function connectHttp(createMcpServer: () => McpServer, port: number, options?: HttpTransportOptions): Promise<HttpTransportHandle>;
36
41
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAUxE,MAAM,MAAM,mBAAmB,GAAG;IAChC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAgE9B"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAUxE,MAAM,MAAM,mBAAmB,GAAG;IAChC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,8EAA8E;IAC9E,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;CACjD,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,eAAe,EAAE,MAAM,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA2G9B"}
@@ -18,7 +18,7 @@ const ALLOWED_HEADERS = 'authorization, content-type, mcp-session-id, mcp-protoc
18
18
  * the underlying Node HTTP server. To bind a public interface, pass `host` and
19
19
  * configure an auth token.
20
20
  */
21
- export async function connectHttp(server, port, options = {}) {
21
+ export async function connectHttp(createMcpServer, port, options = {}) {
22
22
  const host = options.host ?? DEFAULT_HOST;
23
23
  const authToken = options.authToken ?? process.env.PASCAL_MCP_HTTP_TOKEN;
24
24
  if (!(isLoopbackHost(host) || authToken)) {
@@ -29,14 +29,11 @@ export async function connectHttp(server, port, options = {}) {
29
29
  allowedOrigins: options.allowedOrigins ?? envAllowedOrigins(),
30
30
  rateLimitPerMinute: options.rateLimitPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE,
31
31
  });
32
- const transport = new StreamableHTTPServerTransport({
33
- sessionIdGenerator: () => randomUUID(),
34
- });
35
- await server.connect(transport);
32
+ const transports = new Map();
36
33
  const httpServer = createServer((req, res) => {
37
34
  if (!guard(req, res))
38
35
  return;
39
- transport.handleRequest(req, res).catch((err) => {
36
+ handleRequest(req, res).catch((err) => {
40
37
  // Log to stderr; never touch stdout (stdio transport uses it).
41
38
  console.error('[pascal-mcp] http transport error', err);
42
39
  if (!res.writableEnded) {
@@ -49,6 +46,51 @@ export async function connectHttp(server, port, options = {}) {
49
46
  }
50
47
  });
51
48
  });
49
+ const handleRequest = async (req, res) => {
50
+ const pathname = req.url ? new URL(req.url, 'http://localhost').pathname : '/';
51
+ if (pathname === '/health') {
52
+ if (!options.health)
53
+ return sendJson(res, 404, { error: 'not_found' });
54
+ if (req.method !== 'GET') {
55
+ res.setHeader('Allow', 'GET');
56
+ return sendJson(res, 405, { error: 'method_not_allowed' });
57
+ }
58
+ return sendJson(res, 200, {
59
+ status: 'ok',
60
+ app: 'mcp',
61
+ version: options.health.version,
62
+ instanceId: options.health.instanceId,
63
+ });
64
+ }
65
+ const sessionId = headerValue(req.headers['mcp-session-id']);
66
+ let transport = sessionId ? transports.get(sessionId) : undefined;
67
+ if (!transport && req.method === 'POST' && !sessionId) {
68
+ let createdTransport;
69
+ createdTransport = new StreamableHTTPServerTransport({
70
+ sessionIdGenerator: () => randomUUID(),
71
+ onsessioninitialized: (id) => {
72
+ transports.set(id, createdTransport);
73
+ },
74
+ });
75
+ createdTransport.onclose = () => {
76
+ const id = createdTransport.sessionId;
77
+ if (id)
78
+ transports.delete(id);
79
+ };
80
+ await createMcpServer().connect(createdTransport);
81
+ transport = createdTransport;
82
+ }
83
+ if (!transport) {
84
+ if (req.method === 'GET' && !sessionId) {
85
+ res.setHeader('Allow', 'POST');
86
+ return sendJson(res, 405, { error: 'session_required' });
87
+ }
88
+ return sendJson(res, 400, { error: 'invalid_session' });
89
+ }
90
+ await transport.handleRequest(req, res);
91
+ if (!transport.sessionId)
92
+ await transport.close();
93
+ };
52
94
  await new Promise((resolve, reject) => {
53
95
  const onError = (err) => {
54
96
  httpServer.off('listening', onListening);
@@ -68,6 +110,8 @@ export async function connectHttp(server, port, options = {}) {
68
110
  host,
69
111
  port: boundPort,
70
112
  close: async () => {
113
+ await Promise.all([...transports.values()].map((transport) => transport.close()));
114
+ transports.clear();
71
115
  await new Promise((resolve, reject) => {
72
116
  httpServer.close((err) => {
73
117
  if (err)
@@ -76,7 +120,6 @@ export async function connectHttp(server, port, options = {}) {
76
120
  resolve();
77
121
  });
78
122
  });
79
- await transport.close();
80
123
  },
81
124
  };
82
125
  }
@@ -97,7 +140,7 @@ function createHttpGuard(options) {
97
140
  return false;
98
141
  }
99
142
  const pathname = req.url ? new URL(req.url, 'http://localhost').pathname : '/';
100
- if (pathname !== '/mcp') {
143
+ if (pathname !== '/mcp' && pathname !== '/health') {
101
144
  sendJson(res, 404, { error: 'not_found' });
102
145
  return false;
103
146
  }
@@ -108,7 +151,7 @@ function createHttpGuard(options) {
108
151
  return false;
109
152
  }
110
153
  }
111
- if (options.rateLimitPerMinute > 0) {
154
+ if (pathname === '/mcp' && options.rateLimitPerMinute > 0) {
112
155
  const now = Date.now();
113
156
  const key = req.socket.remoteAddress ?? 'unknown';
114
157
  const bucket = buckets.get(key);
@@ -0,0 +1,2 @@
1
+ export declare const version: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,OAAO,QAMT,CAAA"}
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from 'node:fs';
2
+ export const version = process.env.PASCAL_MCP_VERSION ??
3
+ JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/mcp",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
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": "^1.0.0-beta.4"
58
+ "@pascal-app/core": "^1.0.0-beta.5"
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": "^1.0.0-beta.4",
66
+ "@pascal-app/core": "^1.0.0-beta.5",
67
67
  "@pascal/typescript-config": "*",
68
68
  "@types/node": "^22.19.20",
69
69
  "typescript": "6.0.3"