opentakeoff-mcp 0.9.49 → 0.9.56

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
@@ -4,9 +4,15 @@ Listed in the [official MCP registry](https://registry.modelcontextprotocol.io)
4
4
  `io.github.Kentucky-ai/opentakeoff`, on [Glama](https://glama.ai/mcp/servers/Kentucky-ai/opentakeoff),
5
5
  and on [Smithery](https://smithery.ai/servers/Kentucky-ai/opentakeoff).
6
6
 
7
+ **This page is the reference—every tool, every rule, every limit.** For *how to run a
8
+ takeoff well* with it—the operating model, the standard finish, what the engine withholds
9
+ and why, and the move that answers each refusal—read
10
+ [`docs/AGENT_GUIDE.md`](../docs/AGENT_GUIDE.md) first. It's short, and it's the half that
11
+ decides whether the numbers are any good.
12
+
7
13
  ## Run it in 60 seconds (npx)
8
14
 
9
- No clone, no build point your MCP client at the published package:
15
+ No clone, no build—point your MCP client at the published package:
10
16
 
11
17
  ```json
12
18
  {
@@ -25,7 +31,7 @@ Works with Claude Code (`claude mcp add opentakeoff -- npx -y opentakeoff-mcp`),
25
31
 
26
32
  No Node, no npm: download **`opentakeoff-mcp.mcpb`** from the
27
33
  [latest release](https://github.com/Kentucky-ai/opentakeoff/releases) and
28
- double-click it Claude Desktop installs the server with its dependencies
34
+ double-click it—Claude Desktop installs the server with its dependencies
29
35
  bundled. Built by `npm run mcpb` and attached automatically to every `mcp-v*`
30
36
  release. The bundle is platform-neutral on purpose: it excludes the optional
31
37
  native canvas, so every JSON tool and the text/metadata resources work
@@ -33,7 +39,7 @@ everywhere; the sheet-image resource and the `view_sheet` tool say exactly
33
39
  what's missing where rendering isn't available.
34
40
 
35
41
 
36
- The takeoff engine One-Click Area, the scale model, conditions, totals on
42
+ The takeoff engine—One-Click Area, the scale model, conditions, totals—on
37
43
  **stdio for your MCP client**. An agent can open a plan, read the title block,
38
44
  set the scale, click rooms, and hand back the same takeoff payload the browser
39
45
  app autosaves. Same engine, same math: the server imports
@@ -83,10 +89,10 @@ Then register it with your MCP client (any stdio MCP client works):
83
89
  }
84
90
  ```
85
91
 
86
- Point `command` at `node` directly, as above — **never `npm start` in a client
92
+ Point `command` at `node` directly, as above—**never `npm start` in a client
87
93
  config**: npm prints its banner to stdout, and stdout is the MCP wire. (Same
88
- reason the server redirects `console.log` to stderr before pdf.js loads
89
- see `src/hush.ts`.)
94
+ reason the server redirects `console.log` to stderr before pdf.js loads—see
95
+ `src/hush.ts`.)
90
96
 
91
97
  `tsx` is a runtime dependency, not a build tool: the engine is imported
92
98
  straight from `web/src/lib` as TypeScript, so plain `node` can't run it.
@@ -103,7 +109,7 @@ includes document text, shape vertices, or result payload content.
103
109
 
104
110
  ### Staged tool exposure (opt-in)
105
111
 
106
- By default every client gets all 40 tool schemas on `tools/list` — the flat
112
+ By default every client gets all 40 tool schemas on `tools/list`—the flat
107
113
  contract every published client already expects. Forty descriptions is real
108
114
  token weight for an agent session that may never touch half of them, so the
109
115
  server can instead stage the surface along the workflow it already teaches:
@@ -112,13 +118,13 @@ server can instead stage the surface along the workflow it already teaches:
112
118
  OPENTAKEOFF_MCP_STAGED_TOOLS=1 npx -y opentakeoff-mcp
113
119
  ```
114
120
 
115
- Staged, only the **setup** stage (load, scale, read the set 10 tools) starts
121
+ Staged, only the **setup** stage (load, scale, read the set—10 tools) starts
116
122
  enabled, plus one opener: `open_tool_stage`. Calling it with `"measure"`,
117
123
  `"revise"`, or `"handoff"` enables that stage's tools and fires
118
124
  `tools/list_changed`, so any client that supports dynamic tool lists (Claude
119
125
  Code, Claude Desktop, anything built against the current spec) sees the group
120
126
  appear the moment the agent asks for it. Opening is idempotent and never
121
- closes anything the surface only grows. The initialize instructions state
127
+ closes anything—the surface only grows. The initialize instructions state
122
128
  the scheme, so an agent knows to open a stage before it needs one. Requires a
123
129
  client that honors `tools/list_changed`; leave the flag unset for one that
124
130
  reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/issues/230))
@@ -127,53 +133,53 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
127
133
 
128
134
  | Tool | What it does |
129
135
  |---|---|
130
- | `load_plan` | Open a plan PDF from disk. Default replaces the whole session; **`merge: true` ADDS the document to the working set** (#152) plans + schedule + addenda as one takeoff, sheet graph spanning the whole set, marked set covering every worked sheet. Returns per-sheet dims, title-block `sheet_number`, and the detected drawn scale where present. |
136
+ | `load_plan` | Open a plan PDF from disk. Default replaces the whole session; **`merge: true` ADDS the document to the working set** (#152)—plans + schedule + addenda as one takeoff, sheet graph spanning the whole set, marked set covering every worked sheet. Returns per-sheet dims, title-block `sheet_number`, and the detected drawn scale where present. |
131
137
  | `sheet_info` | One sheet's dims, vector segment count, scale status, detected suggestion, committed shape count. |
132
- | `set_scale` | Set a sheet's scale exactly one of `label`, `upp`, `calibrate {p1, p2, feet}`, `use_detected`. **Lands unconfirmed** (`confirmed: false`) until a human confirms in the canvas see Scale rules. |
133
- | `one_click` | One-Click Area at (x, y): the sealed flood engine bounded by the plan linework, traced, vertices snapped the SAME feet-true arguments the canvas passes at a click (gap sealing up to a door width, door-swing wedge inclusion, the half-foot minimum-passage rule), so an MCP trace and a canvas click at one seed measure the same square footage (pinned against the bench corpus goldens in `test/parity.test.ts`). Every trace carries the engine's account of itself: `confidence` 0–1 with `confidence_factors` naming each deduction (`gap_sealed_px`, `door_wedges`, `min_pass_delta`, …) a review prioritizer, never a verification; a low score is a `view_sheet {overlay: true}` audit prompt, not a fact. On a SCANNED sheet (no usable linework) the flood falls back automatically to the rendered pixels same engine as the canvas with `raster_traced` disclosed on the reply and on the shape's origin (#154). Pass `condition` to commit (the full account stamps `origin` centrally at the commit); `role: "deduct"` subtracts. |
134
- | `detect_rooms` | Batch One-Click: reads every room-number label off the sheet's text layer and floods each one call instead of `read_sheet_text` + reasoning + N `one_click` calls, through the SAME sealed engine per room (confidence + the engine account ride each room and its committed origin). Only cleanly-traced rooms come back; everything skipped is counted and reasoned in `withheld` (degenerate / duplicate / implausible / unresolved), never dropped silently. To commit: `assign_from_schedule: true` routes each room through its OWN room-finish schedule row and commits under the FLOOR finish that row states (rooms the schedule can't answer for return in `unresolved[]` with reasons and re-seedable coordinates); or pass `condition` to commit every room under one stated tag. |
138
+ | `set_scale` | Set a sheet's scale—exactly one of `label`, `upp`, `calibrate {p1, p2, feet}`, `use_detected`. **Lands unconfirmed** (`confirmed: false`) until a human confirms in the canvas—see Scale rules. |
139
+ | `one_click` | One-Click Area at (x, y): the sealed flood engine bounded by the plan linework, traced, vertices snapped—the SAME feet-true arguments the canvas passes at a click (gap sealing up to a door width, door-swing wedge inclusion, the half-foot minimum-passage rule), so an MCP trace and a canvas click at one seed measure the same square footage (pinned against the bench corpus goldens in `test/parity.test.ts`). Every trace carries the engine's account of itself: `confidence` 0–1 with `confidence_factors` naming each deduction (`gap_sealed_px`, `door_wedges`, `min_pass_delta`, …)—a review prioritizer, never a verification; a low score is a `view_sheet {overlay: true}` audit prompt, not a fact. On a SCANNED sheet (no usable linework) the flood falls back automatically to the rendered pixels—same engine as the canvas—with `raster_traced` disclosed on the reply and on the shape's origin (#154). Pass `condition` to commit (the full account stamps `origin` centrally at the commit); `role: "deduct"` subtracts. |
140
+ | `detect_rooms` | Batch One-Click: reads every room-number label off the sheet's text layer and floods each—one call instead of `read_sheet_text` + reasoning + N `one_click` calls, through the SAME sealed engine per room (confidence + the engine account ride each room and its committed origin). Only cleanly-traced rooms come back; everything skipped is counted and reasoned in `withheld` (degenerate / duplicate / implausible / unresolved), never dropped silently. To commit: `assign_from_schedule: true` routes each room through its OWN room-finish schedule row and commits under the FLOOR finish that row states (rooms the schedule can't answer for return in `unresolved[]` with reasons and re-seedable coordinates); or pass `condition` to commit every room under one stated tag. |
135
141
  | `measure_polygon` | Area + perimeter of a polygon you supply (min 3 verts). Requires scale. |
136
- | `cut_out` | **A real hole in a committed floor shape** (#206), reconciled the way the canvas cuts one (#137) the same `lib/cutout.js` boolean subtract, one module, so a headless session and the app can never disagree about what a hole holds. The parent keeps its outer ring + `verts_norm_holes`, its `computed` nets for real (N cuts compose, overlap never double-deducts, a hole ADDS perimeter), and the deduct carries `cuts_shape_id` so report/legend read the reconciled number. Refuses a ring not FULLY inside the parent (the canvas's edge-clip is a canvas affordance; over the wire it's refusal-over-guessing) and a cut that would erase or split the parent. One undo step restores parent and hole together; `delete_shape` on the deduct reverts the cut too (multi-cut parents rebuild from the chain's pristine snapshot minus survivors the canvas's own delete semantics, ported as the spec). |
142
+ | `cut_out` | **A real hole in a committed floor shape** (#206), reconciled the way the canvas cuts one (#137)—the same `lib/cutout.js` boolean subtract, one module, so a headless session and the app can never disagree about what a hole holds. The parent keeps its outer ring + `verts_norm_holes`, its `computed` nets for real (N cuts compose, overlap never double-deducts, a hole ADDS perimeter), and the deduct carries `cuts_shape_id` so report/legend read the reconciled number. Refuses a ring not FULLY inside the parent (the canvas's edge-clip is a canvas affordance; over the wire it's refusal-over-guessing) and a cut that would erase or split the parent. One undo step restores parent and hole together; `delete_shape` on the deduct reverts the cut too (multi-cut parents rebuild from the chain's pristine snapshot minus survivors—the canvas's own delete semantics, ported as the spec). |
137
143
  | `measure_line` | Length of an open polyline (min 2 points). Requires scale. |
138
- | `derive_base` | **Base LF from committed rooms**: for every floor shape of a source condition, commits a linear base run tracing that room's boundary, quantified net of the door openings YOU state per room (`{shape_id, lf}` — your claim, recorded on `origin.derived`; the tool never guesses). All-or-nothing; one undo step. |
139
- | `derive_transitions` | **The transition where two finishes meet**: pass two finish tags and the tag to commit under, and every committed room of each is compared against every room of the other. The catch this is built around flood-traced rooms **do not share edges**, a partition puts 4–8″ between them so proximity comes in two flavours and they are never conflated. A **butt joint** (rings running together inside one open space, within an inch) *is* the transition and commits as a linear shape, `origin.derived` naming both parents, the tags, and the measured gap. A **wall-separated** run means the rooms are adjacent across a partition, where the transition is a threshold in a doorway that nothing in the trace record locates (the flood engine reports how *much* boundary it sealed, never where) those return in `withheld` with length, gap in inches, and an `at` point to `view_sheet`, as questions rather than a confident wrong number. `max_gap_in` (default 12) only ever turns more of the plan into questions, never into committed LF. All-or-nothing; one undo step. |
140
- | `measure_surface` | **Wall SF**: an open run traced along the wall, quantified as traced LF × the condition's height (the canvas's H knob pass `height_ft` to set it, or set it once with `edit_condition`). Wall tile, wainscot, wall systems. Refuses without a height, minting nothing. |
141
- | `place_count` | **EA markers**: one point, one each thresholds, stair nosings, floor boxes. No scale required (EA is scale-free). One shape per point; the whole call is one undo step. |
142
- | `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. |
143
- | `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. |
144
+ | `derive_base` | **Base LF from committed rooms**: for every floor shape of a source condition, commits a linear base run tracing that room's boundary, quantified net of the door openings YOU state per room (`{shape_id, lf}`—your claim, recorded on `origin.derived`; the tool never guesses). All-or-nothing; one undo step. |
145
+ | `derive_transitions` | **The transition where two finishes meet**: pass two finish tags and the tag to commit under, and every committed room of each is compared against every room of the other. The catch this is built around—flood-traced rooms **do not share edges**, a partition puts 4–8″ between them—so proximity comes in two flavours and they are never conflated. A **butt joint** (rings running together inside one open space, within an inch) *is* the transition and commits as a linear shape, `origin.derived` naming both parents, the tags, and the measured gap. A **wall-separated** run means the rooms are adjacent across a partition, where the transition is a threshold in a doorway that nothing in the trace record locates (the flood engine reports how *much* boundary it sealed, never where)—those return in `withheld` with length, gap in inches, and an `at` point to `view_sheet`, as questions rather than a confident wrong number. `max_gap_in` (default 12) only ever turns more of the plan into questions, never into committed LF. All-or-nothing; one undo step. |
146
+ | `measure_surface` | **Wall SF**: an open run traced along the wall, quantified as traced LF × the condition's height (the canvas's H knob—pass `height_ft` to set it, or set it once with `edit_condition`). Wall tile, wainscot, wall systems. Refuses without a height, minting nothing. |
147
+ | `place_count` | **EA markers**: one point, one each—thresholds, stair nosings, floor boxes. No scale required (EA is scale-free). One shape per point; the whole call is one undo step. |
148
+ | `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. **Counter-examples** (`exclude`, #259): rects around instances you do NOT mean, marqueed like the seed—the rect's own contents decide whether it rejects by extra contained linework or by the background line running THROUGH it that a real instance would break; every rejection disclosed in `rejected[]`, reinstatable with `place_count`, dead negatives refused with instructions. **Stroke-luminance gate** (`luminance_tolerance`, #260): for flattened exports where layers and pen weights are stripped but the file still states stroke color—a stated tolerance holds candidates to the seed's own pen, opt-in both ways, with `lum_gate` naming every placement the pen pulled under the bar. |
149
+ | `sweep_schedule_row` | **Take off a schedule row's mark from the row itself**: pass the row's key (for example, `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. |
144
150
  | `takeoff_summary` | Per-condition totals + grand totals, computed by the Report's rules. |
145
- | `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). |
151
+ | `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). |
146
152
  | `delete_shape` | Remove a committed shape by id. |
147
- | `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. |
148
- | `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`. |
149
- | `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. |
150
- | `duplicate_condition` | **Twin a condition** — the same finish measured somewhere else, with its own supporting materials. One finish in two areas is neither two conditions nor one: the same sheet goods over a slab and over a raised deck take the same field material and different preparation underneath. The twin arrives carrying the original's whole materials list and keeps **following** it fix a coverage rate on the original and every twin that hasn't touched that row gets it; edit a row on the twin and only THAT row stops following. `label` is required and becomes the tag suffix (`CPT-1` + `Level 2` → `CPT-1 – Level 2`). Reversible with `undo_last`. |
151
- | `split_condition` | **Cut a twin loose** from its family: every following material row freezes at its current values and edits to the original stop reaching it. It keeps its finish tag and still groups with its siblings only the inheritance ends. For when two variants have diverged far enough that following each other is wrong. A condition that already owns its materials returns `split: false` rather than erroring. Reversible with `undo_last`. |
152
- | `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). |
153
- | `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. Correction rules (#88) ride the file too — `apply_rules` re-runs them. |
154
- | `apply_rules` | **Re-run the correction rules the takeoff arrived with** (#207) the lessons an estimator TAUGHT the canvas (#88), e.g. "every room like this loses the mechanical chase". Same pure `rules.ts` engine the canvas Preview runs; committed as the ONE batch its Apply makes (`reviewed: false`, one undo step, `origin {method rule_v1, actor rule}` with the rule/seed/room citation). The reply's per-rule disclosure produced, skipped, ids IS the preview an agent gets. Idempotent by construction: anything an existing deduct covers is dropped by the engine, so re-running after new rooms commit is the intended workflow. Rules arrive ONLY via `import_takeoff`; minting one is an estimator's correction and stays behind the canvas's human Preview→Apply gate. |
155
- | `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`. |
156
- | `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. |
153
+ | `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. |
154
+ | `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`. |
155
+ | `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. |
156
+ | `duplicate_condition` | **Twin a condition**—the same finish measured somewhere else, with its own supporting materials. One finish in two areas is neither two conditions nor one: the same sheet goods over a slab and over a raised deck take the same field material and different preparation underneath. The twin arrives carrying the original's whole materials list and keeps **following** it—fix a coverage rate on the original and every twin that hasn't touched that row gets it; edit a row on the twin and only THAT row stops following. `label` is required and becomes the tag suffix (`CPT-1` + `Level 2` → `CPT-1 – Level 2`). Reversible with `undo_last`. |
157
+ | `split_condition` | **Cut a twin loose** from its family: every following material row freezes at its current values and edits to the original stop reaching it. It keeps its finish tag and still groups with its siblings—only the inheritance ends. For when two variants have diverged far enough that following each other is wrong. A condition that already owns its materials returns `split: false` rather than erroring. Reversible with `undo_last`. |
158
+ | `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). |
159
+ | `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. Correction rules (#88) ride the file too—`apply_rules` re-runs them. |
160
+ | `apply_rules` | **Re-run the correction rules the takeoff arrived with** (#207)—the lessons an estimator TAUGHT the canvas (#88), for example, "every room like this loses the mechanical chase". Same pure `rules.ts` engine the canvas Preview runs; committed as the ONE batch its Apply makes (`reviewed: false`, one undo step, `origin {method rule_v1, actor rule}` with the rule/seed/room citation). The reply's per-rule disclosure—produced, skipped, ids—IS the preview an agent gets. Idempotent by construction: anything an existing deduct covers is dropped by the engine, so re-running after new rooms commit is the intended workflow. Rules arrive ONLY through `import_takeoff`; minting one is an estimator's correction and stays behind the canvas's human Preview→Apply gate. |
161
+ | `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`. |
162
+ | `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. |
157
163
  | `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. |
158
- | `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. |
159
- | `list_annotations` | Every annotation with its condition RESOLVED to a finish tag, coordinates back in image px; filter by sheet/condition. `unattached` counts the link_annotation candidates. `verdicts[]` is the approval family's inventory every mark with its actor stated, a condition filter reaching a verdict through its target shape. |
160
- | `link_annotation` | Attach an existing annotation to a condition (or detach with an empty tag) the canvas's Attach/Detach control, reachable by an agent. |
161
- | `mark_verdict` | The **agent's pencil-signature** on work it checked the agent half of the approval family (#176). Mints the graphite AGENT diamond, and structurally nothing else: the tool takes no actor input, so the estimator's APPROVED ring stays behind the canvas's human-only Approve tool. Target a committed `shape_id` (anchored on the shape a room's centroid, a run's midpoint with the id recorded as provenance) or a `sheet` + `at` point; optional short `text` rides the record. Touches no quantity; renders on the canvas and in the marked set, whose cover tallies the split (`Approval stamps: N estimator-approved · M agent-marked`); rides the annotations payload through `export_takeoff`/`import_takeoff` and the app's own saves. One mark per shape. |
162
- | `delete_verdict` | Lift an agent verdict mark by id. Agent marks only the estimator's seal is human ink and is refused, the same line `edit_shape` holds on reviewed shapes. `undo_last` re-seats a lifted mark exactly where it was. |
163
- | `read_sheet_text` | Positioned page text (image px), optionally restricted to a region title blocks, room labels, finish schedules. |
164
- | `find_text` | **Locate** a known string the complement to `read_sheet_text` (which returns what a region *says*; this finds *where* a string sits). Case-insensitive substring match per pdf.js text run; each hit's center feeds straight into `one_click`'s seed. |
165
- | `sheet_graph` | The plan-set INDEX (#87): every sheet's role with evidence, the schedule tables found, every room tag with its stacked name, the detail callouts, and every revision marker (text `Δ2`/`REV 2` tags AND drawn deltas a bare digit inside a triangle of linework, proven from the sheet's vector geometry in `revisions`) how an agent decides WHAT to measure without a human enumerating rooms. |
166
- | `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. A delta/REV marker on the answering row rides the result as `revisions` — the codes are the post-revision answer, and you're told the ink changed. |
167
- | `find_schedule` | Locate a schedule table by kind ("room finish", "material") sheet, title, headers, row count, a `view_sheet`-ready region, and `revised_rows` when delta/REV-marked rows exist. |
168
- | `sheet_context` | The region's STRUCTURE in one frame: classified vector segments (endpoints as drawn, meta byte per segment), text spans with bboxes, and hatch-family instances with content-derived ids same pattern spec ⇒ same id anywhere on the sheet, so plan↔legend matching is `id === id`. Decimation is declared and counted on every reply: `kept + dropped === total_in_region`, cap applies longest-first so walls survive. |
164
+ | `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, labeled 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 color on the canvas and in the marked set. No review gate: notes are not geometry. |
165
+ | `list_annotations` | Every annotation with its condition RESOLVED to a finish tag, coordinates back in image px; filter by sheet/condition. `unattached` counts the link_annotation candidates. `verdicts[]` is the approval family's inventory—every mark with its actor stated, a condition filter reaching a verdict through its target shape. |
166
+ | `link_annotation` | Attach an existing annotation to a condition (or detach with an empty tag)—the canvas's Attach/Detach control, reachable by an agent. |
167
+ | `mark_verdict` | The **agent's pencil-signature** on work it checked—the agent half of the approval family (#176). Mints the graphite AGENT diamond, and structurally nothing else: the tool takes no actor input, so the estimator's APPROVED ring stays behind the canvas's human-only Approve tool. Target a committed `shape_id` (anchored on the shape—a room's centroid, a run's midpoint—with the id recorded as provenance) or a `sheet` + `at` point; optional short `text` rides the record. Touches no quantity; renders on the canvas and in the marked set, whose cover tallies the split (`Approval stamps: N estimator-approved · M agent-marked`); rides the annotations payload through `export_takeoff`/`import_takeoff` and the app's own saves. One mark per shape. |
168
+ | `delete_verdict` | Lift an agent verdict mark by id. Agent marks only—the estimator's seal is human ink and is refused, the same line `edit_shape` holds on reviewed shapes. `undo_last` re-seats a lifted mark exactly where it was. |
169
+ | `read_sheet_text` | Positioned page text (image px), optionally restricted to a region—title blocks, room labels, finish schedules. |
170
+ | `find_text` | **Locate** a known string—the complement to `read_sheet_text` (which returns what a region *says*; this finds *where* a string sits). Case-insensitive substring match per pdf.js text run; each hit's center feeds straight into `one_click`'s seed. |
171
+ | `sheet_graph` | The plan-set INDEX (#87): every sheet's role with evidence, the schedule tables found, every room tag with its stacked name, the detail callouts, and every revision marker (text `Δ2`/`REV 2` tags AND drawn deltas—a bare digit inside a triangle of linework, proven from the sheet's vector geometry—in `revisions`)—how an agent decides WHAT to measure without a human enumerating rooms. |
172
+ | `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. A delta/REV marker on the answering row rides the result as `revisions`—the codes are the post-revision answer, and you're told the ink changed. |
173
+ | `find_schedule` | Locate a schedule table by kind ("room finish", "material")—sheet, title, headers, row count, a `view_sheet`-ready region, and `revised_rows` when delta/REV-marked rows exist. |
174
+ | `sheet_context` | The region's STRUCTURE in one frame: classified vector segments (endpoints as drawn, meta byte per segment), text spans with bboxes, and hatch-family instances with content-derived ids—same pattern spec ⇒ same id anywhere on the sheet, so plan↔legend matching is `id === id`. Decimation is declared and counted on every reply: `kept + dropped === total_in_region`, cap applies longest-first so walls survive. |
169
175
  | `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed. |
170
176
 
171
177
  ### The agent revises its own work
172
178
 
173
179
  `edit_shape` and `undo_last` exist because an agent that can only *append* has
174
- one recovery move: delete and re-derive. The loop they enable instead
175
- **commit → `view_sheet overlay:true` → see the ring overshot into the corridor
176
- → move those two vertices → look again** — is the loop a human estimator
180
+ one recovery move: delete and re-derive. The loop they enable instead—**commit
181
+ → `view_sheet overlay:true` → see the ring overshot into the corridor
182
+ → move those two vertices → look again**—is the loop a human estimator
177
183
  already runs, and it is the difference between an agent that drafts and one
178
184
  that works.
179
185
 
@@ -181,7 +187,7 @@ Two rules hold the surface honest:
181
187
 
182
188
  - **Ink is not pencil.** A shape carrying `origin.reviewed === true` is work a
183
189
  human affirmed, and no agent verb touches it. This server has no review gate
184
- of its own, so the guard is inert here it is the contract that makes the
190
+ of its own, so the guard is inert here—it is the contract that makes the
185
191
  surface safe to port to a host that *does* have one. The approval family
186
192
  holds the same line at the mark itself: `mark_verdict` can mint only the
187
193
  AGENT diamond (there is no actor input to misuse), and `delete_verdict`
@@ -193,27 +199,27 @@ Two rules hold the surface honest:
193
199
  measures whether the machine is getting better.
194
200
 
195
201
  Every JSON tool declares an **`outputSchema`**, and every reply carries the
196
- payload as **`structuredContent`** — typed, machine-validated on every call
197
- alongside the same compact JSON in a single text item for clients that predate
202
+ payload as **`structuredContent`**—typed, machine-validated on every call—alongside
203
+ the same compact JSON in a single text item for clients that predate
198
204
  structured output. `view_sheet` is the one image tool: its reply is a PNG
199
205
  content item plus a JSON meta text item (image replies aren't structured
200
206
  output, so it declares no schema by design). Failures come back as
201
- `isError: true` with `{"error": "..."}` — never a dropped connection.
207
+ `isError: true` with `{"error": "..."}`—never a dropped connection.
202
208
 
203
209
  ## Resources — browse before you measure
204
210
 
205
211
  Tools let an agent act; resources let it **see**. When a plan loads, the sheet
206
- set becomes browsable natively (`resources/list` re-announces itself via
212
+ set becomes browsable natively (`resources/list` re-announces itself through
207
213
  `list_changed`):
208
214
 
209
215
  | URI | Contents |
210
216
  |---|---|
211
- | `takeoff://sheets` | The plan index file, page count, every sheet's dims, title-block number, detected scale, scale state, shape count. Always listed; before any plan loads it says so and points at `load_plan`. |
217
+ | `takeoff://sheets` | The plan index—file, page count, every sheet's dims, title-block number, detected scale, scale state, shape count. Always listed; before any plan loads it says so and points at `load_plan`. |
212
218
  | `takeoff://sheet/{page}` | One sheet's metadata (JSON), addressed by 1-based page number. |
213
- | `takeoff://sheet/{page}/text` | The sheet's text, joined title block, room labels, schedules. Positions live in the `read_sheet_text` tool. |
214
- | `takeoff://sheet/{page}/image` | The page rendered to PNG, long edge capped at **1568 px** — the native resolution of vision-model eyes. Rendered lazily, cached until the next `load_plan`. |
219
+ | `takeoff://sheet/{page}/text` | The sheet's text, joined—title block, room labels, schedules. Positions live in the `read_sheet_text` tool. |
220
+ | `takeoff://sheet/{page}/image` | The page rendered to PNG, long edge capped at **1568 px**—the native resolution of vision-model eyes. Rendered lazily, cached until the next `load_plan`. |
215
221
 
216
- Page numbers not file-derived sheet keys address resources, so URIs stay
222
+ Page numbers—not file-derived sheet keys—address resources, so URIs stay
217
223
  clean regardless of the PDF's name; the human-facing key (`plan.pdf#2`) and
218
224
  title-block number (`A-101`) ride along as the resource name and title.
219
225
  Rendering uses `@napi-rs/canvas`, declared as this package's own optional
@@ -236,11 +242,11 @@ space, which makes them usable directly as click targets.
236
242
 
237
243
  ## Scale rules
238
244
 
239
- - A detected scale is a **suggestion** — it is never applied automatically.
245
+ - A detected scale is a **suggestion**—it is never applied automatically.
240
246
  Adopting it is always an explicit `set_scale { use_detected: true }`.
241
247
  - **Agent proposes, human confirms.** `set_scale` is the agent surface, so a
242
248
  scale set here lands **unconfirmed** (`confirmed: false` in the reply).
243
- Quantities still flow the gate is a flag, never a refusal but
249
+ Quantities still flow—the gate is a flag, never a refusal—but
244
250
  `takeoff_summary` names the affected sheets in
245
251
  `scale_unconfirmed`, and the export/report carry `scale_confirmed` so the
246
252
  canvas can ask the estimator to confirm (its scale menu grows a
@@ -250,7 +256,7 @@ space, which makes them usable directly as click targets.
250
256
  `Set the scale for <sheet> first — use set_scale (detected: <label>).`
251
257
  - `one_click` without a scale returns a **px-only preview**
252
258
  (`area_px2`, `perimeter_px`) with a warning, and commits nothing.
253
- - `upp` is real feet per image px at render scale 2.0, per sheet the same
259
+ - `upp` is real feet per image px at render scale 2.0, per sheet—the same
254
260
  number the app stores as `units_per_px`.
255
261
 
256
262
  ## A whole takeoff, end to end
@@ -281,21 +287,21 @@ what pricing consumes.
281
287
 
282
288
  ### Writing to disk
283
289
 
284
- `path` is not confined to a working directory, and deliberately so the marked
290
+ `path` is not confined to a working directory, and deliberately so—the marked
285
291
  set belongs in the job folder, wherever that is. What the export tools will not
286
292
  do is destroy a file they didn't write:
287
293
 
288
294
  - **Nothing at the path** → written.
289
- - **A previous export of ours at the path** → overwritten silently. Fix a
295
+ - **A previous export of OpenTakeoff's own at the path** → overwritten silently. Fix a
290
296
  condition and export again to the same path as often as you like; that's the
291
297
  normal loop, and it needs no flag.
292
298
  - **Any other existing file** → refused, with the path named. Pass
293
299
  `overwrite: true` to replace it anyway.
294
- - **Corrupt, encrypted, or unreadable** → treated as *not* ours, so refused. An
300
+ - **Corrupt, encrypted, or unreadable** → treated as *not* OpenTakeoff's, so refused. An
295
301
  unrecognizable file is exactly the kind worth not overwriting.
296
302
 
297
303
  A marked set is recognized by its PDF `Producer`; the JSON exports by the
298
- `schema` key they stamp. This is data-loss protection, not a sandbox the
304
+ `schema` key they stamp. This is data-loss protection, not a sandbox—the
299
305
  server runs as you, with your privileges. See [`SECURITY.md`](../SECURITY.md)
300
306
  for the threat model.
301
307
 
@@ -307,24 +313,24 @@ sheet number (`A-101`) wherever a sheet is named.
307
313
 
308
314
  - **Scanned sheets flood, but don't index.** `one_click` and `detect_rooms`
309
315
  fall back to the sheet's rendered pixels where vectors can't bound the room
310
- (#154) disclosed as `raster_traced` — but a scan with no text layer still
316
+ (#154)—disclosed as `raster_traced`—but a scan with no text layer still
311
317
  has nothing for `detect_rooms`/`sheet_graph`/`resolve_tag` to read: seeds
312
318
  come from you (`view_sheet`, then `one_click`). The raster path needs the
313
319
  same optional `@napi-rs/canvas` as `view_sheet`.
314
- - **Stitching is human-only deliberately, not a gap.** The canvas can join
320
+ - **Stitching is human-only—deliberately, not a gap.** The canvas can join
315
321
  2–4 sheets split at a match line into one composite surface (#200), but no
316
322
  MCP verb creates, aligns, or addresses a stitch. Joining the match line
317
- means clicking the same drawn wall junction on both halves a judgment
323
+ means clicking the same drawn wall junction on both halves—a judgment
318
324
  call with a worse blast radius than a bad scale, because a sloppy align
319
325
  silently skews every quantity that crosses the seam. That stays behind
320
326
  human eyes; it is not staged for later exposure. For a split floor: a
321
327
  human stitches and aligns in the canvas, and over MCP you work each member
322
- sheet as its own surface a seam-crossing room belongs to the canvas.
328
+ sheet as its own surface—a seam-crossing room belongs to the canvas.
323
329
  This server also doesn't read the app's additive `stitches` payload field,
324
330
  so a stitched takeoff round-tripped through `import_takeoff` →
325
- `export_takeoff` comes back without its stitches when a stitch is in
331
+ `export_takeoff` comes back without its stitches—when a stitch is in
326
332
  play, the app's own save is the one to keep.
327
- - `load_plan` replaces the session by default; `merge: true` builds a multi-document working set (#152). Reloading a merged file is refused reload = replace, deliberately.
333
+ - `load_plan` replaces the session by default; `merge: true` builds a multi-document working set (#152). Reloading a merged file is refused—reload = replace, deliberately.
328
334
  - The takeoff lives in memory. `export_takeoff` (the app's exact save payload,
329
335
  nothing lost in translation) and `export_marked_pdf` (the reviewable marked
330
336
  planset) are the ways out.
@@ -339,14 +345,14 @@ npm test # session + tool-layer + e2e, against demo/sample-plan.pdf
339
345
  ## Releasing (maintainers)
340
346
 
341
347
  MCP releases live in the **`mcp-v*`** tag namespace — bare `v*` tags belong to
342
- the app (v0.2.0, v0.3.0 are app releases). Releases publish via **npm trusted
348
+ the app (v0.2.0, v0.3.0 are app releases). Releases publish through **npm trusted
343
349
  publishing**: the tag push fires `.github/workflows/publish-mcp.yml`, which
344
- runs straight through no approval click and publishes the npm artifact
345
- over OIDC with a **provenance attestation** (no npm token exists anywhere
346
- the npm package designates that exact repo + workflow as its trusted
350
+ runs straight through—no approval click—and publishes the npm artifact
351
+ over OIDC with a **provenance attestation** (no npm token exists anywhere—the
352
+ npm package designates that exact repo + workflow as its trusted
347
353
  publisher), followed by the MCP registry entry, the GitHub release, and the
348
354
  MCPB bundle. The `release` environment's required-reviewer gate existed
349
- briefly and was deliberately removed (2026-07-22) the tag push is the one
355
+ briefly and was deliberately removed (2026-07-22)—the tag push is the one
350
356
  human decision, and it's already admin-gated, so a second click added
351
357
  friction without adding safety.
352
358
 
@@ -358,8 +364,8 @@ git tag mcp-v<version> && git push origin mcp-v<version>
358
364
  ```
359
365
 
360
366
  ⚠️ Because there's no approval step, an accidental or mistyped `mcp-v*` tag
361
- publishes to npm immediately, and npm unpublish is heavily restricted
362
- double-check the version before tagging.
367
+ publishes to npm immediately, and npm unpublish is heavily restricted—double-check
368
+ the version before tagging.
363
369
 
364
370
  The workflow checks version consistency, runs the full publish gate
365
371
  (`prepublishOnly` = typecheck + tests + build), publishes to npm and the
@@ -370,12 +376,12 @@ retry.
370
376
 
371
377
  ### Refreshing the Smithery listing
372
378
 
373
- Smithery isn't part of the automated release above it needs a **separate,
379
+ Smithery isn't part of the automated release above—it needs a **separate,
374
380
  manual** publish after any tool signature change, because of a genuine spec
375
381
  conflict between two validators: the official MCPB validator (what
376
382
  `npm run mcpb` gates on) rejects a `tools[].inputSchema` key outright, while
377
383
  Smithery's registry rejects a bundle *without* real `inputSchema` per tool
378
- (smithery-ai/cli#770, #797, #787 no manifest satisfies both). The canonical
384
+ (smithery-ai/cli#770, #797, #787—no manifest satisfies both). The canonical
379
385
  `dist-mcpb/opentakeoff-mcp.mcpb` stays spec-compliant for Claude Desktop / the
380
386
  official registry / Glama; `scripts/build-smithery-mcpb.mjs` builds a
381
387
  Smithery-only bundle instead, with live-introspected tools + inputSchema baked