opentakeoff-mcp 0.9.39 → 0.9.40
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 +22 -0
- package/dist/server-core.js +293 -44
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -101,6 +101,28 @@ Each tool call writes one JSON line to stderr with the tool name, duration,
|
|
|
101
101
|
sheet, result size, and error flag. The trace never writes to stdout and never
|
|
102
102
|
includes document text, shape vertices, or result payload content.
|
|
103
103
|
|
|
104
|
+
### Staged tool exposure (opt-in)
|
|
105
|
+
|
|
106
|
+
By default every client gets all 40 tool schemas on `tools/list` — the flat
|
|
107
|
+
contract every published client already expects. Forty descriptions is real
|
|
108
|
+
token weight for an agent session that may never touch half of them, so the
|
|
109
|
+
server can instead stage the surface along the workflow it already teaches:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
OPENTAKEOFF_MCP_STAGED_TOOLS=1 npx -y opentakeoff-mcp
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Staged, only the **setup** stage (load, scale, read the set — 10 tools) starts
|
|
116
|
+
enabled, plus one opener: `open_tool_stage`. Calling it with `"measure"`,
|
|
117
|
+
`"revise"`, or `"handoff"` enables that stage's tools and fires
|
|
118
|
+
`tools/list_changed`, so any client that supports dynamic tool lists (Claude
|
|
119
|
+
Code, Claude Desktop, anything built against the current spec) sees the group
|
|
120
|
+
appear the moment the agent asks for it. Opening is idempotent and never
|
|
121
|
+
closes anything — the surface only grows. The initialize instructions state
|
|
122
|
+
the scheme, so an agent knows to open a stage before it needs one. Requires a
|
|
123
|
+
client that honors `tools/list_changed`; leave the flag unset for one that
|
|
124
|
+
reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/issues/230))
|
|
125
|
+
|
|
104
126
|
## Tools
|
|
105
127
|
|
|
106
128
|
| Tool | What it does |
|
package/dist/server-core.js
CHANGED
|
@@ -1005,12 +1005,12 @@ function circleFitOk(segs, chain, c0, c1) {
|
|
|
1005
1005
|
my /= m;
|
|
1006
1006
|
let sxx = 0, sxy = 0, syy = 0, sxz = 0, syz = 0;
|
|
1007
1007
|
for (let i = 0; i < m; i++) {
|
|
1008
|
-
const x = xs[i] - mx, y = ys[i] - my,
|
|
1008
|
+
const x = xs[i] - mx, y = ys[i] - my, z4 = x * x + y * y;
|
|
1009
1009
|
sxx += x * x;
|
|
1010
1010
|
sxy += x * y;
|
|
1011
1011
|
syy += y * y;
|
|
1012
|
-
sxz += x *
|
|
1013
|
-
syz += y *
|
|
1012
|
+
sxz += x * z4;
|
|
1013
|
+
syz += y * z4;
|
|
1014
1014
|
}
|
|
1015
1015
|
const det = sxx * syy - sxy * sxy;
|
|
1016
1016
|
if (Math.abs(det) < 1e-9) return null;
|
|
@@ -1993,12 +1993,12 @@ function arcClusterFit(cl, mw, mask) {
|
|
|
1993
1993
|
my /= m;
|
|
1994
1994
|
let sxx = 0, sxy = 0, syy = 0, sxz = 0, syz = 0;
|
|
1995
1995
|
for (const i of cl) {
|
|
1996
|
-
const x = X(i) - mx, y = Y(i) - my,
|
|
1996
|
+
const x = X(i) - mx, y = Y(i) - my, z4 = x * x + y * y;
|
|
1997
1997
|
sxx += x * x;
|
|
1998
1998
|
sxy += x * y;
|
|
1999
1999
|
syy += y * y;
|
|
2000
|
-
sxz += x *
|
|
2001
|
-
syz += y *
|
|
2000
|
+
sxz += x * z4;
|
|
2001
|
+
syz += y * z4;
|
|
2002
2002
|
}
|
|
2003
2003
|
const tr = sxx + syy, dsc = Math.sqrt(Math.max(0, ((sxx - syy) / 2) ** 2 + sxy * sxy));
|
|
2004
2004
|
const l1 = tr / 2 + dsc;
|
|
@@ -4860,7 +4860,7 @@ function drawShapes(ctx, toCanvas, shapes, sheetW, sheetH, longEdge) {
|
|
|
4860
4860
|
var SNAP_CELL = 24;
|
|
4861
4861
|
var SNAP_TOL = 7;
|
|
4862
4862
|
var PALETTE = ["#c96442", "#2f7d54", "#2563eb", "#9333ea", "#b8860b", "#0d9488", "#be185d", "#1f2937", "#dc2626", "#0891b2"];
|
|
4863
|
-
var HATCH_IDS = ["solid", "diag", "diag2", "cross", "diagdense", "horiz", "vert", "grid", "brick", "plank", "herring", "basket", "checker", "wave", "dots", "speckle", "iso", "honeycomb", "scan", "plus", "circuit", "topo"];
|
|
4863
|
+
var HATCH_IDS = ["solid", "diag", "diag2", "cross", "diagdense", "horiz", "vert", "grid", "brick", "plank", "herring", "basket", "checker", "wave", "dots", "speckle", "iso", "honeycomb", "scan", "plus", "circuit", "topo", "woodgrain", "chevron", "pinwheel", "harlequin", "hexagon", "penny", "octagondot", "fleur", "concrete"];
|
|
4864
4864
|
var mintUuid2 = () => globalThis.crypto && typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
4865
4865
|
var uid = (p) => `${p}-${mintUuid2()}`;
|
|
4866
4866
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8807,6 +8807,80 @@ function rfiStatus(id) {
|
|
|
8807
8807
|
return STATUS_BY_ID[id] || RFI_STATUSES[0];
|
|
8808
8808
|
}
|
|
8809
8809
|
|
|
8810
|
+
// ../web/src/lib/stitches.ts
|
|
8811
|
+
function stitchExtent(members, dims) {
|
|
8812
|
+
let w = 0, h = 0;
|
|
8813
|
+
for (const m of members) {
|
|
8814
|
+
const d = dims[m.key];
|
|
8815
|
+
if (!d?.w) continue;
|
|
8816
|
+
w = Math.max(w, m.dx + d.w);
|
|
8817
|
+
h = Math.max(h, m.dy + d.h);
|
|
8818
|
+
}
|
|
8819
|
+
return { w: Math.ceil(w), h: Math.ceil(h) };
|
|
8820
|
+
}
|
|
8821
|
+
function memberBoxes(members, dims) {
|
|
8822
|
+
return members.map((m) => {
|
|
8823
|
+
const d = dims[m.key] || { w: 0, h: 0 };
|
|
8824
|
+
return { x0: m.dx, y0: m.dy, x1: m.dx + d.w, y1: m.dy + d.h };
|
|
8825
|
+
});
|
|
8826
|
+
}
|
|
8827
|
+
function seamClips(members, dims) {
|
|
8828
|
+
const boxes = memberBoxes(members, dims);
|
|
8829
|
+
const clips = boxes.map((b) => ({ ...b }));
|
|
8830
|
+
if (boxes.length < 2) return clips;
|
|
8831
|
+
const cxs = boxes.map((b) => (b.x0 + b.x1) / 2), cys = boxes.map((b) => (b.y0 + b.y1) / 2);
|
|
8832
|
+
const spread = (a) => Math.max(...a) - Math.min(...a);
|
|
8833
|
+
const horizontal = spread(cxs) >= spread(cys);
|
|
8834
|
+
const order = boxes.map((_, i) => i).sort((a, b) => horizontal ? boxes[a].x0 - boxes[b].x0 : boxes[a].y0 - boxes[b].y0);
|
|
8835
|
+
for (let k = 0; k + 1 < order.length; k++) {
|
|
8836
|
+
const i = order[k], j = order[k + 1];
|
|
8837
|
+
if (horizontal && boxes[j].x0 < boxes[i].x1) {
|
|
8838
|
+
const seam = (boxes[j].x0 + boxes[i].x1) / 2;
|
|
8839
|
+
clips[i].x1 = Math.min(clips[i].x1, seam);
|
|
8840
|
+
clips[j].x0 = Math.max(clips[j].x0, seam);
|
|
8841
|
+
} else if (!horizontal && boxes[j].y0 < boxes[i].y1) {
|
|
8842
|
+
const seam = (boxes[j].y0 + boxes[i].y1) / 2;
|
|
8843
|
+
clips[i].y1 = Math.min(clips[i].y1, seam);
|
|
8844
|
+
clips[j].y0 = Math.max(clips[j].y0, seam);
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
return clips;
|
|
8848
|
+
}
|
|
8849
|
+
function stitchPagePlan(members, dims) {
|
|
8850
|
+
const clips = seamClips(members, dims);
|
|
8851
|
+
return {
|
|
8852
|
+
extent: stitchExtent(members, dims),
|
|
8853
|
+
members: members.map((m, i) => ({ key: m.key, dx: m.dx, dy: m.dy, clip: clips[i] }))
|
|
8854
|
+
};
|
|
8855
|
+
}
|
|
8856
|
+
function memberEmbed(vpTransform, member, pageH, renderScale) {
|
|
8857
|
+
const [a, b, c, d, e, f] = Array.from(vpTransform);
|
|
8858
|
+
const det = a * d - b * c;
|
|
8859
|
+
const inv = (x, y) => [(d * (x - e) - c * (y - f)) / det, (-b * (x - e) + a * (y - f)) / det];
|
|
8860
|
+
const { clip, dx, dy } = member;
|
|
8861
|
+
const corners = [
|
|
8862
|
+
inv(clip.x0 - dx, clip.y0 - dy),
|
|
8863
|
+
inv(clip.x1 - dx, clip.y0 - dy),
|
|
8864
|
+
inv(clip.x0 - dx, clip.y1 - dy),
|
|
8865
|
+
inv(clip.x1 - dx, clip.y1 - dy)
|
|
8866
|
+
];
|
|
8867
|
+
const xs = corners.map((p) => p[0]), ys = corners.map((p) => p[1]);
|
|
8868
|
+
const z4 = (n) => n + 0 || 0;
|
|
8869
|
+
return {
|
|
8870
|
+
bbox: { left: z4(Math.min(...xs)), bottom: z4(Math.min(...ys)), right: z4(Math.max(...xs)), top: z4(Math.max(...ys)) },
|
|
8871
|
+
// user → visual px (vpTransform), + stitch offset, ÷ renderScale into
|
|
8872
|
+
// points, y flipped into PDF's y-up — composed into one affine
|
|
8873
|
+
matrix: [
|
|
8874
|
+
a / renderScale,
|
|
8875
|
+
-b / renderScale,
|
|
8876
|
+
c / renderScale,
|
|
8877
|
+
-d / renderScale,
|
|
8878
|
+
(e + dx) / renderScale,
|
|
8879
|
+
pageH - (f + dy) / renderScale
|
|
8880
|
+
]
|
|
8881
|
+
};
|
|
8882
|
+
}
|
|
8883
|
+
|
|
8810
8884
|
// ../web/src/lib/lineStyles.js
|
|
8811
8885
|
var LINE_STYLES = {
|
|
8812
8886
|
solid: { label: "Solid", dash: null },
|
|
@@ -9182,7 +9256,8 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
9182
9256
|
for (const sh of marked) {
|
|
9183
9257
|
if (y < 90) break;
|
|
9184
9258
|
const items = shapesBy.get(sh.key) || [];
|
|
9185
|
-
|
|
9259
|
+
const where = sh.stitch ? `stitched \xB7 ${sh.stitch.members.length} sheets` : `page ${sh.page}`;
|
|
9260
|
+
draw(`${sh.label} \xB7 ${where} \xB7 ${items.length + (marksBy.get(sh.key) || []).length + (apBy.get(sh.key) || []).length} item(s)`, { x: 52, y, size: 9.5, font: bold, color: ink });
|
|
9186
9261
|
y -= 13;
|
|
9187
9262
|
for (const r of bySheetId.get(sh.key)?.rows || []) {
|
|
9188
9263
|
if (y < 92) break;
|
|
@@ -9261,37 +9336,100 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
9261
9336
|
}
|
|
9262
9337
|
}
|
|
9263
9338
|
const srcDocs = /* @__PURE__ */ new Map();
|
|
9339
|
+
const srcDocFor = async (file) => {
|
|
9340
|
+
let src = srcDocs.get(file);
|
|
9341
|
+
if (!src) {
|
|
9342
|
+
src = await PDFDocument.load(await loadPdfData(file), { ignoreEncryption: true });
|
|
9343
|
+
srcDocs.set(file, src);
|
|
9344
|
+
}
|
|
9345
|
+
return src;
|
|
9346
|
+
};
|
|
9264
9347
|
for (const sh of marked) {
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9348
|
+
let pg, toPage, chipRot = degrees(0), W, H;
|
|
9349
|
+
if (sh.stitch) {
|
|
9350
|
+
const members = sh.stitch.members;
|
|
9351
|
+
const pages = [];
|
|
9352
|
+
const dims = {};
|
|
9353
|
+
for (const m of members) {
|
|
9354
|
+
const page = await getPage(m.file, m.page);
|
|
9355
|
+
const vpR = page.getViewport({ scale: RENDER_SCALE });
|
|
9356
|
+
dims[m.key] = { w: vpR.width, h: vpR.height };
|
|
9357
|
+
pages.push({ m, page, vpR });
|
|
9358
|
+
}
|
|
9359
|
+
const plan = stitchPagePlan(members, dims);
|
|
9360
|
+
W = plan.extent.w;
|
|
9361
|
+
H = plan.extent.h;
|
|
9362
|
+
const pageW = W / RENDER_SCALE, pageH = H / RENDER_SCALE;
|
|
9363
|
+
if (dark) {
|
|
9364
|
+
const s = Math.min(RASTER_MAX / Math.max(pageW, pageH), 4);
|
|
9365
|
+
const cv = document.createElement("canvas");
|
|
9366
|
+
cv.width = Math.ceil(pageW * s);
|
|
9367
|
+
cv.height = Math.ceil(pageH * s);
|
|
9368
|
+
const ctx = cv.getContext("2d");
|
|
9369
|
+
ctx.fillStyle = "#fff";
|
|
9370
|
+
ctx.fillRect(0, 0, cv.width, cv.height);
|
|
9371
|
+
const k = s / RENDER_SCALE;
|
|
9372
|
+
for (let i = 0; i < pages.length; i++) {
|
|
9373
|
+
const { page } = pages[i], pm = plan.members[i];
|
|
9374
|
+
const vp = page.getViewport({ scale: s });
|
|
9375
|
+
const mc = document.createElement("canvas");
|
|
9376
|
+
mc.width = Math.ceil(vp.width);
|
|
9377
|
+
mc.height = Math.ceil(vp.height);
|
|
9378
|
+
await page.render({ canvasContext: mc.getContext("2d"), viewport: vp }).promise;
|
|
9379
|
+
ctx.save();
|
|
9380
|
+
ctx.beginPath();
|
|
9381
|
+
ctx.rect(pm.clip.x0 * k, pm.clip.y0 * k, (pm.clip.x1 - pm.clip.x0) * k, (pm.clip.y1 - pm.clip.y0) * k);
|
|
9382
|
+
ctx.clip();
|
|
9383
|
+
ctx.drawImage(mc, pm.dx * k, pm.dy * k);
|
|
9384
|
+
ctx.restore();
|
|
9385
|
+
}
|
|
9386
|
+
invertPixels(cv);
|
|
9387
|
+
const png = await doc.embedPng(cv.toDataURL("image/png"));
|
|
9388
|
+
pg = doc.addPage([pageW, pageH]);
|
|
9389
|
+
pg.drawImage(png, { x: 0, y: 0, width: pageW, height: pageH });
|
|
9390
|
+
} else {
|
|
9391
|
+
pg = doc.addPage([pageW, pageH]);
|
|
9392
|
+
for (let i = 0; i < pages.length; i++) {
|
|
9393
|
+
const { m, vpR } = pages[i], pm = plan.members[i];
|
|
9394
|
+
const src = await srcDocFor(m.file);
|
|
9395
|
+
const { bbox, matrix } = memberEmbed(vpR.transform, pm, pageH, RENDER_SCALE);
|
|
9396
|
+
const emb = await doc.embedPage(src.getPage(m.page - 1), bbox, matrix);
|
|
9397
|
+
pg.drawPage(emb, { x: 0, y: 0 });
|
|
9398
|
+
}
|
|
9399
|
+
}
|
|
9400
|
+
toPage = (x, y) => [x / RENDER_SCALE, pageH - y / RENDER_SCALE];
|
|
9283
9401
|
} else {
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9402
|
+
const page = await getPage(sh.file, sh.page);
|
|
9403
|
+
const vpR = page.getViewport({ scale: RENDER_SCALE });
|
|
9404
|
+
W = vpR.width;
|
|
9405
|
+
H = vpR.height;
|
|
9406
|
+
if (dark) {
|
|
9407
|
+
const vp1 = page.getViewport({ scale: 1 });
|
|
9408
|
+
const s = Math.min(RASTER_MAX / Math.max(vp1.width, vp1.height), 4);
|
|
9409
|
+
const vp = page.getViewport({ scale: s });
|
|
9410
|
+
const cv = document.createElement("canvas");
|
|
9411
|
+
cv.width = Math.ceil(vp.width);
|
|
9412
|
+
cv.height = Math.ceil(vp.height);
|
|
9413
|
+
await page.render({ canvasContext: cv.getContext("2d"), viewport: vp }).promise;
|
|
9414
|
+
invertPixels(cv);
|
|
9415
|
+
const png = await doc.embedPng(cv.toDataURL("image/png"));
|
|
9416
|
+
pg = doc.addPage([vp1.width, vp1.height]);
|
|
9417
|
+
pg.drawImage(png, { x: 0, y: 0, width: vp1.width, height: vp1.height });
|
|
9418
|
+
const k = vp1.width / W;
|
|
9419
|
+
toPage = (x, y) => [x * k, vp1.height - y * k];
|
|
9420
|
+
} else {
|
|
9421
|
+
let src = srcDocs.get(sh.file);
|
|
9422
|
+
if (!src) {
|
|
9423
|
+
src = await PDFDocument.load(await loadPdfData(sh.file), { ignoreEncryption: true });
|
|
9424
|
+
srcDocs.set(sh.file, src);
|
|
9425
|
+
}
|
|
9426
|
+
const [copied] = await doc.copyPages(src, [sh.page - 1]);
|
|
9427
|
+
pg = doc.addPage(copied);
|
|
9428
|
+
const [a, b, c, d, e, f] = vpR.transform;
|
|
9429
|
+
const det = a * d - b * c;
|
|
9430
|
+
toPage = (x, y) => [(d * (x - e) - c * (y - f)) / det, (-b * (x - e) + a * (y - f)) / det];
|
|
9431
|
+
chipRot = degrees(page.rotate || 0);
|
|
9432
|
+
}
|
|
9295
9433
|
}
|
|
9296
9434
|
const ptScale = Math.hypot(...(() => {
|
|
9297
9435
|
const p0 = toPage(0, 0), p1 = toPage(1, 0);
|
|
@@ -9495,7 +9633,8 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
9495
9633
|
const tw = bold.widthOfTextAtSize(label, size);
|
|
9496
9634
|
pg.drawText(label, { x: pcx - tw / 2, y: pcy - size / 2.7, size, font: bold, color: acol, rotate: chipRot });
|
|
9497
9635
|
}
|
|
9498
|
-
|
|
9636
|
+
const stamp = sh.stitch ? `${sh.label} \xB7 stitched composite (${sh.stitch.members.map((m) => m.label || m.key).join(" + ")}) \xB7 marked set` : `${sh.label} \xB7 marked set`;
|
|
9637
|
+
text(stamp, 14, 20, 8, muted);
|
|
9499
9638
|
}
|
|
9500
9639
|
const allPages = doc.getPages();
|
|
9501
9640
|
const lastPg = allPages[allPages.length - 1];
|
|
@@ -9856,7 +9995,16 @@ var run = (tool, fn) => async (args) => {
|
|
|
9856
9995
|
traceToolCall(tool, args, startedAt, reply);
|
|
9857
9996
|
return reply;
|
|
9858
9997
|
};
|
|
9859
|
-
function registerTools(
|
|
9998
|
+
function registerTools(realServer, session) {
|
|
9999
|
+
const registered = /* @__PURE__ */ new Map();
|
|
10000
|
+
const server = {
|
|
10001
|
+
registerTool(name, meta, handler) {
|
|
10002
|
+
const tool = realServer.registerTool(name, meta, handler);
|
|
10003
|
+
registered.set(name, tool);
|
|
10004
|
+
return tool;
|
|
10005
|
+
},
|
|
10006
|
+
sendResourceListChanged: () => realServer.sendResourceListChanged()
|
|
10007
|
+
};
|
|
9860
10008
|
server.registerTool("load_plan", {
|
|
9861
10009
|
description: `Open a plan PDF from disk. Default: replace the whole session (previous documents, scales, conditions, and shapes are cleared). merge: true ADDS the document to the working set instead (#152) \u2014 a bid set is plans + schedule + addenda, not one PDF \u2014 keeping every scale, condition, and shape; sheet keys carry file names so documents never collide, the sheet graph spans the whole set (resolve_tag can chain a plan tag on one file to a schedule row in another), and the marked set covers every worked sheet. Re-loading an already-merged file is refused \u2014 reload = replace, deliberately. Returns file, files, page_count, and one entry per sheet. The loaded sheets also become browsable resources (takeoff://sheets). ${COORDS}`,
|
|
9862
10010
|
inputSchema: {
|
|
@@ -10312,6 +10460,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
10312
10460
|
},
|
|
10313
10461
|
outputSchema: deleteVerdictOutput
|
|
10314
10462
|
}, run("delete_verdict", ({ verdict_id }) => session.deleteVerdict(verdict_id)));
|
|
10463
|
+
return registered;
|
|
10315
10464
|
}
|
|
10316
10465
|
|
|
10317
10466
|
// src/resources.ts
|
|
@@ -10390,10 +10539,107 @@ function registerResources(server, session) {
|
|
|
10390
10539
|
);
|
|
10391
10540
|
}
|
|
10392
10541
|
|
|
10542
|
+
// src/staging.ts
|
|
10543
|
+
import { z as z3 } from "zod";
|
|
10544
|
+
var TOOL_STAGES = {
|
|
10545
|
+
// Always enabled: an agent needs these to orient before anything else is useful.
|
|
10546
|
+
setup: [
|
|
10547
|
+
"load_plan",
|
|
10548
|
+
"sheet_info",
|
|
10549
|
+
"set_scale",
|
|
10550
|
+
"sheet_graph",
|
|
10551
|
+
"resolve_tag",
|
|
10552
|
+
"find_schedule",
|
|
10553
|
+
"read_sheet_text",
|
|
10554
|
+
"find_text",
|
|
10555
|
+
"sheet_context",
|
|
10556
|
+
"view_sheet"
|
|
10557
|
+
],
|
|
10558
|
+
measure: [
|
|
10559
|
+
"one_click",
|
|
10560
|
+
"detect_rooms",
|
|
10561
|
+
"measure_polygon",
|
|
10562
|
+
"cut_out",
|
|
10563
|
+
"measure_line",
|
|
10564
|
+
"measure_surface",
|
|
10565
|
+
"place_count",
|
|
10566
|
+
"symbol_sweep",
|
|
10567
|
+
"sweep_schedule_row",
|
|
10568
|
+
"derive_base",
|
|
10569
|
+
"derive_transitions"
|
|
10570
|
+
],
|
|
10571
|
+
revise: [
|
|
10572
|
+
"list_shapes",
|
|
10573
|
+
"delete_shape",
|
|
10574
|
+
"edit_shape",
|
|
10575
|
+
"edit_materials",
|
|
10576
|
+
"edit_condition",
|
|
10577
|
+
"duplicate_condition",
|
|
10578
|
+
"split_condition",
|
|
10579
|
+
"undo_last",
|
|
10580
|
+
"annotate",
|
|
10581
|
+
"list_annotations",
|
|
10582
|
+
"link_annotation",
|
|
10583
|
+
"mark_verdict",
|
|
10584
|
+
"delete_verdict"
|
|
10585
|
+
],
|
|
10586
|
+
handoff: [
|
|
10587
|
+
"takeoff_summary",
|
|
10588
|
+
"export_takeoff",
|
|
10589
|
+
"export_report",
|
|
10590
|
+
"import_takeoff",
|
|
10591
|
+
"apply_rules",
|
|
10592
|
+
"export_marked_pdf"
|
|
10593
|
+
]
|
|
10594
|
+
};
|
|
10595
|
+
var OPENABLE = ["measure", "revise", "handoff"];
|
|
10596
|
+
var openToolStageOutput = {
|
|
10597
|
+
stage: z3.string().describe("The stage that was opened"),
|
|
10598
|
+
enabled: z3.array(z3.string()).describe("Tool names enabled by this call (empty if the stage was already open)"),
|
|
10599
|
+
open_stages: z3.array(z3.string()).describe("Every stage currently enabled, setup included"),
|
|
10600
|
+
closed_stages: z3.array(z3.string()).describe("Stages still closed \u2014 open them here when the work reaches them")
|
|
10601
|
+
};
|
|
10602
|
+
var STAGED_INSTRUCTIONS = 'TOOL EXPOSURE IS STAGED: only the setup tools are enabled at start. Before measuring, call open_tool_stage {stage:"measure"}; likewise "revise" for edit/annotate/verdict tools and "handoff" for summaries and exports. Opening a stage is instant, idempotent, and never closes anything.';
|
|
10603
|
+
function applyStagedTools(server, registered) {
|
|
10604
|
+
const openStages = /* @__PURE__ */ new Set(["setup"]);
|
|
10605
|
+
for (const stage of OPENABLE) {
|
|
10606
|
+
for (const name of TOOL_STAGES[stage]) registered.get(name)?.disable();
|
|
10607
|
+
}
|
|
10608
|
+
server.registerTool("open_tool_stage", {
|
|
10609
|
+
description: `Enable a stage of this server's tools. Tool exposure is staged to match the takeoff workflow: "setup" (orient: load, scale, read the set) is always enabled; "measure" (commit shapes: one_click, detect_rooms, measure_*, sweeps and derives), "revise" (edit, annotate, verdict-mark, undo), and "handoff" (summaries, exports, the marked set) start closed and open here on demand. Opening a stage is idempotent and never closes another \u2014 the surface only grows. Call it the moment the work reaches a closed stage; the reply lists exactly which tools just became available.`,
|
|
10610
|
+
inputSchema: {
|
|
10611
|
+
stage: z3.enum(OPENABLE).describe('Which stage to enable: "measure", "revise", or "handoff"')
|
|
10612
|
+
},
|
|
10613
|
+
outputSchema: openToolStageOutput
|
|
10614
|
+
}, async ({ stage }) => {
|
|
10615
|
+
try {
|
|
10616
|
+
const names = TOOL_STAGES[stage];
|
|
10617
|
+
const enabled = [];
|
|
10618
|
+
for (const name of names) {
|
|
10619
|
+
const tool = registered.get(name);
|
|
10620
|
+
if (!tool) throw new UserError(`Stage table names an unregistered tool: ${name}`);
|
|
10621
|
+
if (!tool.enabled) {
|
|
10622
|
+
tool.enable();
|
|
10623
|
+
enabled.push(name);
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
openStages.add(stage);
|
|
10627
|
+
return ok({
|
|
10628
|
+
stage,
|
|
10629
|
+
enabled,
|
|
10630
|
+
open_stages: Object.keys(TOOL_STAGES).filter((s) => openStages.has(s)),
|
|
10631
|
+
closed_stages: Object.keys(TOOL_STAGES).filter((s) => !openStages.has(s))
|
|
10632
|
+
});
|
|
10633
|
+
} catch (e) {
|
|
10634
|
+
return fail(e);
|
|
10635
|
+
}
|
|
10636
|
+
});
|
|
10637
|
+
}
|
|
10638
|
+
|
|
10393
10639
|
// package.json
|
|
10394
10640
|
var package_default = {
|
|
10395
10641
|
name: "opentakeoff-mcp",
|
|
10396
|
-
version: "0.9.
|
|
10642
|
+
version: "0.9.40",
|
|
10397
10643
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
10398
10644
|
type: "module",
|
|
10399
10645
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -10409,7 +10655,7 @@ var package_default = {
|
|
|
10409
10655
|
mcpb: "npm run build && node scripts/build-mcpb.mjs",
|
|
10410
10656
|
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
10411
10657
|
typecheck: "tsc --noEmit",
|
|
10412
|
-
test: "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/labels.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
10658
|
+
test: "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/labels.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/staging.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
10413
10659
|
},
|
|
10414
10660
|
dependencies: {
|
|
10415
10661
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
@@ -10466,7 +10712,8 @@ var package_default = {
|
|
|
10466
10712
|
};
|
|
10467
10713
|
|
|
10468
10714
|
// server.ts
|
|
10469
|
-
function buildServer(session = new Session()) {
|
|
10715
|
+
function buildServer(session = new Session(), opts = {}) {
|
|
10716
|
+
const staged = opts.stagedTools ?? process.env.OPENTAKEOFF_MCP_STAGED_TOOLS === "1";
|
|
10470
10717
|
const server = new McpServer2({ name: "opentakeoff", version: package_default.version }, {
|
|
10471
10718
|
// Served to every client at initialize — the discipline that makes agent
|
|
10472
10719
|
// takeoffs land as reviewable work instead of a bare numbers report.
|
|
@@ -10478,11 +10725,13 @@ function buildServer(session = new Session()) {
|
|
|
10478
10725
|
"3. DERIVE what follows from the rooms instead of re-measuring it: derive_base for base LF (perimeter \u2212 the door openings YOU state), derive_transitions for the line where two finishes meet. Both read committed floor shapes, so they come after step 2 and their output is audited in step 4 like anything else.",
|
|
10479
10726
|
"4. LOOK at what landed with view_sheet overlay:true and fix misses with edit_shape before trusting totals \u2014 crop the work region tight (full-sheet renders downsample too far to audit a ring).",
|
|
10480
10727
|
"5. Finish by writing the marked-up planset with export_marked_pdf and give the user its file path, alongside export_report for the numbers. Never end a takeoff with numbers alone.",
|
|
10481
|
-
"WITHHELD IS NOT A FAILURE \u2014 IT IS THE ANSWER. detect_rooms, symbol_sweep, sweep_schedule_row and derive_transitions all measure things they then decline to commit, and say why: a near-match in the score band, a room the schedule cannot answer for, adjacency across a WALL rather than a butt joint. Read those arrays, view_sheet the coordinates they hand you, and resolve them or report them. A withheld item you ignore is a hole in the bid; one you never mention is worse."
|
|
10728
|
+
"WITHHELD IS NOT A FAILURE \u2014 IT IS THE ANSWER. detect_rooms, symbol_sweep, sweep_schedule_row and derive_transitions all measure things they then decline to commit, and say why: a near-match in the score band, a room the schedule cannot answer for, adjacency across a WALL rather than a butt joint. Read those arrays, view_sheet the coordinates they hand you, and resolve them or report them. A withheld item you ignore is a hole in the bid; one you never mention is worse.",
|
|
10729
|
+
...staged ? [STAGED_INSTRUCTIONS] : []
|
|
10482
10730
|
].join("\n")
|
|
10483
10731
|
});
|
|
10484
|
-
registerTools(server, session);
|
|
10732
|
+
const registered = registerTools(server, session);
|
|
10485
10733
|
registerResources(server, session);
|
|
10734
|
+
if (staged) applyStagedTools(server, registered);
|
|
10486
10735
|
return server;
|
|
10487
10736
|
}
|
|
10488
10737
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.40",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"mcpb": "npm run build && node scripts/build-mcpb.mjs",
|
|
17
17
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"test": "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/labels.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
19
|
+
"test": "node --import tsx --test test/conformance.test.ts test/context.test.ts test/e2e.test.ts test/labels.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/staging.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|