Skip to content
verify mcp Beta VerifyMCP is currently in beta. If you notice any issues, get in touch and we’ll put it right.

OpenTakeoff

NPM · OPENTAKEOFF-MCP

Construction takeoff for AI agents: load plans, set scale, measure, count, export with provenance.

Available components

0 this week 94 Trust /100

Last scanned (2 hours ago)

Scores higher than 97% of npm servers

Trust breakdown (7 categories)

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score → Why this is hard to score →

Supply Chain Security99
  • No malware found by supply-chain analysis.Pass
  • No known CVEs affecting this package version or its production dependencies.Pass
  • No install/post-install scripts declared.Pass
  • 37 of 142 dependencies flagged as unhealthy. View diagnostics → Partial
Provenance & Transparency100
  • Source repository is publicly reachable at the declared URL. View diagnostics → Pass
  • Cryptographically verified build provenance (signed, bound to Kentucky-ai/opentakeoff). View diagnostics → Pass
  • Clear OSI-approved license (AGPL-3.0-only).Pass
  • Actively maintained (last published 10 days ago).Pass
  • Publishes a security disclosure policy (SECURITY.md).Pass
Schema Quality & AI Usability77
  • 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).Pass
  • AI-judged instruction clarity (excellent).Pass
  • Context-footprint check failed: tool/resource definitions use about 21951 tokens (~274/item across 80 items; 58 tools + 22 resources), over budget; trim descriptions and params. See how to fix → Fail
  • Usage-examples check failed: none of the tools include examples. See how to fix → Fail
Stability & Change Management100
  • No destabilizing schema changes in the last 30 days.Pass
Tool Coverage96
  • 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
  • 86% of tool parameters carry a description.Partial
  • Structured output schemas are declared (98% of tools); any adoption earns full credit.Pass
Tool Safety75
  • No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.Pass
  • 0 of 6 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "delete_shape" implies "delete" and declares no destructiveHint at all, which the MCP spec reads as destructive by default. See how to fix → Fail
  • An AI judge read all 60 captured unit(s) of tool text and found none that tries to manipulate the model reading it.Pass
Capabilities100
  • Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.Pass
Changelog

Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.

  • 25 Oct 26 +21
    • Stability: pass → unverified ▼ security
    • Tool safety: pass → unverified ▼ security
    • CVE-2023-54404 no longer affects this package ▲ security
    • Malware scan: unverified → pass ▲ security
    • Known CVEs: fail → pass ▲ security
    • Capabilities: pass → unverified ▼ functional
    • Tool coverage: 100 → unverified ▼ functional
    • Schema quality: 100 → unverified ▼ functional
    • Package version: 0.9.99 → 0.9.100 functional
  • 24 Oct 26 −21
    • CVE-2023-54404 affects this package: high ▼ security
    • Malware scan: pass → unverified ▼ security
    • Known CVEs: pass → fail ▼ security
    • Package version: 0.9.98 → 0.9.99 functional
  • 20 Oct 26 +1
    • Stability: 0.97 → pass security
  • 19 Oct 26 −1
    • Stability: pass → 0.97 functional
  • 17 Oct 26 +2
    • Stability: 0.87 → pass security
  • 16 Oct 26 +1

    No change was recorded against any check on this day. Stability & Change Management went from 83 to 87. That category is still filling its 30-day observation window: 25 days of observed history at the previous scan, 26 at this one. The score rises as the window fills, whether or not the server changes.

  • 14 Oct 26 +41
    • Injection markers: unverified → pass ▲ security
    • Tool coverage: unverified → 100 ▲ functional
    • Schema quality: unverified → 100 ▲ functional
    • MCP protocol: unverified → pass ▲ functional
    • Stability: unverified → 0.80 ▲ functional
    • Package version: 0.9.96 → 0.9.98 functional
  • 12 Oct 26 −44
    • Stability: pass → unverified ▼ security
    • Tool safety: pass → unverified ▼ security
    • Capabilities: pass → unverified ▼ functional
    • Tool coverage: 100 → unverified ▼ functional
    • Schema quality: 100 → unverified ▼ functional
    • Licence: Apache-2.0 → AGPL-3.0-only functional
    • Package version: 0.9.96 → 0.9.98 functional
Diagnostics

Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.

Captured 5 Nov 2026 · Analysed npm/opentakeoff-mcp@0.9.100

Provenance Verified

A signed build attestation was found and verified, binding this exact artifact to the source repository it claims to come from.

Result Verified
Ecosystem npm
Reason Verified
Discovered via Registry attestation endpoint
Source repo Kentucky-ai/opentakeoff
Certificate issuer https://token.actions.githubusercontent.com
Certificate SAN https://github.com/Kentucky-ai/opentakeoff/.github/workflows/publish-mcp.yml@refs/tags/mcp-v0.9.100
Rekor log index 3459408250
Predicate type SLSA build provenance https://slsa.dev/provenance/v1
Subject digest sha512:24036b597f3070763fbcec691711952d0768195d561196f3cfecac1e025e3ef7f2cea4c7cf531941f76a5d0b1db4cd6cd3d8ee4f12991bd470849287c

Background: How many MCP packages publish verified provenance →

Dependencies 142 packages
Packages resolved 142
Stale 37
Tree resolution Complete

Background: SBOMs and build attestations, explained →

MCP tools · 58 exposed · ~20,339 tokens

The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability. A tool's description is untrusted text the model reads on every call, which is what makes this list a security surface and not just an inventory: how tool poisoning works →

Tool Tokens
annotate ~730

