@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.
- package/README.md +56 -44
- package/dist/bin/pascal-mcp.d.ts +1 -1
- package/dist/bin/pascal-mcp.js +19 -12
- package/dist/bridge/scene-bridge.d.ts +6 -2
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +21 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/operations/scene-operations.js +1 -0
- package/dist/prompts/from-brief.d.ts.map +1 -1
- package/dist/prompts/from-brief.js +4 -3
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
- package/dist/storage/sqlite-scene-store.js +10 -1
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +4 -1
- package/dist/tools/check-collisions.d.ts.map +1 -1
- package/dist/tools/check-collisions.js +23 -35
- package/dist/tools/door-clearance.d.ts +119 -0
- package/dist/tools/door-clearance.d.ts.map +1 -0
- package/dist/tools/door-clearance.js +240 -0
- package/dist/tools/layout-clearance.d.ts +103 -0
- package/dist/tools/layout-clearance.d.ts.map +1 -0
- package/dist/tools/layout-clearance.js +186 -0
- package/dist/tools/measure.d.ts.map +1 -1
- package/dist/tools/measure.js +6 -2
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +90 -34
- package/dist/tools/scene-query.d.ts +1 -0
- package/dist/tools/scene-query.d.ts.map +1 -1
- package/dist/tools/scene-query.js +6 -0
- package/dist/tools/templates/create-from-template.d.ts +2 -0
- package/dist/tools/templates/create-from-template.d.ts.map +1 -1
- package/dist/tools/templates/create-from-template.js +2 -0
- package/dist/transports/http.d.ts +6 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +52 -9
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- 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;
|
|
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"}
|
package/dist/tools/room-tools.js
CHANGED
|
@@ -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 {
|
|
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({
|
|
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({
|
|
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:
|
|
281
|
-
z:
|
|
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
|
|
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
|
|
470
|
-
const
|
|
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
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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: [
|
|
494
|
-
rotation: [0,
|
|
545
|
+
position: [x, 0, z],
|
|
546
|
+
rotation: [0, rotRad, 0],
|
|
495
547
|
asset: makeItemAsset(asset),
|
|
496
|
-
metadata: {
|
|
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;
|
|
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(
|
|
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;
|
|
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"}
|
package/dist/transports/http.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,OAAO,QAMT,CAAA"}
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/mcp",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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.
|
|
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.
|
|
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"
|