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 +23 -3
- package/dist/server-core.js +74 -16
- package/package.json +2 -2
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.
|
package/dist/server-core.js
CHANGED
|
@@ -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
|
|
5647
|
+
const open2 = byShape.get(f.id) ?? 0;
|
|
5648
5648
|
const gross = f.computed.perimeter_lf ?? 0;
|
|
5649
|
-
if (
|
|
5650
|
-
throw new UserError(`Shape ${f.id}: stated openings (${round2(
|
|
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
|
|
5657
|
-
const net = round2(gross -
|
|
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(
|
|
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(
|
|
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: {
|
|
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.
|
|
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.
|
|
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",
|