opentakeoff-mcp 0.8.0 → 0.9.2
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 +38 -11
- package/dist/server-core.js +167 -4
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# OpenTakeoff MCP server
|
|
2
2
|
|
|
3
3
|
Listed in the [official MCP registry](https://registry.modelcontextprotocol.io) as
|
|
4
|
-
`io.github.Kentucky-ai/opentakeoff
|
|
4
|
+
`io.github.Kentucky-ai/opentakeoff`, on [Glama](https://glama.ai/mcp/servers/Kentucky-ai/opentakeoff),
|
|
5
|
+
and on [Smithery](https://smithery.ai/servers/Kentucky-ai/opentakeoff).
|
|
5
6
|
|
|
6
7
|
## Run it in 60 seconds (npx)
|
|
7
8
|
|
|
@@ -167,9 +168,10 @@ set becomes browsable natively (`resources/list` re-announces itself via
|
|
|
167
168
|
Page numbers — not file-derived sheet keys — address resources, so URIs stay
|
|
168
169
|
clean regardless of the PDF's name; the human-facing key (`plan.pdf#2`) and
|
|
169
170
|
title-block number (`A-101`) ride along as the resource name and title.
|
|
170
|
-
Rendering uses `@napi-rs/canvas
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
Rendering uses `@napi-rs/canvas`, declared as this package's own optional
|
|
172
|
+
dependency so a plain `npx opentakeoff-mcp` installs the prebuilt binary and
|
|
173
|
+
arrives with eyes; on a platform without a prebuilt binary every non-raster
|
|
174
|
+
capability still works and the image read explains exactly what's missing.
|
|
173
175
|
|
|
174
176
|
The intended agent loop: read `takeoff://sheets` → look at
|
|
175
177
|
`takeoff://sheet/{page}/image` → pick click targets → measure with the tools.
|
|
@@ -240,23 +242,48 @@ npm test # session + tool-layer + e2e, against demo/sample-plan.pdf
|
|
|
240
242
|
MCP releases live in the **`mcp-v*`** tag namespace — bare `v*` tags belong to
|
|
241
243
|
the app (v0.2.0, v0.3.0 are app releases). Releases publish via **npm trusted
|
|
242
244
|
publishing**: the tag push fires `.github/workflows/publish-mcp.yml`, which
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
release
|
|
245
|
+
runs straight through — no approval click — and publishes the npm artifact
|
|
246
|
+
over OIDC with a **provenance attestation** (no npm token exists anywhere —
|
|
247
|
+
the npm package designates that exact repo + workflow as its trusted
|
|
248
|
+
publisher), followed by the MCP registry entry, the GitHub release, and the
|
|
249
|
+
MCPB bundle. The `release` environment's required-reviewer gate existed
|
|
250
|
+
briefly and was deliberately removed (2026-07-22) — the tag push is the one
|
|
251
|
+
human decision, and it's already admin-gated, so a second click added
|
|
252
|
+
friction without adding safety.
|
|
248
253
|
|
|
249
254
|
```bash
|
|
250
255
|
# 1. bump the version — all three fields together:
|
|
251
256
|
# package.json .version, server.json .version, server.json .packages[0].version
|
|
252
|
-
# 2. tag and push — this fires the whole release:
|
|
257
|
+
# 2. tag and push — this fires the whole release, fully unattended:
|
|
253
258
|
git tag mcp-v<version> && git push origin mcp-v<version>
|
|
254
|
-
# 3. approve the run (GitHub → Actions → the paused "Publish to MCP Registry" run)
|
|
255
259
|
```
|
|
256
260
|
|
|
261
|
+
⚠️ Because there's no approval step, an accidental or mistyped `mcp-v*` tag
|
|
262
|
+
publishes to npm immediately, and npm unpublish is heavily restricted —
|
|
263
|
+
double-check the version before tagging.
|
|
264
|
+
|
|
257
265
|
The workflow checks version consistency, runs the full publish gate
|
|
258
266
|
(`prepublishOnly` = typecheck + tests + build), publishes to npm and the
|
|
259
267
|
official MCP registry, verifies the registry listing, and creates the GitHub
|
|
260
268
|
release (titled `opentakeoff-mcp <version>`). A re-run skips the npm publish
|
|
261
269
|
if that version already shipped, so a transient failure downstream is safe to
|
|
262
270
|
retry.
|
|
271
|
+
|
|
272
|
+
### Refreshing the Smithery listing
|
|
273
|
+
|
|
274
|
+
Smithery isn't part of the automated release above — it needs a **separate,
|
|
275
|
+
manual** publish after any tool signature change, because of a genuine spec
|
|
276
|
+
conflict between two validators: the official MCPB validator (what
|
|
277
|
+
`npm run mcpb` gates on) rejects a `tools[].inputSchema` key outright, while
|
|
278
|
+
Smithery's registry rejects a bundle *without* real `inputSchema` per tool
|
|
279
|
+
(smithery-ai/cli#770, #797, #787 — no manifest satisfies both). The canonical
|
|
280
|
+
`dist-mcpb/opentakeoff-mcp.mcpb` stays spec-compliant for Claude Desktop / the
|
|
281
|
+
official registry / Glama; `scripts/build-smithery-mcpb.mjs` builds a
|
|
282
|
+
Smithery-only bundle instead, with live-introspected tools + inputSchema baked
|
|
283
|
+
in, packed with a plain zip (bypassing `mcpb validate`, which would reject it):
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm run build
|
|
287
|
+
node scripts/build-smithery-mcpb.mjs
|
|
288
|
+
smithery mcp publish dist-smithery/opentakeoff-mcp.mcpb -n Kentucky-ai/opentakeoff
|
|
289
|
+
```
|
package/dist/server-core.js
CHANGED
|
@@ -1073,7 +1073,7 @@ function drawShapes(ctx, toCanvas, shapes, sheetW, sheetH, longEdge) {
|
|
|
1073
1073
|
var SNAP_CELL = 24;
|
|
1074
1074
|
var SNAP_TOL = 7;
|
|
1075
1075
|
var PALETTE = ["#c96442", "#2f7d54", "#2563eb", "#9333ea", "#b8860b", "#0d9488", "#be185d", "#1f2937", "#dc2626", "#0891b2"];
|
|
1076
|
-
var HATCH_IDS = ["solid", "diag", "diag2", "cross", "diagdense", "horiz", "vert", "grid", "brick", "plank", "herring", "basket", "checker", "wave", "
|
|
1076
|
+
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"];
|
|
1077
1077
|
var mintUuid = () => globalThis.crypto && typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1078
1078
|
var uid = (p) => `${p}-${mintUuid()}`;
|
|
1079
1079
|
var ANN_SCHEMA = "opentakeoff.takeoff_canvas.v1";
|
|
@@ -1121,6 +1121,7 @@ var Session = class {
|
|
|
1121
1121
|
doc = null;
|
|
1122
1122
|
sheets = /* @__PURE__ */ new Map();
|
|
1123
1123
|
conditions = [];
|
|
1124
|
+
markups = [];
|
|
1124
1125
|
shapes = [];
|
|
1125
1126
|
/** Newest-last. Capped at UNDO_CAP; the oldest entry falls off the front. */
|
|
1126
1127
|
journal = [];
|
|
@@ -1148,6 +1149,7 @@ var Session = class {
|
|
|
1148
1149
|
this.sheets.clear();
|
|
1149
1150
|
this.conditions = [];
|
|
1150
1151
|
this.shapes = [];
|
|
1152
|
+
this.markups = [];
|
|
1151
1153
|
this.file = null;
|
|
1152
1154
|
this.journal = [];
|
|
1153
1155
|
this.pendingCommits = [];
|
|
@@ -1805,6 +1807,95 @@ var Session = class {
|
|
|
1805
1807
|
...undone.length < n ? { note: `Only ${undone.length} step(s) were available to undo.` } : {}
|
|
1806
1808
|
};
|
|
1807
1809
|
}
|
|
1810
|
+
/** Place an annotation. A note ABOUT the work — it never measures anything
|
|
1811
|
+
* and never touches a quantity, which is why there is no review gate here:
|
|
1812
|
+
* the pencil-not-ink rule exists to stop an agent inventing GEOMETRY, and a
|
|
1813
|
+
* cloud saying "verify substrate" is not geometry.
|
|
1814
|
+
*
|
|
1815
|
+
* `condition` attaches it to a scope by finish tag, minting the condition on
|
|
1816
|
+
* first touch exactly like one_click/measure_polygon — so an agent can note
|
|
1817
|
+
* something about CPT-1 before anything is traced for CPT-1. Omit it for a
|
|
1818
|
+
* note about the sheet rather than about a finish. */
|
|
1819
|
+
annotate(a) {
|
|
1820
|
+
const s = this.sheet(a.sheet);
|
|
1821
|
+
const n = ([x, y]) => [x / s.widthPx, y / s.heightPx];
|
|
1822
|
+
if ((a.type === "cloud" || a.type === "highlight") && !a.rect) throw new UserError(`a ${a.type} needs rect: [[x0,y0],[x1,y1]] in image px`);
|
|
1823
|
+
if ((a.type === "text" || a.type === "callout") && !a.at) throw new UserError(`a ${a.type} needs at: [x,y] in image px`);
|
|
1824
|
+
if (a.type === "callout" && !a.target) throw new UserError("a callout needs target: [x,y] \u2014 the point the leader line aims at");
|
|
1825
|
+
const cond = a.condition ? this.conditionFor(a.condition) : null;
|
|
1826
|
+
const m = {
|
|
1827
|
+
id: uid("mk"),
|
|
1828
|
+
sheet_id: s.key,
|
|
1829
|
+
type: a.type,
|
|
1830
|
+
text: a.text || "",
|
|
1831
|
+
condition_id: cond?.id ?? "",
|
|
1832
|
+
rfi_id: "",
|
|
1833
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1834
|
+
...a.at ? { at: n(a.at) } : {},
|
|
1835
|
+
...a.target ? { target: n(a.target) } : {},
|
|
1836
|
+
...a.rect ? { rect: [n(a.rect[0]), n(a.rect[1])] } : {}
|
|
1837
|
+
};
|
|
1838
|
+
this.markups.push(m);
|
|
1839
|
+
return {
|
|
1840
|
+
id: m.id,
|
|
1841
|
+
sheet: s.key,
|
|
1842
|
+
type: m.type,
|
|
1843
|
+
text: m.text,
|
|
1844
|
+
condition: cond?.finish_tag ?? "",
|
|
1845
|
+
condition_id: m.condition_id,
|
|
1846
|
+
note: cond ? `Attached to ${cond.finish_tag} \u2014 it wears that condition's colour on the canvas and in the marked set.` : "Unattached \u2014 a note about the sheet. Pass condition to tie it to a scope."
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/** Read annotations, optionally narrowed to a sheet and/or a condition.
|
|
1850
|
+
* Resolves condition_id to its finish tag so a caller can act on the reply
|
|
1851
|
+
* without joining against the conditions array. */
|
|
1852
|
+
listAnnotations(f = {}) {
|
|
1853
|
+
const tagById = new Map(this.conditions.map((c) => [c.id, c.finish_tag]));
|
|
1854
|
+
let rows = this.markups;
|
|
1855
|
+
if (f.sheet) {
|
|
1856
|
+
const s = this.sheet(f.sheet);
|
|
1857
|
+
rows = rows.filter((m) => m.sheet_id === s.key);
|
|
1858
|
+
}
|
|
1859
|
+
if (f.condition) {
|
|
1860
|
+
const c = this.conditions.find((x) => x.finish_tag === f.condition);
|
|
1861
|
+
if (!c) throw new UserError(`no condition "${f.condition}" \u2014 tags: ${this.conditions.map((x) => x.finish_tag).join(", ") || "(none)"}`);
|
|
1862
|
+
rows = rows.filter((m) => m.condition_id === c.id);
|
|
1863
|
+
}
|
|
1864
|
+
const s0 = this.sheets;
|
|
1865
|
+
const px = (m, p) => {
|
|
1866
|
+
const sh = s0.get(m.sheet_id);
|
|
1867
|
+
if (!p || !sh) return void 0;
|
|
1868
|
+
return [round1(p[0] * sh.widthPx), round1(p[1] * sh.heightPx)];
|
|
1869
|
+
};
|
|
1870
|
+
return {
|
|
1871
|
+
annotations: rows.map((m) => ({
|
|
1872
|
+
id: m.id,
|
|
1873
|
+
sheet: m.sheet_id,
|
|
1874
|
+
type: m.type,
|
|
1875
|
+
text: m.text,
|
|
1876
|
+
condition: tagById.get(m.condition_id) ?? "",
|
|
1877
|
+
condition_id: m.condition_id,
|
|
1878
|
+
...m.at ? { at: px(m, m.at) } : {},
|
|
1879
|
+
...m.target ? { target: px(m, m.target) } : {},
|
|
1880
|
+
...m.rect ? { rect: [px(m, m.rect[0]), px(m, m.rect[1])] } : {}
|
|
1881
|
+
})),
|
|
1882
|
+
count: rows.length,
|
|
1883
|
+
unattached: rows.filter((m) => !m.condition_id).length
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
/** Attach an existing annotation to a condition, or detach it with "". The
|
|
1887
|
+
* canvas's Attach/Detach, reachable by an agent. */
|
|
1888
|
+
linkAnnotation(id, condition) {
|
|
1889
|
+
const m = this.markups.find((x) => x.id === id);
|
|
1890
|
+
if (!m) throw new UserError(`no annotation "${id}" \u2014 call list_annotations for real ids`);
|
|
1891
|
+
if (!condition) {
|
|
1892
|
+
m.condition_id = "";
|
|
1893
|
+
return { id: m.id, condition: "", note: "Detached \u2014 now a note about the sheet." };
|
|
1894
|
+
}
|
|
1895
|
+
const c = this.conditionFor(condition);
|
|
1896
|
+
m.condition_id = c.id;
|
|
1897
|
+
return { id: m.id, condition: c.finish_tag, condition_id: c.id, note: `Attached to ${c.finish_tag}.` };
|
|
1898
|
+
}
|
|
1808
1899
|
/** The exact browser save payload (TakeoffCanvas.jsx autosave + the schema key
|
|
1809
1900
|
* store.saveAnnotations stamps) — importable by the app. */
|
|
1810
1901
|
exportPayload() {
|
|
@@ -1816,7 +1907,7 @@ var Session = class {
|
|
|
1816
1907
|
sheets: [...this.sheets.values()].filter((s) => s.upp != null).map((s) => ({ sheet_id: s.key, units_per_px: s.upp })),
|
|
1817
1908
|
conditions: this.conditions,
|
|
1818
1909
|
shapes: this.shapes,
|
|
1819
|
-
markups:
|
|
1910
|
+
markups: this.markups,
|
|
1820
1911
|
sheet_group: [],
|
|
1821
1912
|
last_group: [],
|
|
1822
1913
|
sheet_tabs: [],
|
|
@@ -2113,6 +2204,37 @@ var sheetContextOutput = {
|
|
|
2113
2204
|
}),
|
|
2114
2205
|
hatch: z.object({ families: z.array(hatchFamilyRow), count: z.number().int() })
|
|
2115
2206
|
};
|
|
2207
|
+
var annotationRow = z.object({
|
|
2208
|
+
id: z.string(),
|
|
2209
|
+
sheet: z.string(),
|
|
2210
|
+
type: z.string(),
|
|
2211
|
+
text: z.string(),
|
|
2212
|
+
condition: z.string().describe("Resolved finish tag, or '' when unattached \u2014 saves joining against conditions[]"),
|
|
2213
|
+
condition_id: z.string(),
|
|
2214
|
+
at: z.tuple([z.number(), z.number()]).optional(),
|
|
2215
|
+
target: z.tuple([z.number(), z.number()]).optional(),
|
|
2216
|
+
rect: z.array(z.tuple([z.number(), z.number()]).optional()).optional()
|
|
2217
|
+
});
|
|
2218
|
+
var annotateOutput = {
|
|
2219
|
+
id: z.string(),
|
|
2220
|
+
sheet: z.string(),
|
|
2221
|
+
type: z.string(),
|
|
2222
|
+
text: z.string(),
|
|
2223
|
+
condition: z.string(),
|
|
2224
|
+
condition_id: z.string(),
|
|
2225
|
+
note: z.string()
|
|
2226
|
+
};
|
|
2227
|
+
var listAnnotationsOutput = {
|
|
2228
|
+
annotations: z.array(annotationRow),
|
|
2229
|
+
count: z.number().int(),
|
|
2230
|
+
unattached: z.number().int().describe("How many carry no condition \u2014 candidates for link_annotation")
|
|
2231
|
+
};
|
|
2232
|
+
var linkAnnotationOutput = {
|
|
2233
|
+
id: z.string(),
|
|
2234
|
+
condition: z.string(),
|
|
2235
|
+
condition_id: z.string().optional(),
|
|
2236
|
+
note: z.string()
|
|
2237
|
+
};
|
|
2116
2238
|
|
|
2117
2239
|
// src/tools.ts
|
|
2118
2240
|
var COORDS = "Coordinates are image px at render scale 2.0: PDF pt \xD7 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.";
|
|
@@ -2311,6 +2433,39 @@ function registerTools(server, session) {
|
|
|
2311
2433
|
traceToolCall("view_sheet", a, startedAt, reply);
|
|
2312
2434
|
return reply;
|
|
2313
2435
|
});
|
|
2436
|
+
server.registerTool("annotate", {
|
|
2437
|
+
description: `Place an annotation on a sheet \u2014 a note ABOUT the work, never a measurement of it. Types: cloud and highlight take rect:[[x0,y0],[x1,y1]] (a revision cloud around an area, a highlight box over it), text takes at:[x,y], callout takes at:[x,y] plus target:[x,y] (the point its leader aims at).
|
|
2438
|
+
|
|
2439
|
+
Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like one_click/measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself.
|
|
2440
|
+
|
|
2441
|
+
No review gate: the pencil-not-ink rule exists to stop an agent inventing geometry, and a cloud reading "verify substrate" is not geometry. It touches no quantity. ${COORDS}`,
|
|
2442
|
+
inputSchema: {
|
|
2443
|
+
sheet: z2.string().describe("Sheet name or number, as sheet_info reports it"),
|
|
2444
|
+
type: z2.enum(["cloud", "text", "callout", "highlight"]).describe("cloud/highlight need rect; text/callout need at; callout also needs target"),
|
|
2445
|
+
text: z2.string().default("").describe("The note. A cloud with no text still reads as 'look here'"),
|
|
2446
|
+
condition: z2.string().optional().describe("Finish tag to attach this note to, e.g. 'CPT-1' (minted on first use). Omit for an unattached sheet note"),
|
|
2447
|
+
at: pointSchema.optional().describe("Anchor point (image px) \u2014 text and callout"),
|
|
2448
|
+
target: pointSchema.optional().describe("What a callout's leader line points at (image px)"),
|
|
2449
|
+
rect: z2.tuple([pointSchema, pointSchema]).optional().describe("Corners (image px) \u2014 cloud and highlight")
|
|
2450
|
+
},
|
|
2451
|
+
outputSchema: annotateOutput
|
|
2452
|
+
}, run("annotate", (a) => session.annotate(a)));
|
|
2453
|
+
server.registerTool("list_annotations", {
|
|
2454
|
+
description: `Every annotation on the takeoff, with condition_id RESOLVED to its finish tag so you can act on the reply without joining against conditions[]. Filter by sheet, by condition, or both. Coordinates come back in image px (the same frame you passed in), not the normalized form they're stored as. \`unattached\` counts the notes carrying no condition \u2014 the candidates for link_annotation. ${COORDS}`,
|
|
2455
|
+
inputSchema: {
|
|
2456
|
+
sheet: z2.string().optional().describe("Only annotations on this sheet"),
|
|
2457
|
+
condition: z2.string().optional().describe("Only annotations attached to this finish tag")
|
|
2458
|
+
},
|
|
2459
|
+
outputSchema: listAnnotationsOutput
|
|
2460
|
+
}, run("list_annotations", (a) => session.listAnnotations(a)));
|
|
2461
|
+
server.registerTool("link_annotation", {
|
|
2462
|
+
description: `Attach an existing annotation to a condition, or detach it by passing an empty condition \u2014 the canvas's Attach/Detach control, reachable by an agent. Use it to tie up notes left unattached (list_annotations reports how many), or to move one to the finish it actually concerns. Attaching mints the tag on first use.`,
|
|
2463
|
+
inputSchema: {
|
|
2464
|
+
annotation_id: z2.string().describe("Id from annotate or list_annotations"),
|
|
2465
|
+
condition: z2.string().describe("Finish tag to attach to; empty string detaches")
|
|
2466
|
+
},
|
|
2467
|
+
outputSchema: linkAnnotationOutput
|
|
2468
|
+
}, run("link_annotation", (a) => session.linkAnnotation(a.annotation_id, a.condition)));
|
|
2314
2469
|
}
|
|
2315
2470
|
|
|
2316
2471
|
// src/resources.ts
|
|
@@ -2392,7 +2547,7 @@ function registerResources(server, session) {
|
|
|
2392
2547
|
// package.json
|
|
2393
2548
|
var package_default = {
|
|
2394
2549
|
name: "opentakeoff-mcp",
|
|
2395
|
-
version: "0.
|
|
2550
|
+
version: "0.9.2",
|
|
2396
2551
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
2397
2552
|
type: "module",
|
|
2398
2553
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -2415,6 +2570,9 @@ var package_default = {
|
|
|
2415
2570
|
"pdfjs-dist": "^4.10.38",
|
|
2416
2571
|
zod: "^3.24.1"
|
|
2417
2572
|
},
|
|
2573
|
+
optionalDependencies: {
|
|
2574
|
+
"@napi-rs/canvas": "^0.1.100"
|
|
2575
|
+
},
|
|
2418
2576
|
devDependencies: {
|
|
2419
2577
|
"@types/node": "^22.19.21",
|
|
2420
2578
|
typescript: "^5.7.2",
|
|
@@ -2443,7 +2601,12 @@ var package_default = {
|
|
|
2443
2601
|
"estimating",
|
|
2444
2602
|
"pdf",
|
|
2445
2603
|
"quantity-takeoff",
|
|
2446
|
-
"flooring"
|
|
2604
|
+
"flooring",
|
|
2605
|
+
"ai-agent",
|
|
2606
|
+
"agentic-ai",
|
|
2607
|
+
"preconstruction",
|
|
2608
|
+
"construction-management",
|
|
2609
|
+
"project-management"
|
|
2447
2610
|
],
|
|
2448
2611
|
overrides: {
|
|
2449
2612
|
"fast-uri": "^3.1.4",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"pdfjs-dist": "^4.10.38",
|
|
24
24
|
"zod": "^3.24.1"
|
|
25
25
|
},
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"@napi-rs/canvas": "^0.1.100"
|
|
28
|
+
},
|
|
26
29
|
"devDependencies": {
|
|
27
30
|
"@types/node": "^22.19.21",
|
|
28
31
|
"typescript": "^5.7.2",
|
|
@@ -51,10 +54,15 @@
|
|
|
51
54
|
"estimating",
|
|
52
55
|
"pdf",
|
|
53
56
|
"quantity-takeoff",
|
|
54
|
-
"flooring"
|
|
57
|
+
"flooring",
|
|
58
|
+
"ai-agent",
|
|
59
|
+
"agentic-ai",
|
|
60
|
+
"preconstruction",
|
|
61
|
+
"construction-management",
|
|
62
|
+
"project-management"
|
|
55
63
|
],
|
|
56
64
|
"overrides": {
|
|
57
65
|
"fast-uri": "^3.1.4",
|
|
58
66
|
"@hono/node-server": "^2.0.5"
|
|
59
67
|
}
|
|
60
|
-
}
|
|
68
|
+
}
|