opentakeoff-mcp 0.8.0 → 0.9.0

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 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` and on [Glama](https://glama.ai/mcp/servers/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
 
@@ -240,23 +241,48 @@ npm test # session + tool-layer + e2e, against demo/sample-plan.pdf
240
241
  MCP releases live in the **`mcp-v*`** tag namespace — bare `v*` tags belong to
241
242
  the app (v0.2.0, v0.3.0 are app releases). Releases publish via **npm trusted
242
243
  publishing**: the tag push fires `.github/workflows/publish-mcp.yml`, which
243
- pauses at the `release` environment for maintainer approval, then publishes
244
- the npm artifact over OIDC with a **provenance attestation** (no npm token
245
- exists anywhere — the npm package designates that exact repo + workflow as
246
- its trusted publisher), followed by the MCP registry entry, the GitHub
247
- release, and the MCPB bundle.
244
+ runs straight through no approval click and publishes the npm artifact
245
+ over OIDC with a **provenance attestation** (no npm token exists anywhere —
246
+ the npm package designates that exact repo + workflow as its trusted
247
+ publisher), followed by the MCP registry entry, the GitHub release, and the
248
+ MCPB bundle. The `release` environment's required-reviewer gate existed
249
+ briefly and was deliberately removed (2026-07-22) — the tag push is the one
250
+ human decision, and it's already admin-gated, so a second click added
251
+ friction without adding safety.
248
252
 
249
253
  ```bash
250
254
  # 1. bump the version — all three fields together:
251
255
  # package.json .version, server.json .version, server.json .packages[0].version
252
- # 2. tag and push — this fires the whole release:
256
+ # 2. tag and push — this fires the whole release, fully unattended:
253
257
  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
258
  ```
256
259
 
260
+ ⚠️ Because there's no approval step, an accidental or mistyped `mcp-v*` tag
261
+ publishes to npm immediately, and npm unpublish is heavily restricted —
262
+ double-check the version before tagging.
263
+
257
264
  The workflow checks version consistency, runs the full publish gate
258
265
  (`prepublishOnly` = typecheck + tests + build), publishes to npm and the
259
266
  official MCP registry, verifies the registry listing, and creates the GitHub
260
267
  release (titled `opentakeoff-mcp <version>`). A re-run skips the npm publish
261
268
  if that version already shipped, so a transient failure downstream is safe to
262
269
  retry.
270
+
271
+ ### Refreshing the Smithery listing
272
+
273
+ Smithery isn't part of the automated release above — it needs a **separate,
274
+ manual** publish after any tool signature change, because of a genuine spec
275
+ conflict between two validators: the official MCPB validator (what
276
+ `npm run mcpb` gates on) rejects a `tools[].inputSchema` key outright, while
277
+ Smithery's registry rejects a bundle *without* real `inputSchema` per tool
278
+ (smithery-ai/cli#770, #797, #787 — no manifest satisfies both). The canonical
279
+ `dist-mcpb/opentakeoff-mcp.mcpb` stays spec-compliant for Claude Desktop / the
280
+ official registry / Glama; `scripts/build-smithery-mcpb.mjs` builds a
281
+ Smithery-only bundle instead, with live-introspected tools + inputSchema baked
282
+ in, packed with a plain zip (bypassing `mcpb validate`, which would reject it):
283
+
284
+ ```bash
285
+ npm run build
286
+ node scripts/build-smithery-mcpb.mjs
287
+ smithery mcp publish dist-smithery/opentakeoff-mcp.mcpb -n Kentucky-ai/opentakeoff
288
+ ```
@@ -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", "fleur", "speckle"];
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.8.0",
2550
+ version: "0.9.0",
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.",
@@ -2443,7 +2598,12 @@ var package_default = {
2443
2598
  "estimating",
2444
2599
  "pdf",
2445
2600
  "quantity-takeoff",
2446
- "flooring"
2601
+ "flooring",
2602
+ "ai-agent",
2603
+ "agentic-ai",
2604
+ "preconstruction",
2605
+ "construction-management",
2606
+ "project-management"
2447
2607
  ],
2448
2608
  overrides: {
2449
2609
  "fast-uri": "^3.1.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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.",
@@ -51,10 +51,15 @@
51
51
  "estimating",
52
52
  "pdf",
53
53
  "quantity-takeoff",
54
- "flooring"
54
+ "flooring",
55
+ "ai-agent",
56
+ "agentic-ai",
57
+ "preconstruction",
58
+ "construction-management",
59
+ "project-management"
55
60
  ],
56
61
  "overrides": {
57
62
  "fast-uri": "^3.1.4",
58
63
  "@hono/node-server": "^2.0.5"
59
64
  }
60
- }
65
+ }