opentakeoff-mcp 0.6.0 → 0.6.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.
- package/dist/server-core.js +87 -24
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -1293,48 +1293,101 @@ var Session = class {
|
|
|
1293
1293
|
* exactly like oneClick — just N of them from one call instead of N
|
|
1294
1294
|
* reasoning-heavy round-trips. Same contract as oneClick: no scale → a
|
|
1295
1295
|
* px-only preview per room; no condition → nothing commits (a review
|
|
1296
|
-
* pass, not a proposal-acceptance gate — this server has none).
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1296
|
+
* pass, not a proposal-acceptance gate — this server has none).
|
|
1297
|
+
*
|
|
1298
|
+
* Withholding — nothing is committed until it survives all three, and the
|
|
1299
|
+
* batch NEVER silently drops work: every withheld seed is counted and
|
|
1300
|
+
* reasoned in `withheld`, because a room the tool knows it skipped is a
|
|
1301
|
+
* question the caller can ask, while a room it skipped silently is a hole
|
|
1302
|
+
* in a bid.
|
|
1303
|
+
* 1. degenerate — traced to fewer than 3 vertices.
|
|
1304
|
+
* 2. duplicate — two labels flooding one region (a room tagged twice, or
|
|
1305
|
+
* a legend number landing in the same space) trace to an identical
|
|
1306
|
+
* ring. Committing both double-counts the area with no signal, which
|
|
1307
|
+
* is the worst failure mode an estimating tool has. One region commits
|
|
1308
|
+
* once; the collapsed labels ride along on `merged_labels`.
|
|
1309
|
+
* 3. implausible — a flood trapped inside a room-number bubble, a door
|
|
1310
|
+
* swing, or a wall cavity is fully enclosed, so it traces clean and
|
|
1311
|
+
* `detectRegions` passes it. Area is the only thing that separates it
|
|
1312
|
+
* from a room. Withheld below `minAreaSf` (default 5 SF — smaller than
|
|
1313
|
+
* any real finished space; a broom closet is ~10 SF). Only applied
|
|
1314
|
+
* once a scale exists, since without one there is no real area to
|
|
1315
|
+
* judge and nothing commits anyway. */
|
|
1300
1316
|
async detectRooms(name, opts) {
|
|
1301
1317
|
const s = this.sheet(name);
|
|
1302
1318
|
const mask = await this.ensureMask(name);
|
|
1303
1319
|
if (!mask) throw new UserError("This sheet has no vector linework (likely a scan); raster fallback not yet available in the MCP server.");
|
|
1320
|
+
const minAreaSf = opts.minAreaSf ?? 5;
|
|
1304
1321
|
const seeds = roomLabelSeeds(s.text);
|
|
1305
1322
|
const regions = detectRegions(mask, seeds);
|
|
1306
|
-
const
|
|
1323
|
+
const withheld = { degenerate: 0, duplicate: 0, implausible: 0 };
|
|
1324
|
+
const byRing = /* @__PURE__ */ new Map();
|
|
1325
|
+
const order = [];
|
|
1326
|
+
for (const r of regions) {
|
|
1307
1327
|
const ring = snapVertices(traceRegion(r.flood), (px, py, d) => s.snap ? nearestSnap(s.snap, px, py, d) : null, SNAP_TOL);
|
|
1308
|
-
if (ring.length < 3)
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1328
|
+
if (ring.length < 3) {
|
|
1329
|
+
withheld.degenerate++;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const key = ring.map(([x, y]) => `${Math.round(x)},${Math.round(y)}`).join(";");
|
|
1333
|
+
const seen = byRing.get(key);
|
|
1334
|
+
if (seen) {
|
|
1335
|
+
seen.merged.push(r.str);
|
|
1336
|
+
withheld.duplicate++;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const cand = {
|
|
1312
1340
|
label: r.str,
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1341
|
+
ring,
|
|
1342
|
+
areaPx2: ringArea(ring),
|
|
1343
|
+
perimPx: closedMetrics(ring).perim,
|
|
1344
|
+
seed: r.seed,
|
|
1345
|
+
hatch: !!r.flood.hatchFiltered,
|
|
1346
|
+
merged: []
|
|
1316
1347
|
};
|
|
1317
|
-
|
|
1318
|
-
|
|
1348
|
+
byRing.set(key, cand);
|
|
1349
|
+
order.push(cand);
|
|
1350
|
+
}
|
|
1351
|
+
const upp = s.upp;
|
|
1352
|
+
const rooms = order.map((c) => {
|
|
1353
|
+
const common = {
|
|
1354
|
+
label: c.label,
|
|
1355
|
+
nverts: c.ring.length,
|
|
1356
|
+
...c.merged.length ? { merged_labels: c.merged } : {},
|
|
1357
|
+
...c.hatch ? { hatch_filtered: true } : {},
|
|
1358
|
+
...opts.returnVerts ? { verts: c.ring.map(([vx, vy]) => [round1(vx), round1(vy)]) } : {}
|
|
1359
|
+
};
|
|
1360
|
+
if (upp == null) {
|
|
1361
|
+
return { ...common, area_px2: round1(c.areaPx2), perimeter_px: round1(c.perimPx) };
|
|
1362
|
+
}
|
|
1363
|
+
const area_sf = round2(c.areaPx2 * upp * upp);
|
|
1364
|
+
if (area_sf < minAreaSf) {
|
|
1365
|
+
withheld.implausible++;
|
|
1366
|
+
return null;
|
|
1319
1367
|
}
|
|
1320
|
-
const
|
|
1321
|
-
const area_sf = round2(areaPx2 * upp * upp);
|
|
1322
|
-
const perimeter_lf = round2(perimPx * upp);
|
|
1368
|
+
const perimeter_lf = round2(c.perimPx * upp);
|
|
1323
1369
|
let shape_id;
|
|
1324
1370
|
if (opts.condition) {
|
|
1325
|
-
shape_id = this.commit(s, opts.condition, opts.role, ring, { area_sf, perimeter_lf }, {
|
|
1371
|
+
shape_id = this.commit(s, opts.condition, opts.role, c.ring, { area_sf, perimeter_lf }, {
|
|
1326
1372
|
method: "one_click_v1",
|
|
1327
1373
|
actor: "agent",
|
|
1328
|
-
seed_norm: [
|
|
1374
|
+
seed_norm: [c.seed[0] / s.widthPx, c.seed[1] / s.heightPx],
|
|
1329
1375
|
reviewed: false,
|
|
1330
|
-
...
|
|
1376
|
+
...c.hatch ? { hatch_filtered: true } : {}
|
|
1331
1377
|
}).id;
|
|
1332
1378
|
}
|
|
1333
1379
|
return { ...common, area_sf, perimeter_lf, ...shape_id ? { shape_id } : {} };
|
|
1334
1380
|
}).filter((r) => r !== null);
|
|
1381
|
+
const withheldTotal = withheld.degenerate + withheld.duplicate + withheld.implausible;
|
|
1335
1382
|
return {
|
|
1336
1383
|
detected: rooms.length,
|
|
1337
1384
|
rooms,
|
|
1385
|
+
withheld: {
|
|
1386
|
+
total: withheldTotal,
|
|
1387
|
+
...withheld,
|
|
1388
|
+
...upp != null ? { min_area_sf: minAreaSf } : {}
|
|
1389
|
+
},
|
|
1390
|
+
...withheldTotal ? { note: `${withheldTotal} seed(s) withheld \u2014 ${withheld.duplicate} duplicate region(s), ${withheld.implausible} under ${minAreaSf} SF, ${withheld.degenerate} untraceable. Raise or lower min_area_sf to see more.` } : {},
|
|
1338
1391
|
...s.upp == null ? { warning: `No scale set for ${s.key} \u2014 quantities unavailable. Call set_scale${s.detected ? ` (detected: ${s.detected.label})` : ""}.` } : {}
|
|
1339
1392
|
};
|
|
1340
1393
|
}
|
|
@@ -1462,6 +1515,7 @@ var oneClickOutput = {
|
|
|
1462
1515
|
var detectedRoom = z.object({
|
|
1463
1516
|
label: z.string().describe('The room-number text the seed was read from (e.g. "104", "139A")'),
|
|
1464
1517
|
nverts: z.number().int().describe("Vertex count of the traced polygon"),
|
|
1518
|
+
merged_labels: z.array(z.string()).optional().describe("Other labels that flooded to this same region \u2014 the area is counted once, under `label`"),
|
|
1465
1519
|
hatch_filtered: z.literal(true).optional().describe("Present when hatch/pattern linework was classified out of the boundary"),
|
|
1466
1520
|
verts: z.array(point).optional().describe("Traced polygon vertices (image px), when return_verts was set"),
|
|
1467
1521
|
area_sf: z.number().optional().describe("Scaled mode: traced area in SF"),
|
|
@@ -1473,6 +1527,14 @@ var detectedRoom = z.object({
|
|
|
1473
1527
|
var detectRoomsOutput = {
|
|
1474
1528
|
detected: z.number().int().describe("Count of cleanly-detected rooms \u2014 may be fewer than the labels found on the sheet"),
|
|
1475
1529
|
rooms: z.array(detectedRoom),
|
|
1530
|
+
withheld: z.object({
|
|
1531
|
+
total: z.number().int().describe("Seeds found on the sheet but not reported as rooms"),
|
|
1532
|
+
degenerate: z.number().int().describe("Traced to fewer than 3 vertices"),
|
|
1533
|
+
duplicate: z.number().int().describe("Flooded to a region another label already claimed \u2014 counted once, never twice"),
|
|
1534
|
+
implausible: z.number().int().describe("Enclosed and clean, but smaller than min_area_sf \u2014 a label bubble, door swing, or wall cavity rather than a room"),
|
|
1535
|
+
min_area_sf: z.number().optional().describe("The plausibility floor applied (scaled mode only)")
|
|
1536
|
+
}).describe("What detection skipped and why \u2014 a withheld room is a question the caller can ask; a silently dropped one is a hole in a bid"),
|
|
1537
|
+
note: z.string().optional().describe("Human-readable summary of what was withheld, when anything was"),
|
|
1476
1538
|
warning: z.string().optional().describe("Preview mode (no scale): why quantities are unavailable and what to do")
|
|
1477
1539
|
};
|
|
1478
1540
|
var measurePolygonOutput = {
|
|
@@ -1614,15 +1676,16 @@ function registerTools(server, session) {
|
|
|
1614
1676
|
outputSchema: oneClickOutput
|
|
1615
1677
|
}, run("one_click", (a) => session.oneClick(a.sheet, a.x, a.y, { condition: a.condition, role: a.role, returnVerts: a.return_verts })));
|
|
1616
1678
|
server.registerTool("detect_rooms", {
|
|
1617
|
-
description: `Batch room detection: reads every room-number label off the sheet's text layer (e.g. "134", "OFFICE 101") and runs One-Click at each \u2014 one call instead of read_sheet_text + reasoning + N one_click calls.
|
|
1679
|
+
description: `Batch room detection: reads every room-number label off the sheet's text layer (e.g. "134", "OFFICE 101") and runs One-Click at each \u2014 one call instead of read_sheet_text + reasoning + N one_click calls. A seed is only reported as a room once it survives three gates, and everything skipped is counted and reasoned in \`withheld\` \u2014 never dropped silently, because a room the tool tells you it skipped is a question you can ask, while one it hides is a hole in a bid. The gates: a flood that leaked or landed in dense linework never becomes a region; two labels flooding the SAME region commit once (the extra labels ride on \`merged_labels\` \u2014 double-counting an area is the worst failure an estimating tool has); and a flood that is enclosed and clean but smaller than min_area_sf is a room-number bubble, a door swing, or a wall cavity rather than a room. With the sheet's scale set, returns area_sf/perimeter_lf per room; pass condition to commit every detected room under that finish tag (role "deduct" makes them subtract). Without a scale, returns px-only quantities per room and commits nothing \u2014 the plausibility floor needs real units, so it only applies once a scale is set. ${COORDS}`,
|
|
1618
1680
|
inputSchema: {
|
|
1619
1681
|
sheet: z2.string(),
|
|
1620
1682
|
condition: z2.string().optional().describe("Finish tag to commit every detected room under (minted on first use)"),
|
|
1621
1683
|
role: roleSchema,
|
|
1622
|
-
return_verts: z2.boolean().default(false).describe("Include each traced polygon's vertices (image px)")
|
|
1684
|
+
return_verts: z2.boolean().default(false).describe("Include each traced polygon's vertices (image px)"),
|
|
1685
|
+
min_area_sf: z2.number().positive().default(5).describe("Plausibility floor: enclosed regions smaller than this are withheld as label bubbles/cavities, not rooms. Default 5 SF \u2014 below any real finished space (a broom closet is ~10 SF). Lower it to inspect what was skipped.")
|
|
1623
1686
|
},
|
|
1624
1687
|
outputSchema: detectRoomsOutput
|
|
1625
|
-
}, run("detect_rooms", (a) => session.detectRooms(a.sheet, { condition: a.condition, role: a.role, returnVerts: a.return_verts })));
|
|
1688
|
+
}, run("detect_rooms", (a) => session.detectRooms(a.sheet, { condition: a.condition, role: a.role, returnVerts: a.return_verts, minAreaSf: a.min_area_sf })));
|
|
1626
1689
|
server.registerTool("measure_polygon", {
|
|
1627
1690
|
description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. ${COORDS}`,
|
|
1628
1691
|
inputSchema: {
|
|
@@ -1774,7 +1837,7 @@ function registerResources(server, session) {
|
|
|
1774
1837
|
// package.json
|
|
1775
1838
|
var package_default = {
|
|
1776
1839
|
name: "opentakeoff-mcp",
|
|
1777
|
-
version: "0.6.
|
|
1840
|
+
version: "0.6.1",
|
|
1778
1841
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
1779
1842
|
type: "module",
|
|
1780
1843
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED