opentakeoff-mcp 0.9.33 → 0.9.35

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
@@ -119,14 +119,14 @@ includes document text, shape vertices, or result payload content.
119
119
  | `symbol_sweep` | **Every instance of a repeated plan symbol, from ONE example**: marquee a tight `seed_rect` around a single drain/threshold/fixture symbol and the vector linework is searched deterministically for every other placement — translation plus 0/90/180/270 rotation and mirroring (both on by default). Score = length-weighted fraction of the seed's segments matched within `tolerance_px`; ≥ 0.92 is a match, the 0.75–0.92 band returns in `withheld` with reasons (never committed, never dropped silently), and the work cap is disclosed when it bites. **`scope: "set"` sweeps the whole working set, counting on PLAN-role sheets only** (the sheet graph decides; every excluded sheet disclosed in `skipped` with role and reason) — and the seed rect may sit on a detail or legend sheet, which then serves as the fingerprint SOURCE while staying excluded from counting: the estimator's "click the assembly in the detail, count it on the plans" gesture. Per-sheet results carry their own match/withheld lists, per-sheet cap accounting, and wall-clock `elapsed_ms`. `commit: true` + `condition` commits every match center as an EA count marker — the whole sweep (set-wide included) is one undo step, `origin.method "symbol_sweep"` with per-marker score, transform, and seed source (`origin.symbol.seed`). No scale required. |
120
120
  | `sweep_schedule_row` | **Take off a schedule row's mark from the row itself**: pass the row's key (e.g. `T1`) and the tool reads the row from the set's schedule tables (the row is the condition's cited source), anchors a fingerprint on the marker the tag is DRAWN as on a plan sheet (a deterministic pad ladder around the tag text; where the tag occurs more than once the fingerprint must recur at a second occurrence — `anchor.corroborated` — before it is trusted), and sweeps every plan-role sheet. **The count is geometry AND text agreeing**: drafting reuses one bubble shape across many marks, so a match counts only when the row's own tag sits within the marker footprint (its bbox rides the match as `tag_at` evidence); a match labeled with a sibling key is `excluded` and says whose it is, an unlabeled match is `withheld` as a question, a tag drawn with no matching marker is `text_only`. Refusal over guessing, each with the reason and the fix: no such row, an ambiguous key, a tag drawn on no plan sheet, no repeatable marker linework — a fingerprint is never guessed from text alone. `commit: true` commits the counted matches under the row's own key — one undo step, `origin.assignment {source: "schedule"}` plus the anchor and row citation on `origin.symbol.seed`. No scale required. |
121
121
  | `takeoff_summary` | Per-condition totals + grand totals, computed by the Report's rules. |
122
- | `export_takeoff` | The full `opentakeoff.takeoff_canvas.v1` payload — exactly what the app autosaves. Inline, and to disk with `path`. |
122
+ | `export_takeoff` | The full `opentakeoff.takeoff_canvas.v1` payload — exactly what the app autosaves. Inline, and to disk with `path` (see **Writing to disk** below). |
123
123
  | `delete_shape` | Remove a committed shape by id. |
124
124
  | `edit_shape` | **Revise** a committed shape instead of redoing it: new `verts`, a different `condition`, a different `role`, a `label` (the room it belongs to — what per-room reporting groups by; `""` clears it), or any combination — quantities recomputed from the result. Refuses shapes a human affirmed. |
125
125
  | `edit_materials` | Add/remove/patch supporting-materials rows on a condition — the coverage-rate lines (adhesive at N sf/gal, grout at N lf/bag, …) that turn a measured quantity into an order quantity, matching the canvas's Supporting Materials panel. `basis` is `area` \| `linear` \| `count` \| **`seam_lf`** — the last is the *figured* roll-layout seam length a weld rod or seam tape is bought by (set `roll_setup` on the condition first; without one it reads 0, because nothing has decided how that floor gets cut). `condition` mints on first touch, like `one_click`/`measure_polygon`. No review gate (materials rows are quantity config, not traced geometry) — edits directly, reversible with `undo_last`. |
126
126
  | `edit_condition` | Set a condition's **waste %**, **×N multiplier**, **height_ft** (the H knob `measure_surface` quantifies against), and **roll_setup** (the roll-goods opt-in: seams figured, cuts packed, the reply echoes the order — cuts, `order_lf`, rolls, `order_qty` — and `export_report`'s `roll_goods` block carries the same rows; `null` opts out) — the knobs that turn measured quantities into order quantities. Resolves an **existing** finish tag or errors — a typo must not mint an empty condition. No review gate; one `undo_last` step restores the knobs verbatim. |
127
- | `export_report` | The **computed Report document** — `opentakeoff.report.v1`, the same JSON the canvas Report exports: gross + waste-adjusted quantities, the computed materials **buy list** per condition plus the project-wide roll-up, per-sheet base subtotals, and scale provenance. The contract for pricing consumers — `export_takeoff` carries materials as config rows, `takeoff_summary` strips them. Inline, and to disk with `path`. |
127
+ | `export_report` | The **computed Report document** — `opentakeoff.report.v1`, the same JSON the canvas Report exports: gross + waste-adjusted quantities, the computed materials **buy list** per condition plus the project-wide roll-up, per-sheet base subtotals, and scale provenance. The contract for pricing consumers — `export_takeoff` carries materials as config rows, `takeoff_summary` strips them. Inline, and to disk with `path` (see **Writing to disk** below). |
128
128
  | `import_takeoff` | **The way back in**: load a `takeoff_canvas.v1` file (a prior `export_takeoff`, or the app's own save) through the SAME merge rules as the app's Sheet-menu import — finish-tag identity joins conditions (this session's knobs win), new ids append, duplicates skip (idempotent re-import), this session's calibration wins per sheet. Resume, extend, or audit. |
129
- | `export_marked_pdf` | The **marked-up planset** — the deliverable. Writes a distribution-ready PDF: a legend cover (per-condition totals, swatches, by-sheet breakdown) plus every sheet that carries work, vector-copied from the source with shapes, hatches, per-shape quantity chips, and annotations burned in — built by the same module as the canvas's MARKED SET button. Machine-traced shapes are disclosed as pending human review on the document itself, and the cover states where the finish tags came from (`Finish assignment: N schedule-resolved · N agent-asserted · …`, plus any rooms the last assign run withheld). Default path: `<plan> - marked set.pdf` next to the plan. Works without `@napi-rs/canvas`. |
129
+ | `export_marked_pdf` | The **marked-up planset** — the deliverable. Writes a distribution-ready PDF: a legend cover (per-condition totals, swatches, by-sheet breakdown) plus every sheet that carries work, vector-copied from the source with shapes, hatches, per-shape quantity chips, and annotations burned in — built by the same module as the canvas's MARKED SET button. Machine-traced shapes are disclosed as pending human review on the document itself, and the cover states where the finish tags came from (`Finish assignment: N schedule-resolved · N agent-asserted · …`, plus any rooms the last assign run withheld). Default path: `<plan> - marked set.pdf` next to the plan (see **Writing to disk** below). Works without `@napi-rs/canvas`. |
130
130
  | `list_shapes` | The **mid-session inventory**: every committed shape's id, sheet, condition, role, quantities, room `label`, review state, and assignment verdict (`schedule` \| `asserted` — where its finish tag came from) in one compact read — the ids `edit_shape`/`delete_shape` assume you have, without pulling the whole `export_takeoff` payload. Filters by sheet/condition narrow; empty is a result, not an error. |
131
131
  | `undo_last` | Step back over your own last `n` mutations, newest first. Exact inverses: a commit is removed, an edit restored verbatim, a delete re-inserted where it was, a materials edit's whole array restored, a condition edit's waste/multiplier pair restored. A whole `detect_rooms` sweep is **one** step. |
132
132
  | `annotate` | Place a note ABOUT the work — cloud/highlight (`rect`), text (`at`), callout (`at` + `target`), **arrow** (`from` + `to` — plank/seam direction), **bubble** (`at` + optional `r` — keynote circle, centered text), **dimension** (`from` + `to` — a dimension line with end ticks, labelled with the measured length at the sheet's scale; the one annotation the scale gate applies to — an unscaled sheet refuses like the measure tools). Attach to a condition and it wears that scope's colour on the canvas and in the marked set. No review gate: notes are not geometry. |
@@ -245,6 +245,26 @@ A takeoff finishes with **both** exports: `export_marked_pdf` is what a human
245
245
  reviews (construction takeoffs are no good without markup), `export_report` is
246
246
  what pricing consumes.
247
247
 
248
+ ### Writing to disk
249
+
250
+ `path` is not confined to a working directory, and deliberately so — the marked
251
+ set belongs in the job folder, wherever that is. What the export tools will not
252
+ do is destroy a file they didn't write:
253
+
254
+ - **Nothing at the path** → written.
255
+ - **A previous export of ours at the path** → overwritten silently. Fix a
256
+ condition and export again to the same path as often as you like; that's the
257
+ normal loop, and it needs no flag.
258
+ - **Any other existing file** → refused, with the path named. Pass
259
+ `overwrite: true` to replace it anyway.
260
+ - **Corrupt, encrypted, or unreadable** → treated as *not* ours, so refused. An
261
+ unrecognizable file is exactly the kind worth not overwriting.
262
+
263
+ A marked set is recognized by its PDF `Producer`; the JSON exports by the
264
+ `schema` key they stamp. This is data-loss protection, not a sandbox — the
265
+ server runs as you, with your privileges. See [`SECURITY.md`](../SECURITY.md)
266
+ for the threat model.
267
+
248
268
  Sheet keys follow the app's codec: page 1 is the bare file name
249
269
  (`plan.pdf`), pages 2+ are `plan.pdf#2`. Tools also accept the title-block
250
270
  sheet number (`A-101`) wherever a sheet is named.
@@ -5644,25 +5644,25 @@ var Session = class _Session {
5644
5644
  byShape.set(o.shape_id, (byShape.get(o.shape_id) ?? 0) + o.lf);
5645
5645
  }
5646
5646
  for (const f of floors) {
5647
- const open = byShape.get(f.id) ?? 0;
5647
+ const open2 = byShape.get(f.id) ?? 0;
5648
5648
  const gross = f.computed.perimeter_lf ?? 0;
5649
- if (open >= gross) {
5650
- throw new UserError(`Shape ${f.id}: stated openings (${round2(open)} LF) meet or exceed its perimeter (${round2(gross)} LF) \u2014 nothing would remain.`);
5649
+ if (open2 >= gross) {
5650
+ throw new UserError(`Shape ${f.id}: stated openings (${round2(open2)} LF) meet or exceed its perimeter (${round2(gross)} LF) \u2014 nothing would remain.`);
5651
5651
  }
5652
5652
  }
5653
5653
  const rooms = floors.map((f) => {
5654
5654
  const s = this.sheet(f.sheet_id);
5655
5655
  const gross = f.computed.perimeter_lf ?? 0;
5656
- const open = byShape.get(f.id) ?? 0;
5657
- const net = round2(gross - open);
5656
+ const open2 = byShape.get(f.id) ?? 0;
5657
+ const net = round2(gross - open2);
5658
5658
  const ringPx = f.verts_norm.map(([x, y]) => [x * s.widthPx, y * s.heightPx]);
5659
5659
  const shape = this.commit(s, opts.condition, "linear", [...ringPx, ringPx[0]], { area_sf: 0, perimeter_lf: net }, {
5660
5660
  method: "agent_v1",
5661
5661
  actor: "agent",
5662
5662
  reviewed: false,
5663
- derived: { from_shape_id: f.id, gross_lf: round2(gross), openings_lf: round2(open) }
5663
+ derived: { from_shape_id: f.id, gross_lf: round2(gross), openings_lf: round2(open2) }
5664
5664
  });
5665
- return { source_shape_id: f.id, base_shape_id: shape.id, sheet: f.sheet_id, gross_lf: round2(gross), openings_lf: round2(open), net_lf: net };
5665
+ return { source_shape_id: f.id, base_shape_id: shape.id, sheet: f.sheet_id, gross_lf: round2(gross), openings_lf: round2(open2), net_lf: net };
5666
5666
  });
5667
5667
  this.flushCommits("derive_base");
5668
5668
  return {
@@ -8601,6 +8601,8 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
8601
8601
  if (!marked.length && !rfis?.length) throw new Error("Nothing to export \u2014 no sheet carries takeoffs or markups.");
8602
8602
  const markedShapes = marked.flatMap((sh) => shapesBy.get(sh.key) || []);
8603
8603
  const doc = await PDFDocument.create();
8604
+ doc.setProducer("OpenTakeoff");
8605
+ doc.setCreator("OpenTakeoff");
8604
8606
  const font = await doc.embedFont(StandardFonts.Helvetica);
8605
8607
  const bold = await doc.embedFont(StandardFonts.HelveticaBold);
8606
8608
  const ink = dark ? rgb(0.93, 0.92, 0.89) : rgb(0.13, 0.12, 0.1);
@@ -9033,6 +9035,54 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
9033
9035
  return { bytes, filename };
9034
9036
  }
9035
9037
 
9038
+ // src/safewrite.ts
9039
+ import { open, stat } from "node:fs/promises";
9040
+ var PDF_PRODUCER = "OpenTakeoff";
9041
+ async function head(p, n) {
9042
+ const fh = await open(p, "r");
9043
+ try {
9044
+ const buf = Buffer.alloc(n);
9045
+ const { bytesRead } = await fh.read(buf, 0, n, 0);
9046
+ return buf.subarray(0, bytesRead);
9047
+ } finally {
9048
+ await fh.close();
9049
+ }
9050
+ }
9051
+ async function isOwnExport(outPath, kind) {
9052
+ try {
9053
+ if (kind === "json") {
9054
+ return /"schema"\s*:\s*"opentakeoff\./.test((await head(outPath, 4096)).toString("utf8"));
9055
+ }
9056
+ if ((await head(outPath, 5)).toString("latin1") !== "%PDF-") return false;
9057
+ const { readFile: readFile4 } = await import("node:fs/promises");
9058
+ const { PDFDocument } = await import("pdf-lib");
9059
+ const doc = await PDFDocument.load(await readFile4(outPath), {
9060
+ ignoreEncryption: true,
9061
+ updateMetadata: false
9062
+ });
9063
+ return (doc.getProducer() ?? "").includes(PDF_PRODUCER);
9064
+ } catch {
9065
+ return false;
9066
+ }
9067
+ }
9068
+ async function assertWritable(outPath, kind, overwrite) {
9069
+ if (overwrite === true) return;
9070
+ let st;
9071
+ try {
9072
+ st = await stat(outPath);
9073
+ } catch {
9074
+ return;
9075
+ }
9076
+ if (st.isDirectory()) {
9077
+ throw new UserError(`${outPath} is a directory \u2014 pass the full file path to write, not the folder.`);
9078
+ }
9079
+ if (await isOwnExport(outPath, kind)) return;
9080
+ throw new UserError(
9081
+ `${outPath} already exists and is not an OpenTakeoff export, so writing there would destroy it. Write somewhere else, or pass overwrite: true if replacing that exact file is what you mean to do.`
9082
+ );
9083
+ }
9084
+ var OVERWRITE_DESC = "Replace the file at path even when it is not an OpenTakeoff export. Off by default: re-exporting over a previous export of your own already overwrites without this, so you only need it to deliberately destroy an unrelated file.";
9085
+
9036
9086
  // src/marked.ts
9037
9087
  function assignmentDisclosure(shapes, withheld = []) {
9038
9088
  const agent = shapes.filter((s) => s.origin?.actor === "agent");
@@ -9055,6 +9105,9 @@ async function exportMarkedPdf(session, opts) {
9055
9105
  if (!session.shapes.length && !session.markups.length && !session.approvals.length) {
9056
9106
  throw new UserError("Nothing to mark yet \u2014 commit shapes (one_click / detect_rooms / measure_polygon / measure_line with a condition) or annotate before exporting the marked set.");
9057
9107
  }
9108
+ const base = file.replace(/\.pdf$/i, "");
9109
+ const outPath = path3.resolve(opts.path ?? path3.join(path3.dirname(filePath), `${base} - marked set.pdf`));
9110
+ await assertWritable(outPath, "pdf", opts.overwrite);
9058
9111
  const sheetStates = session.sheetList();
9059
9112
  const byFilePage = new Map(sheetStates.map((s) => [`${session.fileFor(s.key)}#${s.pageNum}`, s.page]));
9060
9113
  const sheets = sheetStates.map((s) => ({
@@ -9086,7 +9139,6 @@ async function exportMarkedPdf(session, opts) {
9086
9139
  }
9087
9140
  return bytes2;
9088
9141
  };
9089
- const base = file.replace(/\.pdf$/i, "");
9090
9142
  const machine = session.shapes.filter((s) => s.origin?.reviewed !== true).length;
9091
9143
  const credit = machine ? `Machine-traced via OpenTakeoff MCP \u2014 ${machine} shape${machine === 1 ? "" : "s"} pending human review` : null;
9092
9144
  const { bytes } = await buildMarkedSetPdf2({
@@ -9108,7 +9160,6 @@ async function exportMarkedPdf(session, opts) {
9108
9160
  provenance: assignmentDisclosure(session.shapes, session.scheduleWithheld),
9109
9161
  coverTitle: "OpenTakeoff \xB7 Marked Set"
9110
9162
  });
9111
- const outPath = path3.resolve(opts.path ?? path3.join(path3.dirname(filePath), `${base} - marked set.pdf`));
9112
9163
  await writeFile(outPath, bytes);
9113
9164
  const markedKeys = /* @__PURE__ */ new Set([...session.shapes.map((s) => s.sheet_id), ...session.markups.map((m) => m.sheet_id), ...session.approvals.map((a) => a.sheet_id)]);
9114
9165
  return {
@@ -9497,11 +9548,15 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
9497
9548
  }, run("takeoff_summary", () => session.summary()));
9498
9549
  server.registerTool("export_takeoff", {
9499
9550
  description: `The full "opentakeoff.takeoff_canvas.v1" annotations payload \u2014 exactly what the app autosaves, importable by it. Returned inline; pass path to also write it to disk as JSON. ${COORDS}`,
9500
- inputSchema: { path: z2.string().optional().describe("File path to write the payload to") },
9551
+ inputSchema: {
9552
+ path: z2.string().optional().describe("File path to write the payload to"),
9553
+ overwrite: z2.boolean().optional().describe(OVERWRITE_DESC)
9554
+ },
9501
9555
  outputSchema: exportTakeoffOutput
9502
- }, run("export_takeoff", async ({ path: outPath }) => {
9556
+ }, run("export_takeoff", async ({ path: outPath, overwrite }) => {
9503
9557
  const payload = session.exportPayload();
9504
9558
  if (outPath) {
9559
+ await assertWritable(outPath, "json", overwrite);
9505
9560
  const { writeFile: writeFile2 } = await import("node:fs/promises");
9506
9561
  await writeFile2(outPath, JSON.stringify(payload));
9507
9562
  }
@@ -9511,12 +9566,14 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
9511
9566
  description: `The computed Report document \u2014 "opentakeoff.report.v1", the same schema the canvas Report's JSON export writes. Everything a pricing consumer needs without re-implementing the app's math: per-condition quantities with waste and multiplier applied (gross and *_net), the computed materials BUY LIST per condition (order quantity = basis \xF7 coverage rate, rounded up to whole purchase units) plus the project-wide roll-up summed by (name, unit), per-sheet BASE subtotals, scale provenance per sheet, and annotations. Contrast: export_takeoff is the raw canvas payload (materials as CONFIG rows, no computed quantities) and takeoff_summary strips materials for a compact reply \u2014 when the numbers are leaving for pricing, consume this. A report alone is HALF the deliverable: pair it with export_marked_pdf, because a takeoff is reviewed on marked drawings, not on numbers. Returned inline; pass path to also write it to disk as JSON.`,
9512
9567
  inputSchema: {
9513
9568
  path: z2.string().optional().describe("File path to write the document to"),
9514
- project_name: z2.string().optional().describe("Label for the document's project_name field (a headless session has no project of its own; omitted \u2192 null)")
9569
+ project_name: z2.string().optional().describe("Label for the document's project_name field (a headless session has no project of its own; omitted \u2192 null)"),
9570
+ overwrite: z2.boolean().optional().describe(OVERWRITE_DESC)
9515
9571
  },
9516
9572
  outputSchema: exportReportOutput
9517
- }, run("export_report", async ({ path: outPath, project_name: projectName }) => {
9573
+ }, run("export_report", async ({ path: outPath, project_name: projectName, overwrite }) => {
9518
9574
  const doc = session.exportReport(projectName);
9519
9575
  if (outPath) {
9576
+ await assertWritable(outPath, "json", overwrite);
9520
9577
  const { writeFile: writeFile2 } = await import("node:fs/promises");
9521
9578
  await writeFile2(outPath, JSON.stringify(doc));
9522
9579
  }
@@ -9533,7 +9590,8 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
9533
9590
  description: `The MARKED-UP PLANSET \u2014 the deliverable of every takeoff. Writes a distribution-ready PDF to disk: a legend cover (per-condition totals, swatches, a by-sheet breakdown) followed by every sheet that carries takeoff shapes or annotations, vector-copied from the source plan with the work burned in as drawn \u2014 condition colors and hatches, a quantity chip on every shape, annotation clouds/callouts/highlights, and approval marks (the estimator's APPROVED rings, the agent's AGENT diamonds \u2014 the cover tallies the split). Built by the same module as the canvas's MARKED SET button, so agent output and app output are one implementation. A construction takeoff is no good without markup: finish EVERY takeoff by writing this file and giving the user its path (export_report carries the numbers for pricing; this carries the evidence). When the shapes were machine-traced and unreviewed, the document says so on its last page \u2014 the review path is importing the export_takeoff payload into the app, where agent shapes arrive as pencil proposals. Default path: next to the loaded plan as "<plan> - marked set.pdf". Needs no native canvas \u2014 pure vector copy, so it works even where view_sheet cannot render.`,
9534
9591
  inputSchema: {
9535
9592
  path: z2.string().optional().describe('Where to write the PDF (default: "<plan dir>/<plan> - marked set.pdf")'),
9536
- project_name: z2.string().optional().describe("Cover-page project name (default: the plan file's name)")
9593
+ project_name: z2.string().optional().describe("Cover-page project name (default: the plan file's name)"),
9594
+ overwrite: z2.boolean().optional().describe(OVERWRITE_DESC)
9537
9595
  },
9538
9596
  outputSchema: exportMarkedPdfOutput
9539
9597
  }, run("export_marked_pdf", (a) => exportMarkedPdf(session, a)));
@@ -9830,7 +9888,7 @@ function registerResources(server, session) {
9830
9888
  // package.json
9831
9889
  var package_default = {
9832
9890
  name: "opentakeoff-mcp",
9833
- version: "0.9.33",
9891
+ version: "0.9.35",
9834
9892
  mcpName: "io.github.Kentucky-ai/opentakeoff",
9835
9893
  type: "module",
9836
9894
  description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
@@ -9846,7 +9904,7 @@ var package_default = {
9846
9904
  mcpb: "npm run build && node scripts/build-mcpb.mjs",
9847
9905
  prepublishOnly: "npm run typecheck && npm test && npm run build",
9848
9906
  typecheck: "tsc --noEmit",
9849
- 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/scalewarn.test.ts test/session.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
9907
+ 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"
9850
9908
  },
9851
9909
  dependencies: {
9852
9910
  "@modelcontextprotocol/sdk": "^1.12.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.9.33",
3
+ "version": "0.9.35",
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/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/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",