Place an annotation on a sheet — a note ABOUT the work, never a measurement of it. Types: cloud and highlight take rect:[[x0,y0],[x1,y1]] (a revision cloud around an area, a highlight box over it), text takes at:[x,y], callout takes at:[x,y] plus target:[x,y] (the point its leader aims at), arrow takes from:[x,y] and to:[x,y] (tail and head — plank/seam direction, the markup flooring drawings use most; #150), bubble takes at:[x,y] plus optional r (a keynote/detail circle carrying centered text), dimension takes from:[x,y] and to:[x,y] (its two measured endpoints) and labels itself with the length between them at the sheet's scale — drawn as a dimension line with end ticks and the measurement centered. A dimension states a REAL length, so it is the one annotation the scale gate applies to: on an unscaled sheet it refuses exactly like the measure tools (set_scale first) rather than dressing a px figure up as feet. It still touches no quantity — a dimension is a note about a distance, not a takeoff line item. Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself. Write {{qty}} in the text to quote the linked condition's live measured quantity (multiplier applied, no waste): "x{{qty}} fixtures" reads "x6 EA fixtures" on the sheet and follows every later count; the stored text keeps the field, and list_annotations reports text_resolved. No review gate: the pencil-not-ink rule exists to stop an agent inventing geometry, and a cloud reading "verify substrate" is not geometry. It touches no quantity. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet paylo…

NameTypeReqDescription
atarray–Anchor point (image px) — text, callout, and bubble (the circle's center)
conditionstring–Finish tag to attach this note to, e.g. 'CPT-1' (minted on first use). Omit for an unattached sheet note
fromarray–Arrow tail / dimension start (image px)
rnumber–Bubble radius (image px); omitted → the canvas default (2% of sheet width)
rectarray–Corners (image px) — cloud and highlight
sheetstringyesSheet name or number, as sheet_info reports it
targetarray–What a callout's leader line points at (image px)
textstring–The note. A cloud with no text still reads as 'look here'; a bubble's text draws centered in the circle; a dimension appends it after the measured length
toarray–Arrow head / dimension end (image px)
typestringyescloud/highlight need rect; text/callout/bubble need at; callout also needs target; arrow and dimension need from + to
NameTypeReqDescription
conditionstringyes–
condition_idstringyes–
idstringyes–
length_lfnumber–Dimension only: the measured length (real feet) the annotation will label itself with
notestringyes–
sheetstringyes–
textstringyes–
typestringyes–

No examples provided.

apply_rules ~290

Re-run the correction rules the takeoff arrived with (#207) — the lessons an estimator TAUGHT the canvas (#88): "every room like this loses the mechanical chase." A rule is a deterministic predicate (enclosed linework islands under a size cap, inside the rule's condition's rooms), never a re-prompt. Evaluation is the same pure rules.ts engine the canvas Preview runs; the commit is the one batch the canvas's Apply makes — ONE journal entry, undo_last takes the whole batch back. Everything lands reviewed: false (this server has no review gate), and the reply's per-rule disclosure — what each rule produced, what was skipped, with ids — IS your preview: read it, then view_sheet overlay:true. Idempotent by construction: any candidate an existing deduct already covers is dropped by the engine, so re-running after new rooms commit is the intended workflow and never double-deducts. Rules arrive ONLY via import_takeoff (minting a new rule is an estimator's correction and stays behind the canvas's human Preview→Apply gate); with none imported this refuses. Pass sheet to scan one sheet; omit it to scan every sheet holding the rules' rooms. Uncalibrated and scanned-raster sheets come back in skipped_sheets, named.

NameTypeReqDescription
sheetstring–Scan only this sheet (default: every sheet holding the rules' conditions' rooms)
NameTypeReqDescription
committedintegeryesDeducts committed across all rules — 0 is a result (idempotence)
notestringyes–
rulesarrayyes–
skipped_rulesarrayyesRules not evaluated, named — never silently dropped
skipped_sheetsarrayyesSheets that could not be scanned (uncalibrated, or scanned raster with no linework mask)
total_deduct_sfnumberyes–

No examples provided.

count_marks ~547

The COUNT TAKEOFF in one deterministic call — no seeds, no model, seconds: census every VALUE-ANNOTATED mark tag on the plan-role sheets, counted per schedule mark, committed as EA markers when asked. The identity rule is the annotated-device drafting pattern: a device is drawn as its mark tag with a value under it ("S1" over "200" — CFM on air devices, GPM on fixtures, a rating on equipment), so a tag WITH a paired value counts, a tag inside a schedule table's own region is a row label (excluded, tallied), and every other occurrence is WITHHELD with a reason and coordinates — a tag amid linework but unvalued may be a real device (view_sheet it), a bare tag is probably a note mention. Marks default to the set's schedule row keys (a compound row "R1 / E1" answers for R1 AND E1; each mark cites its row), or state them: {marks: ["S1","R1"]}. The complement to sweep_schedule_row: THAT tool is for marks drawn ON their marker with no value (finish tags in bubbles) and matches geometry; this one is for annotated devices and needs no fingerprint at all. Refusal-honest: scans refuse (no text layer), a set with no mark-shaped rows refuses unless marks are stated, non-plan sheets are skipped with the role that excused them. commit: true commits every counted occurrence under its mark's own tag — ONE undo step for the whole census, schedule citation on origin. Counts are scale-free (EA) — no set_scale needed. Then AUDIT: view_sheet {overlay: true} where the markers landed, and read every withheld entry — a withheld item you ignore is a hole in the bid. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt. EQUIPMENT marks (a row in an equipment/device schedule — fans, pumps, heaters, fixtures, panels) follow the leader-tag convention instead: a scheduled mark drawn amid linework with no value under it is counted BY LABEL (occurrence by: "label", counted_by_label on the…

NameTypeReqDescription
commitboolean–Commit every counted occurrence as one EA count marker under its mark (withheld/excluded never commit)
marksarray–The marks to census, e.g. ["S1", "R1"] — omit to take them from the schedule tables' row keys
NameTypeReqDescription
completebooleanyes–
excluded_in_tablesinteger–Tag occurrences inside a schedule table's own region — row labels, never instances
marksarrayyes–
per_sheetarrayyes–
skippedarrayyes–
totalintegeryesAll counted instances across every mark

No examples provided.

create_rfi ~413

Raise an RFI — a Request For Information — when the drawing set contradicts itself or cannot answer a question you need answered to take the work off: a room-finish schedule row that names a tag the plan never draws, a room label the schedule has no row for, a finish called out two ways, a scale that disagrees with a stated dimension. It lands in the estimator's RFI register (the canvas's RFI panel) with the next number in that register's own sequence (RFI-001, RFI-002, …), status open, dated today, on the sheet you name. You raise it as the agent: the record carries origin {actor: "agent", reviewed: false} and is PENDING — pencil — until the estimator accepts it in the register, because an RFI goes to the architect and nothing sends without a human. It still prints in the marked set's RFI schedule like any other RFI, so the question is on the deliverable. Pass markup_ids to pin it to annotations already on the sheet (annotate a cloud or callout at the conflict first, then link it here) — a linked markup carries the RFI number on the canvas and in the marked set, and list_rfis reports which finish tags the question touches through those links. Prefer this to describing the conflict in prose: a question in the register is tracked, numbered, and answered; a sentence in a reply is lost. Journaled; undo_last takes it back.

NameTypeReqDescription
markup_idsarray–Annotation ids (annotate / list_annotations) to link — they carry this RFI's number on the sheet
questionstringyesWhat you are asking the architect to answer, stated so a reply can settle it
sheetstringyesSheet name or number the question is about, as sheet_info reports it
titlestringyesThe one-line subject the register and the RFI schedule print — what the question is about
NameTypeReqDescription
actorstringyesWho raised it — "agent" for every RFI minted over MCP, "estimator" for a panel-raised one
conditionsarrayyesFinish tags the linked markups are attached to — the scopes this question touches
datestringyesYYYY-MM-DD opened
idstringyesThe record id ("rfi-…")
linked_markupsarrayyesAnnotation ids carrying this RFI's number on the sheet (markup.rfi_id) — derived, never stored twice
notestringyes–
numberstringyesThe register number, "RFI-001" — next in the panel's own sequence, never reissued
pendingbooleanyestrue = agent-raised and not yet accepted by an estimator in the register (origin.reviewed false) — pencil, not sent
questionstringyes–
responsestringyes–
response_datestringyesYYYY-MM-DD answered, '' while open
sheetstringyesThe sheet the question is about
statusstringyesThe panel's lifecycle: open → answered → closed; void = withdrawn
subjectstringyes–

No examples provided.

cut_out ~569

Cut a REAL hole in a committed floor_area shape (#206) — the way the canvas cuts one (#137): the same lib/cutout.js boolean subtract, so the two surfaces can never disagree about what a hole holds. The parent keeps its outer ring plus the reconciled hole(s) (verts_norm_holes), its computed nets for real — N cuts compose, overlap between cuts never double-deducts (set subtraction), a hole ADDS perimeter — and the deduct commits carrying cuts_shape_id so the report and legend read the reconciled number, never a second arithmetic pass. This is the verb for a column, a floor drain, an island of casework INSIDE a room; an independent measure_polygon role:"deduct" stays the tool for a deduction that isn't a hole in one parent. Refusal over guessing: the ring must sit FULLY inside the parent's outer ring (an edge-crossing cut is a boundary correction — edit_shape the parent instead), and a cut that would erase the parent or split it in two refuses whole (trace the pieces as rooms). One journal entry — undo_last restores parent and hole together; delete_shape on the deduct later reverts the cut too (a multi-cut parent rebuilds from the chain's pristine snapshot minus the survivors). AN OPEN RUN IS CLIPPED, NOT SUBTRACTED: wall tile (surface_area) and base/transitions (linear) are polylines traced in plan, so the ring removes the stretch it covers, the run keeps its id and takes what survives, and a cut through the MIDDLE leaves the far side as its own shape (same condition, same height) — quantities ride the surviving length, which is exact, since wall SF is LF × height and a border's SF is LF × thickness. No deduct is minted for a run: there is no area for one to sit on, and a deduct's SF counts against the FLOOR total a run never fills. A ring that misses the run, one that swallows it whole (delete_shape it), and a curved run (its verts are control points) all refuse. A derived base with numeric openings also refuses: those deductions have no stored location; use measure…

NameTypeReqDescription
parent_shape_idstringyesA committed floor_area shape id, or an open run (surface_area / linear) to clip (list_shapes)
vertsarrayyesThe ring, image px — fully inside the parent for an area; over the stretch to remove for a run
NameTypeReqDescription
deduct_shape_idstring–Area parent: the reconciled deduct — carries cuts_shape_id; totals skip it (the parent nets the hole)
hole_sfnumber–Area parent: what this cut actually removed from the parent's net — 0 when the ring fell entirely inside an existing hole
holesinteger–Area parent: holes the parent now carries
measure_rolestring–Run parent: surface_area or linear
notestringyes–
parent_netobject––
parent_shape_idstring––
piecesarray–Run parent: every stretch that survived the cut — more than one when the ring fell in the middle
removed_lfnumber–Run parent: length the cut took out
removed_sfnumber–Run parent: the SF that rode on that length
shape_idstring–Run parent: the run that was clipped — it keeps its id and takes the first surviving stretch

No examples provided.

delete_rfi ~147

Withdraw an RFI. A TOMBSTONE, never a renumber: the record stays with its number reserved, so the register and the marked set keep printing a gap where it was and the next RFI takes the next number — an RFI number that went out and then meant something else would be a lie. Every markup linked to it keeps its note and loses the link (the canvas's own delete rule). Withdraw a question you raised in error; a question the architect answered is closed in the register, not deleted. Journaled; undo_last puts the record and its links back.

NameTypeReqDescription
rfi_idstringyesRecord id from create_rfi or list_rfis
NameTypeReqDescription
deletedstringyesThe withdrawn record's id
notestringyes–
numberstringyesIts number — stays reserved; the register and the marked set keep the gap
rfis_remainingintegeryesLive RFIs after the withdrawal
unlinked_markupsintegeryesMarkups that kept their note and lost the link

No examples provided.

delete_shape ~72

Remove a committed shape by the id returned when it was committed. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
shape_idstringyes–
NameTypeReqDescription
deletedstringyesThe removed shape's id
notestring–Cutout interplay (#206), when it applies: the parent's cut was reverted, could not be rebuilt, or reconciled deducts were orphaned by a parent delete
shape_countintegeryesCommitted shapes remaining

No examples provided.

delete_verdict ~96

Lift an agent verdict mark by id (mark_verdict's reply, or list_annotations verdicts[]). Agent marks only: the estimator's APPROVED seal is human ink and is refused — the same line edit_shape holds on reviewed shapes. Journaled like every mutation, so undo_last re-seats a lifted mark exactly where it was.

NameTypeReqDescription
verdict_idstringyesRecord id from mark_verdict or list_annotations verdicts[]
NameTypeReqDescription
deletedstringyesThe lifted record's id
verdicts_remainingintegeryesApproval-family records still on the takeoff (both actors)

No examples provided.

derive_base ~310

Mint the wall base from committed rooms (#148) — the estimator's most mechanical derivation: base LF = room perimeter − stated door openings. For every floor_area shape of source_condition, commits ONE linear shape under condition (e.g. 'RB-1') tracing that room's boundary, quantified NET of the openings you state per room. The openings are YOUR claim to make — look at the doors with view_sheet, state {shape_id, lf} per room (repeat a shape_id to stack openings); the tool never guesses, and your claim is recorded on origin.derived (from_shape_id, gross_lf, openings_lf). The output geometry remains the whole perimeter: deducted openings are numerical, not visible gaps. For a drawing of the actual installed base, use measure_line on the physical runs after checking door jambs, alcoves and open finish splits. All-or-nothing: an unknown shape_id, a negative lf, or openings meeting a room's whole perimeter refuses the call before anything commits. The whole derivation is ONE undo step. Deriving onto the source condition is refused — base lands on its own tag.

NameTypeReqDescription
conditionstringyesFinish tag the base commits under (minted on first use), e.g. 'RB-1'
openingsarray–Stated openings per room — omit for gross perimeters
source_conditionstringyesFinish tag whose floor_area rooms the base derives from, e.g. 'CPT-1'
NameTypeReqDescription
committedintegeryes–
conditionstringyesThe tag the base committed under
notestringyes–
roomsarrayyes–
source_conditionstringyes–
total_lfnumberyesSum of net_lf across rooms

No examples provided.

derive_transitions ~775

Mint the transition where two finishes MEET (#202) — the derivation that follows derive_base, and the line an estimator draws by hand on every job. Pass the two finish tags and the tag the transition commits under (e.g. condition_a 'CPT-1', condition_b 'PT-1', condition 'T-1'), and every committed room of each is compared against every committed room of the other. WHAT THE GEOMETRY ACTUALLY IS, because it decides what you get back: flood-traced rooms DO NOT SHARE EDGES. A trace fills to the wall linework, so two rooms across a partition are separated by four to eight inches of nothing — testing for a shared edge finds zero transitions on a real planset. What is there is proximity, in two flavours that mean completely different things: • BUTT JOINT — the two rings run together inside ONE open space (a lobby that changes from carpet to tile with no wall between). The transition IS that run, and it commits as a linear shape under your tag, origin.derived naming both parent shapes and the measured gap. • WALL-SEPARATED — the rings run parallel across a partition. The rooms are adjacent, but the transition is NOT the shared wall: it is a threshold, in the doorway, and NOTHING in the trace record says where the doorway is (the flood engine seals openings and reports how MUCH boundary it synthesised, never where). Committing 34 LF of threshold because two rooms share 34 LF of wall would be a wrong bid with a machine's confidence behind it. These come back in `withheld` — measured, with their length, their gap in inches, and an `at` point — as questions you answer by LOOKING (view_sheet at `at`, then measure_line or place_count the threshold yourself). The symbol_sweep doctrine: a near-match is never a silent commit and never a silent drop. Tuning: max_gap_in (default 12) is how far apart two rings can be and still count as adjacent at all — raise it for thick walls, and every extra inch turns more of the plan into wall_separated questions, never into committed LF. min…

NameTypeReqDescription
conditionstringyesFinish tag the transitions commit under (minted on first use), e.g. 'T-1'. Must differ from both sources
condition_astringyesFirst finish tag, e.g. 'CPT-1' — its committed rooms are walked, and runs are traced along their boundaries
condition_bstringyesSecond finish tag, e.g. 'PT-1'
max_gap_innumber–How far apart two rings can be and still count as adjacent, in inches (default 12 — a thick partition). Wider only produces more wall_separated QUESTIONS, never more committed LF
min_run_innumber–Shortest run worth reporting, in inches (default 12) — below this is a corner where two rooms clip, not a transition
NameTypeReqDescription
betweenarrayyesThe two finish tags
committedintegeryes–
conditionstringyesThe tag the transitions committed under
notestringyes–
runsarrayyes–
total_lfnumberyesSum of committed run lengths — butt joints only
withheldarrayyesAdjacency across a wall: real, measured, and NOT committed — the transition there is a threshold at a doorway this cannot locate
withheld_lfnumberyesShared-wall length held back — never part of total_lf

No examples provided.

duplicate_condition ~299

Twin a condition — the same finish measured somewhere else, with its own supporting materials. One finish in two areas is not two conditions and it is not one either: the same sheet goods over a slab and over a raised deck take the same field material and different preparation underneath (one wants a moisture barrier, the other a primer and a different adhesive). The twin arrives carrying the original's whole materials list and keeps FOLLOWING it — change a coverage rate on the original and every twin that has not 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'), because every tool in this server resolves a condition by finish tag and takes the FIRST match: two conditions sharing a tag would make one permanently unreachable, and a takeoff re-import collapses them last-wins. A label already in use is refused rather than de-collided. No takeoffs come along — measure the new area against the returned condition_id. Reversible with undo_last; use split_condition to end the inheritance permanently.

NameTypeReqDescription
conditionstringyesFinish tag of the condition to twin, e.g. 'CPT-1'
labelstringyesWhat makes this one different, usually the area: 'Level 2', 'Building B', 'Phase 2'
NameTypeReqDescription
conditionstringyesThe twin's finish tag — base tag + the label, e.g. 'CPT-1 – Level 2'
condition_idstringyesThe TWIN — measure the new area against this
family_idstringyesShared by every variant of this finish — survives a split
inherited_rowsintegeryesMaterial rows copied, all still following the original
notestringyes–
variant_labelstringyes–
variant_ofstringyesThe condition whose material rows this one follows

No examples provided.

edit_annotation ~135

Shorten, replace or clear the text of an existing annotation. Get annotation_id from list_annotations (annotations, not verdicts). Changes only text: position, shape, dimension length, condition links, quantities and review records stay unchanged. Empty text clears the note; a dimension still prints its measured length. Refuses an RFI-linked note: review that question's context in the browser RFI register. One undo_last step restores the previous text. Does not create a verdict or human approval.

NameTypeReqDescription
annotation_idstringyesAn annotation id from list_annotations
textstringyesReplacement text; empty string clears it
NameTypeReqDescription
idstringyes–
notestringyes–
textstringyes–

No examples provided.

edit_condition ~556

Set a condition's quantity knobs — waste %, multiplier, height_ft (the H knob measure_surface quantifies against), and/or roll_setup (the roll-goods opt-in: seams and order footage figured from the committed rooms, #147). takeoff_summary emits waste-adjusted *_net order quantities and a per-condition multiplier, and every export carries both, but conditions minted through the measure tools start at waste 0 / multiplier 1 — without this tool an agent's takeoff always ships net === gross (#131). waste_pct is the estimator's cut-waste percentage (carpet commonly 5–10); multiplier scales every quantity on the condition (×N identical floors — takeoff_summary applies it before waste). condition must resolve to an EXISTING finish tag — a typo'd tag errors rather than minting an empty condition (the edit_materials remove/patch rule, not its add rule: these knobs mean nothing on a condition that doesn't exist yet). No review gate — quantity config, not traced geometry; undo_last reverses a call in one step (both knobs snapshotted together, restored verbatim).

NameTypeReqDescription
conditionstringyesFinish tag of an existing condition, e.g. 'CPT-1'
drop_ftnumber–Drop and Rise (#441): the vertical leg DOWN, in feet, every linear run of this condition adds to its plan length. Re-flows existing runs that do not carry their own drop_ft. 0 turns it off
height_ftnumber–Wall height in feet — the canvas's H knob; measure_surface quantifies traced LF × this
multipliernumber–Quantity multiplier (×N identical areas). Note: the canvas treats 0 as 1, so 0 is rejected here rather than silently meaning 'off'
rise_ftnumber–Drop and Rise (#441): the vertical leg UP, in feet, every linear run of this condition adds to its plan length (LF = plan + rise + drop). Re-flows existing runs that do not carry their own rise_ft; d…
roll_setup––Roll-goods opt-in (#147): presence of a setup is what makes the condition roll goods — seams figured, cuts packed, order footage beside the measured quantities. Same-material partial edits patch the…
waste_pctnumber–Waste percentage applied to net order quantities, e.g. 10 for 10%
NameTypeReqDescription
conditionstringyesThe finish tag passed in
condition_idstringyes–
drop_ftnumber–The condition's default drop for its linear runs after this write — present once set (#441)
height_ftnumber–The condition's wall height after this write — present once set (measure_surface multiplies traced LF by it)
multipliernumberyesThe condition's quantity multiplier after this write
rise_ftnumber–The condition's default rise for its linear runs after this write — present once set (#441)
rollobject–The figured order (same row export_report's roll_goods carries) — present when the roll-goods condition has floor shapes on scaled sheets
roll_setupobject–The condition's roll-goods setup after this write — present while opted in
waste_pctnumberyesThe condition's waste % after this write

No examples provided.

edit_materials ~461

Add, remove, or patch supporting-materials rows on a condition — the coverage-rate lines that turn a measured area/length/count into an order quantity (adhesive at N sf/gal, grout at N lf/bag, …), matching the canvas's per-condition Supporting Materials panel. Each row is {name, per, basis, unit, round, note}: quantity = the condition's basis total (area/linear/count/seam_lf) ÷ per, rounded up to whole purchase units unless round:false. basis "seam_lf" is the one basis that is FIGURED rather than measured: it is the length where two cuts meet on the floor, read off the condition's roll layout (set roll_setup with edit_condition), which is what a heat-weld rod or a carpet seam tape is bought by. A 20-ft-wide room off a 12-ft roll seams once down its length; the same square footage as two 10-ft rooms seams not at all, and no percentage of the area or the perimeter can tell those two jobs apart. Without a roll_setup — or with no committed floor shapes to lay out — a seam_lf row reads 0, which is the honest state rather than a guess. condition names an existing OR NEW finish tag (minted on first touch, same as measure_polygon) — add alone is enough to seed materials on a condition before you've traced anything. remove/patch target existing row ids from this reply or export_takeoff (takeoff_summary strips materials for a compact quantities-only reply); a bad id 404s the WHOLE call before anything is written, and referencing an id on a tag with no condition yet errors rather than silently minting an empty one. No review gate here — materials rows are quantity config, not traced geometry, so this edits directly; undo_last reverses a call in one step (the condition's whole materials array, snapshotted before the write, restored verbatim).

NameTypeReqDescription
addarray–New rows to add
conditionstringyesFinish tag, e.g. 'CPT-1'
patcharray–Field changes on existing rows
removearray–Existing row ids to remove
NameTypeReqDescription
changedobjectyes–
conditionstringyesThe finish tag passed in
condition_idstringyes–
materialsarrayyesThe condition's full materials array after this write

No examples provided.

edit_shape ~510

REVISE a shape you already committed, instead of deleting it and starting over: pass new verts to move the geometry, condition to reassign it to a different finish tag, role to switch between floor_area / deduct / linear, label to name the room it belongs to, or any combination. Quantities are recomputed from the result — a role flip alone re-measures (closed area vs open length). The loop this is for: measure_polygon to commit, view_sheet with overlay:true to LOOK at what landed, then edit_shape to fix the two vertices that overshot into the corridor. label is the per-room reporting seam: this is how a shape traced by hand — or one whose room number the sweep read wrong — joins the same per-room breakdown the Report and the workbook's floor × room tab group by. Shapes a human affirmed (origin.reviewed) are ink and are refused — an agent revises its own pencil and nothing else. Agent self-revision is tallied on origin.agent_edits, kept deliberately separate from the human-correction fields. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstring–Reassign to this finish tag (minted on first use)
drop_ft––Linear runs only (#441): this run's vertical leg DOWN in feet, overriding the condition's drop_ft default (0 = none). null clears the override so the condition's default applies again
labelstring–The room (or phase/area) this shape belongs to, e.g. "134" or "OFFICE 101" — what per-room reporting groups by. Pass "" to clear it
rise_ft––Linear runs only (#441): this run's vertical leg UP in feet, overriding the condition's rise_ft default (0 = none). null clears the override so the condition's default applies again. perimeter_lf is…
rolestring–Switch what the shape measures — flipping INTO surface_area needs a height on the shape or its condition
shape_idstringyesId returned when the shape was committed
vertsarray–Replacement geometry (image px): ≥3 vertices for an area shape, ≥2 points for a linear/surface run, ≥1 for a count marker
NameTypeReqDescription
agent_editsintegeryesHow many times the agent has revised this shape — separate from the human-correction tally
area_sfnumber–0 for linear shapes; LF × height for surface_area; absent for count
changedarrayyesWhich fields this call actually changed
countnumber–count shapes only — the marker's EA (preserved across the edit)
labelstring–The shape's room/phase label after this call — absent when it carries none (a cleared label reports as absent, not as an empty string)
measure_rolestringyes–
nvertsintegeryes–
perimeter_lfnumber–Length for linear/surface runs (a linear run's TOTAL incl. rise + drop), perimeter for closed ones; absent for count
plan_lfnumber–Linear runs with a vertical leg: the flat trace alone (#441)
shape_idstringyes–
vertical_lfnumber–Linear runs with a vertical leg: rise + drop (#441)

No examples provided.

estimate_summary ~117

The priced estimate — the canvas Estimate panel as data: one line per measured condition (unit, order and measured quantities, rates, material / labor / supply cost, status priced | partial | unpriced with what is missing), then material, labor, sales tax, cost, overhead, profit and the base bid. complete is false while anything is unentered: the total then counts the missing pieces at $0 and is "priced so far", and unpriced lists every hole — report it to the estimator, never fill it. Read-only.

Input schema present but exposes no named parameters.

NameTypeReqDescription
completebooleanyes–
costnumberyesmaterial + labor + tax
labornumberyes–
linesarrayyes–
materialnumberyesCondition material + supply rows
notestringyes–
overheadnumberyes–
profitnumberyes–
settingsobjectyes–
settings_sourcestringyes–
taxnumberyesSales tax on material
totalnumberyesThe base bid — 'priced so far' while complete is false
unpricedarrayyes–
warningsarrayyes–

No examples provided.

export_dxf ~434

The takeoff as a CAD drawing — a DXF (R2000) AutoCAD, BricsCAD, LibreCAD and Revit import as native geometry, not a picture. ONE sheet per file, like a DWG: every committed shape on that sheet becomes an LWPOLYLINE (floor rings CLOSED, walls and linear runs open, count marks a 1-ft circle), on a layer named for its finish — OT-<TAG>, with -DEDUCT / -HOLE / -WALL / -LINEAR / -COUNT suffix layers so a CAD user isolates any bucket with one layer filter, and room labels as TEXT on OT-LABELS. Coordinates are real units in the sheet's own frame: origin at the sheet's BOTTOM-left, Y up (CAD convention), feet by default ($INSUNITS 2) or metres with units:"m"; a ring's area in CAD equals its area in export_report to rounding, so the drawing IS the audit. Requires the sheet's scale (refuses otherwise — pixels in a DXF are worse than nothing); with several sheets carrying shapes, pass sheet to choose the drawing (the refusal lists them). The reply names every shape left out and why — a reconciled deduct ships as its parent's -HOLE ring, never twice. Writes to path (required — a DXF lives on disk, next to the DWG it aligns to); pair with export_marked_pdf for the reviewed planset.

NameTypeReqDescription
overwriteboolean–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 deliberatel…
pathstringyesFile path to write the .dxf to
sheetstring–Sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101"). Optional only when exactly one calibrated sheet carries shapes
unitsstring–Output units — "ft" (default) or "m"
NameTypeReqDescription
bytesintegeryes–
entitiesintegeryesLWPOLYLINE + CIRCLE + TEXT entities in model space
extents–yesModel-space bounding box in output units; origin = sheet's bottom-left, Y up
layersarrayyesLayer names in table order — OT-<TAG>, plus -DEDUCT/-HOLE/-WALL/-LINEAR/-COUNT suffix layers and OT-LABELS
pathstringyesThe DXF written
shapesintegeryesCommitted shapes that produced geometry
sheetstringyesSheet key the drawing was cut from
sheet_numberstring|nullyes–
skippedarrayyesShapes on this sheet left out, each with why — never silent
unitsstringyes–

No examples provided.

export_estimate ~251

Write the priced estimate as an Excel workbook — the canvas Estimate panel's Excel button: an Estimate tab (every line, then material, labor, tax, overhead, profit, base bid), a Materials tab (each supply's packages, price, cost) and, when anything is unentered, an Unpriced tab. Values only; an unentered rate is an empty cell, never 0. Every tab ends with "Prepared with OpenTakeoff". Default path: next to the loaded plan as "<project or plan> - estimate.xlsx". The reply repeats complete and unpriced — tell the user when the total is only priced so far.

NameTypeReqDescription
company_namestring–Prepared-by line
overwriteboolean–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 deliberatel…
pathstring–Where to write the .xlsx (default: "<plan dir>/<project or plan> - estimate.xlsx")
project_namestring–Project name on each tab (default: the plan file's name)
NameTypeReqDescription
completebooleanyes–
pathstringyes–
tabsarrayyes–
totalnumberyes–
unpricedarrayyes–

No examples provided.

export_marked_pdf ~457

The MARKED-UP PLANSET — 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 — 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 — 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. Every page carries a light diagonal "OpenTakeoff" and the line "Prepared with OpenTakeoff · opentakeoff.kentucky-ai.com". 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 — 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 — pure vector copy, so it works even where view_sheet cannot render. The one source it refuses: an ENCRYPTED plan PDF (owner password, empty user password — it opens everywhere, but its pages cannot be vector-copied and there is no canvas here to render them); the refusal names the sheet — export the marked set from the app, or supply an unencrypted PDF.

NameTypeReqDescription
overwriteboolean–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 deliberatel…
pathstring–Where to write the PDF (default: "<plan dir>/<plan> - marked set.pdf")
project_namestring–Cover-page project name (default: the plan file's name)
NameTypeReqDescription
annotations_drawnintegeryes–
approvals_drawnintegeryesApproval-family glyphs burned in (#176) — estimator APPROVED rings + agent AGENT diamonds; the cover tallies the split when any exist
notestringyes–
pagesintegeryesLegend cover + the RFI schedule page(s) when any RFI is live + one page per marked sheet
pathstringyesAbsolute path of the written marked-set PDF — hand this to the user
rfis_printedintegeryesLive RFIs printed on the RFI schedule page (#364) — agent-raised and panel-raised alike; withdrawn ones leave a numbering gap
shapes_drawnintegeryes–
sheets_markedintegeryesSheets carrying shapes, annotations, or approval marks — unmarked sheets are omitted

No examples provided.

export_proposal ~292

Write the bid proposal PDF — the canvas Proposal dialog's download: letterhead from company, prepared-for (set_estimate_settings client), the scope (each condition, its spec description, measured quantity), the base bid (lump sum, or itemized when the project's proposal says so), inclusions / exclusions / terms and signature blocks. Every page carries a light diagonal "OpenTakeoff" and the line "Prepared with OpenTakeoff · opentakeoff.kentucky-ai.com". Pass company exactly as the estimator gave it — this server has no Company setup of its own. It never refuses an incomplete estimate; the reply's complete and unpriced say what the base bid is missing, and the user must hear it before sending. Default path: next to the loaded plan as "<project or plan> - proposal.pdf".

NameTypeReqDescription
companyobject–The contractor's letterhead, from the estimator
overwriteboolean–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 deliberatel…
pathstring–Where to write the PDF (default: "<plan dir>/<project or plan> - proposal.pdf")
project_namestring–Project name on the proposal (default: the plan file's name)
NameTypeReqDescription
base_bidnumberyes–
completebooleanyes–
itemizedbooleanyes–
notestring––
pagesintegeryes–
pathstringyes–
unpricedarrayyes–

No examples provided.

export_report ~300

The computed Report document — "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 ÷ 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 — 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.

NameTypeReqDescription
overwriteboolean–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 deliberatel…
pathstring–File path to write the document to
project_namestring–Label for the document's project_name field (a headless session has no project of its own; omitted → null)
NameTypeReqDescription
by_labelarrayyes–
by_sheetarrayyesBASE per-sheet subtotals — multiplier NOT applied, no waste, no materials
condition_columnsarrayyes–
conditionsarrayyesconditionTotals rows: gross + *_net quantities AND the computed materials buy list
display_unitsstringyes–
generated_withstringyes–
markupsarrayyes–
materialsarrayyesProject-wide buy list — condition rows summed by (name, unit)
project_namestring|nullyes–
proposed_condition_editsarray–Pending condition-edit proposals (#365) — the rows above print the CURRENT knobs; each entry here carries the proposed values beside them. Present only when any are pending, so a proposal-free report…
rfisarrayyes–
roll_goodsarrayyesRoll-goods order rows (#136) — order_lf / rolls / order_qty per roll-goods condition, ×N applied; empty when no condition carries a roll_setup (always the case for a headless session today)
schemastringyes–
shape_labelsarrayyes–
sheetsarrayyesScale provenance per sheet — how each scale was set
totalsobjectyes–
unitsstringyes–

No examples provided.

export_takeoff ~162

The full "opentakeoff.takeoff_canvas.v1" annotations payload — exactly what the app autosaves, importable by it. Returned inline; pass path to also write it to disk as JSON. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
overwriteboolean–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 deliberatel…
pathstring–File path to write the payload to
NameTypeReqDescription
approvalsarray–Approval-family records (#176) — the estimator's APPROVED seals and the agent's verdict marks {id, actor, ts, sheet_id, at:[nx,ny], shape_id?, text?}. Present only when any exist (the canvas payload'…
bid_proposalobject–The project's proposal layout and text — present once set
client_infoobject–Who the proposal is for (client_name, client_address, reference) — present once set_estimate_settings or an import set it
condition_edit_proposalsarray–Pending condition-edit diffs (#365) — present only when any exist. Nothing on the condition changes until the estimator accepts in the canvas
conditionsarrayyes–
estimate_settingsobject–The project's markups (tax_pct, overhead_pct, profit_pct, profit_mode) — present once set
last_grouparrayyes–
markupsarrayyes–
project_namestringyes–
proposalsarray–Proposal batches (#365) — present only when any exist. Shapes reference them by origin.proposal_id; the canvas shows one Accept per batch
rfisarray–Live RFI records; withdrawn tombstones are omitted
schemastringyes–
shapesarrayyes–
sheet_grouparrayyes–
sheet_levelsobject–Present only when a sheet carries a level label (the app omits it when empty)
sheet_tabsarrayyes–
sheetsarrayyes–
unitsstring–Present only for a metric project; absent means imperial — the app's own diff-only convention

No examples provided.

find_schedule ~257

Locate a schedule table in the set (#87): pass a kind ("room finish", "material"/"finish") and get every matching table's sheet, title, headers, TOTAL row count, and REGION — sized for a view_sheet look or a read_sheet_text pull of exactly the table. A schedule continued across sheets is ONE match whose "parts" list every fragment (base first) with its own viewable region; tables read through rotated headers say so; a table answering for one building carries "building"; a table with delta/REV-marked rows says how many in "revised_rows". Errors with what WAS found when the asked-for kind isn't in the set. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
kindstringyes"room finish" (rooms → surface finishes), "finish"/"material" (codes → products), or "equipment" (MEP device schedules — fans, pumps, heaters, AHUs, VAVs, diffusers/grilles/registers — keyed by mark,…
NameTypeReqDescription
matchesarrayyes–

No examples provided.

find_text ~303

LOCATE a known string on a sheet — the complement to read_sheet_text (which returns what a region SAYS; this finds WHERE a string you already know sits). Case-insensitive substring match against each pdf.js text run, so a room label split across runs ("OFFICE" then "134" as separate items) needs a find_text call per fragment, or read_sheet_text over a region to see the whole thing joined. Every hit tells you which room a polygon belongs to — the locate-then-trace workflow: find_text the room number, then measure_polygon (One-Click is temporarily gated on this server) on that room's wall faces. Optionally restrict to a region {x0, y0, x1, y1}; results cap at limit (default 200), with count/truncated telling you exactly how much a tighter region or higher limit would recover. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
limitinteger–Max hits returned
qstringyesText to find — a room number ('134'), a label fragment ('RECEPTION'), a schedule tag ('CPT-1')
regionobject–Rect in image px (origin top-left, y down); omit for the full sheet
sheetstringyes–
NameTypeReqDescription
countintegeryesTotal matches before the limit cap
hitsarrayyes–
qstringyes–
sheetstringyes–
truncatedbooleanyestrue = count exceeds hits.length; narrow the region or raise limit

No examples provided.

get_sheet_vectors ~554

The STROKES — the sheet's vector layer exactly as the engine is fed it, so you can run your own geometry against what the app sees (#367). view_sheet lets you look, read_sheet_text lets you read, sheet_context classifies a region; this returns the raw extractor output that all of them and every shape verb (the wall network's pen weights, symbol_sweep's matching) work from: flat points [x1, y1, x2, y2, …] in image px, one meta byte per segment (low nibble flags: 1 curve chord, 2 clip-only, 4 fill-only, 8 polyline arc; pen width = meta >> 4), per-segment stroke luminance, the drawn figure each segment belongs to (subpath ordinal), the sheet's placed-image area, and its PDF layer table with a per-segment layer index (sheet_info.layers names and classifies the same ids). Nothing is classified, decimated, or merged here — segments arrive whole, in extraction order, undecimated, which is the point: a reader can build its own room finder, symbol matcher, or wall classifier on the same array and commit through the existing verbs with provenance intact. Paged, never clipped silently: a dense sheet runs to hundreds of thousands of segments, so the reply carries limit (default 20000 segments, ceiling 100000) and the ledger offset + returned + dropped === total on every page; dropped is exactly what passing next_cursor as cursor recovers. region keeps every segment that intersects the rect (endpoints untouched — the same keep test sheet_context uses, so total here equals sheet_context's total_in_region) and echoes it post-clamp. Read-only and stateless — no shape, condition, or scale is touched. A scan has no strokes: the verb refuses and names view_sheet as the path. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
cursorinteger–A previous reply's next_cursor — resume paging there. Omit for the first page
limitinteger–Page size in segments (default 20000, max 100000) — about 1 MB of JSON per 20000
regionobject–Rect in image px (origin top-left, y down); omit for the full sheet. A segment is kept when it intersects the rect
sheetstringyesSheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101")
NameTypeReqDescription
droppedintegeryesMatching segments AFTER this page that limit cut — exactly what next_cursor recovers. offset + returned + dropped === total on every reply; 0 means this page ends the set
image_areanumberyesTotal placed-image area on the sheet, image px² — a value near the sheet area means a scan or photo underlay sits under whatever linework there is
layer_idsarrayyesThe sheet's PDF Optional Content Group ids in first-seen order — the same ids sheet_info.layers reports (with names and roles); [] on an unlayered sheet
layer_ofarrayyesAligned with points: index into layer_ids, or −1 for a segment outside every layer. Empty only when the geometry carries no layer channel
limitintegeryesThe page size that applied
lumarrayyesAligned with points: stroke luminance 0 (black) – 255 (white), Rec. 709 over the stroke colour in force when the path was built. Empty only when the geometry carries no luminance channel
metaarrayyesOne byte per segment, aligned with points: low nibble flags — 1 curve chord (bezier tessellation or detected polyline arc), 2 clip-only path (invisible ink), 4 filled-not-stroked, 8 polyline-arc prov…
next_cursorinteger–Pass as cursor to fetch the next page; absent when dropped is 0
notestring–Present when the sheet is a scan wrapper — the few segments here are a frame, not the drawing
offsetintegeryesMatching segments BEFORE this page (skipped by cursor)
pageintegeryes–
pointsarrayyesFlat [x1, y1, x2, y2, …] — four numbers per segment, image px to 0.01, endpoints exactly as extracted (never clipped to the region, never merged); segments arrive in extraction order, the order the e…
regionarrayyesThe region actually resolved, post-clamp [x0, y0, x1, y1] image px — the same rect sheet_context and view_sheet take, so all three verbs answer in one frame
returnedintegeryesSegments in this reply
sheetstringyes–
sheet_pxarrayyes–
subpatharrayyesAligned with points: ordinal of the drawn FIGURE each segment belongs to (each moveTo starts one, each rectangle is one) — segments sharing a value are one path; −1 = outside every figure
totalintegeryesSegments on the sheet intersecting the region — the whole set, before paging

No examples provided.

import_takeoff ~309

The way BACK IN (#151): load an "opentakeoff.takeoff_canvas.v1" file — a prior export_takeoff, or the app's own save — into this session, through the SAME tested merge rules as the app's Sheet-menu import: finish-tag identity joins imported conditions onto this session's own (their knobs win), new ids append, duplicate ids skip (re-import is idempotent), and THIS session's calibration wins per sheet. An uncalibrated empty session adopts the file wholesale. New dimensional shapes refuse atomically if their source scale differs from the session calibration or is missing; align scales and re-export, or use a fresh session. Counts and duplicate IDs are exempt. Legacy agent traces without review flags arrive reviewed:false. Resume yesterday's work, extend a takeoff a human already reviewed (their ink stays ink — reviewed shapes arrive untouchable by agent verbs), or audit someone else's export with list_shapes/takeoff_summary. Requires a loaded plan; shapes referencing OTHER files ride along and count in totals but can't be viewed against this document — the reply's unknown_files names them. Approval marks ride the file too — transport, not minting: an estimator seal arriving by import stays estimator ink, listable but untouchable here. undo_last removes the imported SHAPES as one step; adopted conditions, scales, annotations, and approval marks stay.

NameTypeReqDescription
pathstringyesPath to a takeoff_canvas.v1 JSON file on disk
NameTypeReqDescription
conditions_addedintegeryes–
conditions_mergedintegeryesImported conditions that joined an existing finish tag (its knobs won)
filestringyesBasename of the imported file
notestringyes–
replacedbooleanyestrue = the session was empty and adopted the file wholesale
rules_importedintegeryesCorrection rules (#88) that arrived with the file — apply_rules re-runs them
scales_adoptedintegeryesSheets whose calibration came from the file (this session's own always wins)
shapes_addedintegeryes–
shapes_pendingintegeryesOf the added shapes, how many are unreviewed machine pencil
shapes_totalintegeryes–
unknown_filesarrayyesFiles referenced by imported shapes that this document doesn't have — they count in totals but can't be viewed here

No examples provided.

link_annotation ~106

Attach an existing annotation to a condition, or detach it by passing an empty condition — the canvas's Attach/Detach control, reachable by an agent. Use it to tie up notes left unattached (list_annotations reports how many), or to move one to the finish it actually concerns. Attaching mints the tag on first use.

NameTypeReqDescription
annotation_idstringyesId from annotate or list_annotations
conditionstringyesFinish tag to attach to; empty string detaches
NameTypeReqDescription
conditionstringyes–
condition_idstring––
idstringyes–
notestringyes–

No examples provided.

list_annotations ~210

Every annotation on the takeoff, with condition_id RESOLVED to its finish tag so you can act on the reply without joining against conditions[]. Filter by sheet, by condition, or both. Coordinates come back in image px (the same frame you passed in), not the normalized form they're stored as. `unattached` counts the notes carrying no condition — the candidates for link_annotation. `verdicts` is the approval family's inventory (mark_verdict/delete_verdict): every mark with its actor stated — the estimator's APPROVED ring or the agent's AGENT diamond — under the same filters, a condition filter reaching a verdict through its target shape. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
conditionstring–Only annotations attached to this finish tag
sheetstring–Only annotations on this sheet
NameTypeReqDescription
annotationsarrayyes–
countintegeryes–
unattachedintegeryesHow many carry no condition — candidates for link_annotation
verdict_countintegeryes–
verdictsarrayyesApproval-family records (#176) under the same filters: sheet applies directly; a condition filter reaches a verdict THROUGH its target shape (a sheet-point mark carries no scope and drops out)

No examples provided.

list_rfis ~99

Every RFI in the register with its status, sheet, who raised it (actor) and whether an agent-raised one is still pending the estimator's acceptance, its linked markup ids, and the finish tags those markups are attached to — the scopes the question touches. withdrawn[] lists the numbers delete_rfi tombstoned, so a gap in the sequence is explained rather than silent. Read this before raising a question the register already holds.

Input schema present but exposes no named parameters.

NameTypeReqDescription
countintegeryes–
openintegeryesStill awaiting an answer
pendingintegeryesAgent-raised and not yet accepted by an estimator
rfisarrayyesEvery live RFI, register order
withdrawnarrayyesNumbers of withdrawn RFIs (delete_rfi tombstones) — the gaps in the sequence, explained

No examples provided.

list_shapes ~121

The mid-session shape inventory (#149): every committed shape's id, sheet, condition tag, role, quantities, room label, vertex count, and review state in one compact read — the ids edit_shape and delete_shape assume you have, without pulling the whole export_takeoff payload to find one shape. Filter by sheet, by condition, or both; filters narrow, an empty list is a result, not an error.

NameTypeReqDescription
conditionstring–Only shapes under this finish tag (must exist)
sheetstring–Only shapes on this sheet
NameTypeReqDescription
countintegeryes–
shapesarrayyes–

No examples provided.

load_plan ~251

Open a plan PDF from disk. Default: replace the whole session (previous documents, scales, conditions, and shapes are cleared). merge: true ADDS the document to the working set instead (#152) — a bid set is plans + schedule + addenda, not one PDF — keeping every scale, condition, and shape; sheet keys carry file names so documents never collide, the sheet graph spans the whole set (resolve_tag can chain a plan tag on one file to a schedule row in another), and the marked set covers every worked sheet. Re-loading an already-merged file is refused — reload = replace, deliberately. Returns file, files, page_count, and one entry per sheet. The loaded sheets also become browsable resources (takeoff://sheets). Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
mergeboolean–true = ADD this document to the working set, keeping all existing work (merge into an empty session is just a load)
pathstringyesPath to a plan PDF on disk
NameTypeReqDescription
filestringyesThe document just loaded (basename)
filesarrayyesEvery document in the working set, load order (#152 — one entry unless merge was used)
notestringyes–
page_countintegeryesTotal sheets across the working set
sheetsarrayyesEVERY sheet in the working set, not just the file loaded by this call

No examples provided.

mark_verdict ~467

Mark the agent's VERDICT on work — the pencil half of the approval family, and the only half an agent can mint. Two actors exist on the record: the estimator's APPROVED ring is ink, minted solely by a human's click at the canvas's Approve tool; this tool mints the AGENT diamond and structurally nothing else — it takes no actor input to misuse. Target the work either way: shape_id anchors the mark ON a committed shape (a room at its area centroid, a run at its on-path midpoint, a count marker at its point) and records WHAT was marked — the shape_id stays on the record as provenance, and the glyph keeps its own anchor even if the shape is later deleted; or sheet + at drops the mark at a sheet point (image px). Exactly one target. Optional text rides the record through every export; the glyph itself always reads AGENT. A verdict touches no quantity and gates nothing: it is the agent's signed claim that it checked this work — pencil beside the estimator's ink, never in its place. The mark renders as the graphite AGENT diamond on the canvas and in the marked set, the marked-set cover tallies the split ("Approval stamps: N estimator-approved · M agent-marked"), and the record rides the annotations payload through export_takeoff / import_takeoff and the app's own saves. One mark per shape (re-mark = delete_verdict, then mark again); list_annotations returns the inventory in verdicts[]; undo_last steps over a mark exactly like any other mutation. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
atarray–Sheet-point mode: where the AGENT diamond renders (image px)
shape_idstring–Mark a committed shape (list_shapes has the ids) — anchored on the shape, recorded as provenance. Exactly one target: this OR sheet + at
sheetstring–Sheet-point mode: the sheet, together with at
textstring–Optional short note riding the record and every export — the glyph always reads AGENT
NameTypeReqDescription
actorstringyesAlways agent — this tool is structurally incapable of minting the estimator's seal
atarray–Where the AGENT diamond renders (image px) — absent only when the marked shape rides a sheet from a file this session hasn't loaded (#152)
conditionstring–Shape mode: the marked shape's finish tag, resolved
idstringyesThe minted record id ("apr-…")
notestringyes–
shape_idstring–Shape mode: the committed shape this verdict is about
sheetstringyes–
textstring––
tsstringyesISO-8601 mint time

No examples provided.

measure_line ~467

Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips, conduit and home runs). A curved run (base along a radius wall, a curved feature strip) takes arc_through: one point on the bow, marked. DROP AND RISE (#441): a plan trace is the flat X–Y path; the material also travels VERTICALLY — a home run drops from the ceiling to a panel, rises to a box. length_lf is the TOTAL: plan + rise + drop. The condition's rise_ft / drop_ft (edit_condition) are the defaults for every run under it; pass rise_ft / drop_ft here to give THIS run its own legs (0 included — "no drop on this one" is a statement), and the reply splits plan_lf / vertical_lf beside the total when a leg exists. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
arc_througharray–Indices of points that are the MIDDLE of an arc: the trace runs the point before → this point → the point after as the unique circle through the three (the canvas's Curve mode). For a curved wall put…
conditionstring––
drop_ftnumber–This run's vertical leg DOWN, in feet, added to its plan length — overrides the condition's drop_ft default for this run (0 = no drop here, whatever the default)
ptsarrayyes–
rise_ftnumber–This run's vertical leg UP, in feet, added to its plan length — overrides the condition's rise_ft default for this run (0 = no rise here, whatever the default)
sheetstringyes–
NameTypeReqDescription
arcsinteger–How many arc_through bows were laid — present only when the trace was bent; the vertices reported are the baked arc, not the three points you gave
drop_ftnumber–The drop this run resolved to — present with vertical_lf
length_lfnumberyesThe run's TOTAL length: plan trace + rise + drop (#441)
nptsintegeryes–
plan_lfnumber–The flat X–Y trace alone — present only when the run carries a vertical leg
rise_ftnumber–The rise this run resolved to (its own, else the condition default) — present with vertical_lf
shape_idstring–Present when condition was passed and the shape committed
vertical_lfnumber–rise_ft + drop_ft — present only when a leg exists

No examples provided.

measure_polygon ~332

Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. A room ring belongs on the innermost wall-face strokes from get_sheet_vectors, crossing each door opening on the wall centerline and wrapping columns and stubs; never on a hatch edge, casework or a door leaf. Check it with view_sheet overlay:true on a tight crop and fix it with edit_shape. A CURVED wall is a circle: do not chord it and do not hand-tessellate it — give the bow one point on the wall and list its index in arc_through. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
arc_througharray–Indices of points that are the MIDDLE of an arc: the trace runs the point before → this point → the point after as the unique circle through the three (the canvas's Curve mode). For a curved wall put…
conditionstring––
rolestring––
sheetstringyes–
vertsarrayyes–
NameTypeReqDescription
arcsinteger–How many arc_through bows were laid — present only when the trace was bent; the vertices reported are the baked arc, not the three points you gave
area_sfnumberyes–
nvertsintegeryes–
perimeter_lfnumberyes–
shape_idstring–Present when condition was passed and the shape committed
warningstring–Mixed-scale warning (#153): a scale note disagreeing with the sheet's sits in the measured region — verify before trusting these numbers

No examples provided.

measure_surface ~359

Surface Area — wall SF (#146): trace an OPEN run along the wall in plan view (min 2 points, image px) and the quantity is traced LF × height. This is how wall tile, wainscot, and wall systems are taken off — the quantity family measure_polygon cannot produce. Height lives on the CONDITION (the canvas's H knob): pass height_ft to set it on this call (journals as its own undo step, like typing H before tracing), or set it once with edit_condition; with neither, this refuses and mints nothing. The shape snapshots the height it was quantified at. Requires the sheet's scale. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

NameTypeReqDescription
arc_througharray–Indices of points that are the MIDDLE of an arc: the trace runs the point before → this point → the point after as the unique circle through the three (the canvas's Curve mode). For a curved wall put…
conditionstringyesFinish tag to commit under (minted on first use), e.g. 'CT-W1'
height_ftnumber–Wall height in feet — written to the condition's H knob first, then used
ptsarrayyesThe wall run, an open polyline (image px)
sheetstringyes–
NameTypeReqDescription
arcsinteger–How many arc_through bows were laid — present only when the trace was bent; the vertices reported are the baked arc, not the three points you gave
area_sfnumberyeslength_lf × height_ft — the wall SF committed
conditionstringyes–
height_ftnumberyesThe height this shape was quantified at (snapshotted on the shape)
length_lfnumberyesThe traced run's open length
nptsintegeryes–
shape_idstringyes–

No examples provided.

Install

How do I install the OpenTakeoff MCP server?

OpenTakeoff runs locally as an npm package, launched with npx -y opentakeoff-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

npm · opentakeoff-mcp

# add to Claude Code
claude mcp add kentucky-ai-opentakeoff -- npx -y opentakeoff-mcp
Common questions

What is the OpenTakeoff MCP server?

OpenTakeoff is an MCP server listed in the public MCP registry as io.github.Kentucky-ai/opentakeoff. Construction takeoff for AI agents: load plans, set scale, measure, count, export with provenance. This page covers its npm package (opentakeoff-mcp).

Is the OpenTakeoff MCP server safe to use?

OpenTakeoff scores 94 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 5 November 2026. It declares no install or post-install scripts. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

What tools does the OpenTakeoff MCP server expose?

OpenTakeoff exposes 58 tools: load_plan, sheet_info, set_scale, propose_takeoff, measure_polygon, and 53 more. Their descriptions and schemas cost roughly 20,339 tokens of context every time the server is loaded.

Is the OpenTakeoff MCP server still maintained?

OpenTakeoff is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

What licence is the OpenTakeoff MCP server under?

OpenTakeoff declares the AGPL-3.0-only licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.