OpenTakeoff
NPM · OPENTAKEOFF-MCP · SCANNED SEP 20
Construction takeoff for AI agents: load plans, set scale, measure, count, export with provenance.
Available components
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
- 38 of 145 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 (Apache-2.0).Pass
- Actively maintained (last published 6 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 20139 tokens (~268/item across 75 items; 53 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 Management95
- Stability check failed: the tool surface changed between 0.9.62 and 0.9.87: 2 tool removals, 0 breaking changes, 14 additions. See how to fix → Fail
Tool Coverage96
- 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
- 87% 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 55 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
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
claude mcp add kentucky-ai-opentakeoff -- npx -y opentakeoff-mcp
{
"mcpServers": {
"kentucky-ai-opentakeoff": {
"command": "npx",
"args": [
"-y",
"opentakeoff-mcp"
]
}
}
} {
"servers": {
"kentucky-ai-opentakeoff": {
"command": "npx",
"args": [
"-y",
"opentakeoff-mcp"
]
}
}
} codex mcp add kentucky-ai-opentakeoff -- npx -y opentakeoff-mcp
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"kentucky-ai-opentakeoff": {
"type": "local",
"command": [
"npx",
"-y",
"opentakeoff-mcp"
],
"enabled": true
}
}
} openclaw mcp add kentucky-ai-opentakeoff --command npx --arg -y --arg opentakeoff-mcp
mcp_servers:
kentucky-ai-opentakeoff:
command: "npx"
args: ["-y", "opentakeoff-mcp"] {
"McpServers": {
"kentucky-ai-opentakeoff": {
"Transport": "stdio",
"Command": "npx",
"Arguments": [
"-y",
"opentakeoff-mcp"
]
}
}
} assistant mcp add kentucky-ai-opentakeoff -t stdio -c npx -a -y opentakeoff-mcp
{
"mcpServers": {
"kentucky-ai-opentakeoff": {
"command": "npx",
"args": [
"-y",
"opentakeoff-mcp"
]
}
}
} 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.
- 18 Sept 26 +1
No change was recorded against any check on this day. Stability & Change Management went from 88 to 92.
- 16 Sept 26 −1
No change was recorded against any check on this day. Stability & Change Management went from 95 to 85.
- 14 Sept 26 +15
- Malware scan: unverified → pass ▲ security
- 13 Sept 26 0
- Malware scan: unverified → pass ▲ security
- Package version: 0.9.86 → 0.9.87 functional
- 12 Sept 26 +12
- Stability: fail → unverified ▼ security
- Tool safety: pass → unverified ▼ security
- Malware scan: unverified → pass ▲ security
- Known CVEs: unverified → pass ▲ security
- Capabilities: pass → unverified ▼ functional
- Tool coverage: 100 → unverified ▼ functional
- Schema quality: 100 → unverified ▼ functional
- Dependency health: unverified → 0.89 ▲ functional
- Package version: 0.9.83 → 0.9.86 functional
- Package version: 0.9.83 → 0.9.85 functional
- Package version: 0.9.83 → 0.9.84 functional
- 11 Sept 26 −27
- Known CVEs: pass → unverified ▼ security
- Malware scan: pass → unverified ▼ security
- Stability: fail → unverified ▼ security
- Tool safety: pass → unverified ▼ security
- Dependency health: 0.89 → unverified ▼ functional
- Capabilities: pass → unverified ▼ functional
- Tool coverage: 100 → unverified ▼ functional
- Schema quality: 100 → unverified ▼ functional
- Security disclosure: pass → unverified ▼ functional
- Schema quality: 350 → 261 ▲ functional
- Schema quality: 350 → 305 ▲ functional
- Package version: 0.9.77 → 0.9.83 functional
- Package version: 0.9.77 → 0.9.82 functional
- Package version: 0.9.77 → 0.9.81 functional
- Package version: 0.9.77 → 0.9.80 functional
- 10 Sept 26 +1
No change was recorded against any check on this day. Stability & Change Management went from 92 to 95.
- 8 Sept 26 +1
No change was recorded against any check on this day. Stability & Change Management went from 85 to 88.
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 20 Sept 2026 · Analysed npm/opentakeoff-mcp@0.9.87
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.87 |
| Rekor log index | 2813525149 |
| Predicate type | https://slsa.dev/provenance/v1 |
| Subject digest | sha512:fd3d45674eca2680ee6f1715c38b8c300e0cb6f77b1c07b5d26af5fc57d2aadc40e67ed7dfcf87633a56312a58e91489dc194d1d0894b7b38ec4fc3d7 |
Background: How many MCP packages publish verified provenance →
Dependencies 145 packages
| Packages resolved | 145 |
|---|---|
| Stale | 38 |
| Tree resolution | Complete |
Background: SBOMs and build attestations, explained →
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 →
resolve_rfi ~148
Answer an OPEN RFI: the answer lands as its response, status becomes answered (the register's own state for "response in"), and the response date stamps exactly as the panel's would, plus an ISO timestamp of the resolve. Only an open RFI resolves — an answered, closed, or void one is refused rather than re-answered or quietly revived (undo_last reverses your own resolve if the answer was wrong). Record the answer the drawings or the architect actually gave; an RFI is not resolved by guessing.
| Name | Type | Req | Description |
|---|---|---|---|
| answer | string | yes | The response — what settles the question |
| rfi_id | string | yes | Record id from create_rfi or list_rfis |
| Name | Type | Req | Description |
|---|---|---|---|
| actor | string | yes | Who raised it — "agent" for every RFI minted over MCP, "estimator" for a panel-raised one |
| conditions | array | yes | Finish tags the linked markups are attached to — the scopes this question touches |
| date | string | yes | YYYY-MM-DD opened |
| id | string | yes | The record id ("rfi-…") |
| linked_markups | array | yes | Annotation ids carrying this RFI's number on the sheet (markup.rfi_id) — derived, never stored twice |
| note | string | yes | – |
| number | string | yes | The register number, "RFI-001" — next in the panel's own sequence, never reissued |
| pending | boolean | yes | true = agent-raised and not yet accepted by an estimator in the register (origin.reviewed false) — pencil, not sent |
| question | string | yes | – |
| resolved_at | string | yes | ISO-8601 time of this resolve |
| response | string | yes | – |
| response_date | string | yes | YYYY-MM-DD answered, '' while open |
| sheet | string | yes | The sheet the question is about |
| status | string | yes | The panel's lifecycle: open → answered → closed; void = withdrawn |
| subject | string | yes | – |
No examples provided.
resolve_tag ~323
Resolve ONE room tag across the set (#87): the plan tag → its room-finish schedule row → each finish code's definition in the finish/material schedule, EVERY edge carrying an evidence pointer (sheet + literal text + bbox — pass a bbox to view_sheet to look at the source). Rows carried by a continuation sheet ("… SCHEDULE — CONT'D") resolve exactly like base-sheet rows, citing the sheet the ink is on. The doctrine is refusal over guessing: a room that appears on the plan with no schedule row returns status "unresolved" with the reason (and still cites the plan tag); reused room numbers return "ambiguous" rather than picking one — on a multi-building set the refusal LISTS the candidate rows per building, and a building-qualified tag ("A-134") picks the building the set names. A delta triangle or REV tag on the answering row (or the plan bubble) rides the result as "revisions": the codes returned are the POST-revision answer, but the ink changed under that delta — view_sheet the marker's bbox and check the addendum before pricing. 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.
| Name | Type | Req | Description |
|---|---|---|---|
| tag | string | yes | The room tag as drawn, e.g. "134" or "139A" — or building-qualified on a multi-building set, e.g. "A-134" (building A, room 134) |
| Name | Type | Req | Description |
|---|---|---|---|
| building | string | – | resolved only — the building whose schedule row answered, when the set names buildings |
| candidates | array | – | unresolved only — every schedule row that COULD have answered (an ambiguous multi-building tag lists one per building; qualify the tag, e.g. "A-134", to pick) |
| finishes | array | – | – |
| reason | string | – | unresolved only — WHY (no schedule row / ambiguous / no schedule found). A room that appears on the plan with no row comes back here, never as a silent omission |
| revisions | array | – | resolved only — delta/REV markers on the answering schedule row or the plan bubble. The finishes above are the POST-revision answer, but the ink changed: check the marker (view_sheet its bbox) and th… |
| room | – | yes | The plan tag, when the room appears on a plan sheet — cited even when resolution fails. null on a multi-building ambiguity: citing one building's tag would be quietly wrong |
| sources | array | – | The chain: plan tag → schedule row (the row cites the sheet that CARRIES it — under a continuation that is the CONT'D sheet) |
| status | string | yes | – |
| tag | string | yes | – |
No examples provided.
revise_proposal ~257
Replace EVERY still-pending shape in a proposal with a new set, as ONE journal step (#365) — the move for "I re-measured and got a better batch". The old pending shapes go, the replacements commit under the same proposal, and undo_last puts the previous batch back exactly. All-or-nothing: the whole replacement is validated (sheet, scale, vertex count, a height for surface_area) before the first pending shape is removed, so a malformed last shape leaves the batch untouched and the error says which entry and why. Shapes the estimator already accepted are ink — they stay, and they are not part of what this replaces. verts are image px like every other tool; roles and minimums match the measure tools (floor_area/deduct ≥3, linear/surface_area ≥2, count 1). An empty shapes list is refused — withdraw_proposal is the verb for that. 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.
| Name | Type | Req | Description |
|---|---|---|---|
| proposal_id | string | yes | The batch, from propose_takeoff |
| shapes | array | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| committed | integer | yes | Replacement shapes committed, all attached to the same batch |
| label | string | yes | – |
| note | string | yes | – |
| proposal_id | string | yes | – |
| replaced | integer | yes | Pending shapes removed from the batch |
| shape_ids | array | yes | – |
No examples provided.
scope_duplicates ~371
Two conditions claiming the same floor, as a list (#366). Every pair of committed floor_area shapes on one sheet whose EXACT polygon intersection exceeds min_fraction of the smaller shape — with the shared SF, which condition each belongs to, whether the estimator already affirmed either, and a look region to pass to view_sheet {overlay: true}. Pairs on DIFFERENT conditions are collisions: every total downstream counts that floor twice. Pairs on the SAME condition are a double trace (a different bug) and come back in duplicates. shared_floor_sf is the whole compared set's Σ areas − union, counted once per cell no matter how many shapes pile on it — the number takeoff_summary carries and the one that has to read 0 before any total means anything. Machine-precision edge remnants are ignored; a real overlap below 0.01 SF stays listed with an explanatory note. Supporting materials belong in edit_materials coverage rows, not duplicate floor polygons. Deducts and runs are not claims. Read-only; a shape on an unscaled sheet or with a degenerate ring is listed in unmeasured, never counted as zero. Same rule as the room eval's shared-floor gate (iou ≥ 0.5 = the same space claimed twice). 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.
| Name | Type | Req | Description |
|---|---|---|---|
| min_fraction | number | – | List a pair only when shared ÷ smaller ≥ this (default 0.05 — rings that merely kiss along a wall are not claims; 0 lists every positive overlap above machine-precision noise) |
| sheet | string | – | Restrict to one sheet; default every sheet with floor shapes |
| Name | Type | Req | Description |
|---|---|---|---|
| by_sheet | array | yes | – |
| collisions | array | yes | Pairs on DIFFERENT conditions, biggest shared floor first |
| duplicates | array | yes | Pairs on the SAME condition — a double trace, a different bug |
| floor_shapes | integer | yes | – |
| min_fraction | number | yes | – |
| note | string | yes | – |
| shared_floor_sf | number | yes | Σ areas − union over the compared floor shapes, counted once per cell |
| unmeasured | array | yes | Shapes left out of every number, with the reason — never silently counted as zero |
No examples provided.
scope_merge ~277
Resolve ONE collision (#366): given a pair of floor shapes and the winner, the loser gives up the shared floor — TRIMMED to its remainder by an exact boolean difference (the cut_out module's own arithmetic; its quantities re-measured from the result), or DELETED outright when the overlap is near-total (≥ 98% of the loser: the same space claimed twice, not a room with a sliver left). One journal step either way; undo_last restores the loser verbatim. Who wins: state winner; with it omitted the reviewed shape wins over a pending one, and the verb refuses when neither is reviewed (it does not guess which condition the floor belongs to) or when BOTH are (that is the estimator's call — the collision shows on both condition rows in the canvas). The ink rule is absolute: a loser the estimator affirmed is refused whoever you name. A trim that would split the loser into disjoint pieces refuses — that is a re-trace decision, not a merge — and a loser carrying reconciled cutouts refuses (delete the cuts first).
| Name | Type | Req | Description |
|---|---|---|---|
| shape_a | string | yes | One shape of the pair (from scope_duplicates) |
| shape_b | string | yes | The other |
| winner | string | – | Which of the two keeps the shared floor; omit to let the reviewed one win |
| Name | Type | Req | Description |
|---|---|---|---|
| action | string | yes | – |
| loser | string | yes | – |
| loser_after_sf | number | yes | – |
| loser_before_sf | number | yes | – |
| loser_holes | integer | – | trimmed: holes the remainder carries (a winner inside the loser leaves one) |
| note | string | yes | – |
| shape_count | integer | yes | – |
| shared_sf | number | yes | – |
| winner | string | yes | – |
No examples provided.
set_scale ~271
Set a sheet's scale — exactly ONE of: label (a standard scale, e.g. '1/4" = 1'-0"'), upp (real feet per image px), calibrate (two points along a known dimension plus its real feet), or use_detected (adopt the drawn scale note read off the sheet). The detected scale is never applied automatically — setting it is always this explicit call. Changing an existing scale recomputes measurements and cutout restore quantities from geometry and records one undo_last step. Counts are unchanged. Human-reviewed dimensional work must be recalibrated in the canvas; scales must be finite and positive. 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.
| Name | Type | Req | Description |
|---|---|---|---|
| calibrate | object | – | Two points (image px) a known real distance apart, and that distance in feet |
| label | string | – | A standard scale label, exactly as listed in the error on a miss |
| sheet | string | yes | – |
| upp | number | – | Real feet per image px at render scale 2.0 |
| use_detected | boolean | – | true = adopt the sheet's detected scale |
| Name | Type | Req | Description |
|---|---|---|---|
| confirmed | boolean | yes | Always false here: set_scale is the agent surface, and an agent-set scale stays UNCONFIRMED until a human confirms it in the canvas — quantities still flow, wearing the caveat |
| label | string | – | The standard scale label, when set by label or detected note |
| sheet | string | yes | – |
| source | string | yes | – |
| upp | number | yes | Real feet per image px at render scale 2.0 |
| warning | string | – | Present when the sheet carries MULTIPLE distinct scale notes (#153) — enlarged plans/details likely; region measurements under a disagreeing note will warn |
No examples provided.
sheet_context ~455
The sheet's STRUCTURE in one call and one frame: the classified vector segments, the positioned text spans, and the hatch-family instances of a region — everything the engine itself floods against, exposed as data instead of pixels. Use it when you need to REASON about a region rather than look at it: which lines bound this space and at what pen weight, what the region says, and which periodic fill pattern covers it. The join is the point — all three arrive in image px with no reconciliation left to do, and the reply echoes the post-clamp region so passing that same rect to view_sheet gives you the matching render by construction. Hatch families carry a content-derived id (same pattern spec ⇒ same id, anywhere on the sheet), so matching a plan region to a legend swatch is comparing two ids, not guessing from a render — read the legend region, read the room region, match ids, and cite both bboxes as evidence. Decimation is declared, ordered, and counted on every reply: segments shorter than min_len_px drop first (invisible ink), then a max_segments cap applies LONGEST-FIRST so walls survive and hatch strokes go; kept + dropped always reconciles to total_in_region, and whole segments drop with their meta intact — nothing is ever simplified or merged, because these are classified segments and a merge would rewrite the classification. A scan returns has_vector_linework: false with empty vectors — absence of linework, never a claim the region is blank. 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.
| Name | Type | Req | Description |
|---|---|---|---|
| max_segments | integer | – | Segment cap, applied longest-first (default 4000). The reply's dropped.cap says exactly what a smaller region would recover |
| min_len_px | number | – | Drop segments shorter than this (default 2 — one PDF point at render scale 2.0, below any pen width). 0 keeps everything |
| region | object | – | Rect in image px (origin top-left, y down); omit for the full sheet |
| sheet | string | yes | – |
| Name | Type | Req | Description |
|---|---|---|---|
| has_vector_linework | boolean | yes | false = a scan: vectors and hatch are empty because there are none, not because the region is blank |
| hatch | object | yes | – |
| page | integer | yes | – |
| region | array | yes | The region actually resolved, post-clamp — pass this same rect to view_sheet and the render is in the same frame by construction |
| sheet | string | yes | – |
| sheet_px | array | yes | – |
| text | object | yes | – |
| vectors | object | yes | – |
No examples provided.
sheet_graph ~389
The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / …, with confidence and the title evidence), the schedule tables found (kind, row count, region — a schedule CONTINUED across sheets ("… SCHEDULE — CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every number CORROBORATED as a room (with the stacked room NAME when one exists, the room's BUILDING on multi-building sets, and "corroboration" saying why it counts as a room) plus "unmatched_tags" — the numbers that are NOT rooms (keynote hexagons, detail markers, dimension fragments, legend rows), each with a reason, listed and never dropped; READ those reasons, one of them may be a room the schedule left out, the detail callouts (3/A-601 → sheet edges), the set's building designators, every REVISION marker the set carries (text markers "Δ2"/"REV 2" AND drawn deltas — a bare digit inside a triangle of linework, proven from vector geometry and flagged drawn — in "revisions", and attached to the schedule row / room tag they sit on), and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with measure_polygon on its wall faces. A scanned set (no text layer) returns available: false — unavailable, never half-populated. 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.
Input schema present but exposes no named parameters.
| Name | Type | Req | Description |
|---|---|---|---|
| available | boolean | yes | false = the set has no text layer (a scan) — the graph degrades to unavailable, never half-populates |
| buildings | array | – | Every building designator the set names (sorted) — present only on multi-building-aware sets. Room numbers reused across these need qualified tags ('A-134') |
| callouts | array | yes | Detail callouts (3/A-601) — edges to their target sheets |
| counts | object | yes | – |
| notes | array | – | Named gaps found while indexing (e.g. a continuation whose rows could not be aligned) — the graph refuses silently dropping anything |
| revisions | array | – | Every delta-triangle / REV-tag marker the set carries — text markers ('Δ2', 'REV 2') and DRAWN deltas (a bare digit inside a triangle of linework, drawn: true) — where one sits, the ink changed under… |
| rooms | array | yes | Numbers CORROBORATED as rooms — a room-finish row answers for them, or (where the set carries no room-finish schedule) a room name is drawn with them. Each says which in `corroboration`. Schedule she… |
| sheets | array | yes | – |
| unmatched_tags | array | – | Numbered tags on plan sheets that are NOT counted as rooms — keynote hexagons, detail markers, dimension fragments, legend rows. Listed with a reason, never dropped. A real finish plan is covered in… |
No examples provided.
sheet_info ~128
Sheet detail: dims (px and pt), vector segment count, whether the sheet has vector linework (measure_polygon runs on it; get_sheet_vectors returns the strokes), scale status, the detected scale suggestion, and this sheet's committed shape count. 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.
| Name | Type | Req | Description |
|---|---|---|---|
| sheet | string | yes | Sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") |
| Name | Type | Req | Description |
|---|---|---|---|
| detected_scale | string | – | Drawn scale note read off the sheet — a suggestion, never auto-applied |
| has_vector_linework | boolean | yes | one_click needs vector linework |
| height_pt | number | yes | – |
| height_px | number | yes | – |
| layers | array | yes | The sheet's PDF layer table (#85) — [] when no Optional Content survived export (every engine path then runs the heuristics unchanged) |
| multiple_scales | boolean | – | Several DISTINCT scale notes on this sheet (#153) — enlarged plans/details likely |
| page | integer | yes | 1-based page number |
| scale_set | boolean | yes | – |
| seg_count | integer | yes | Vector segment count |
| shape_count | integer | yes | Committed shapes on this sheet |
| sheet | string | yes | Sheet key: page 1 is the bare file name ("plan.pdf"), pages 2+ are "plan.pdf#2" |
| sheet_number | string | – | Title-block sheet number ("A-101") where detected |
| upp | number | – | Real feet per image px at render scale 2.0 — present once the scale is set |
| width_pt | number | yes | – |
| width_px | number | yes | Image px at render scale 2.0 — the coordinate space every tool speaks |
No examples provided.
split_condition ~115
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. Use when two variants have diverged far enough that following one another is wrong. A condition that already owns its materials returns split:false rather than erroring. Reversible with undo_last.
| Name | Type | Req | Description |
|---|---|---|---|
| condition | string | yes | Finish tag of the twin to split, e.g. 'CPT-1 – Level 2' |
| Name | Type | Req | Description |
|---|---|---|---|
| condition | string | yes | – |
| condition_id | string | yes | – |
| family_id | string | – | Kept — it still groups with its siblings |
| frozen_rows | integer | yes | Following rows frozen at their current values |
| note | string | yes | – |
| split | boolean | yes | false = it already owned its materials; nothing was following |
No examples provided.
sweep_schedule_row ~750
Take off a schedule row's mark from the row itself — the estimator's own gesture: a transition type sometimes exists only as a schedule row plus tag markers scattered across the plan sheets, and this tool mints the condition FROM the row and finds every occurrence. Pass the row's key (e.g. 'T1') and the tool (1) reads the row from the set's schedule tables (the sheet_graph/find_schedule machinery — the row is the condition's cited source), (2) anchors a geometric 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 before it is trusted — `anchor.corroborated`), and (3) sweeps every PLAN-role sheet for it. 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 row's tag is excluded and says whose it is, an unlabeled match is withheld as a question, and a tag drawn with no matching marker is disclosed as text_only. REFUSAL over guessing, with the reason and the fix: no such row; the same key in two tables (ambiguous); a tag drawn on no plan sheet; no repeatable marker linework around the tag — a fingerprint is never guessed from text alone (the fallback is always: marquee one instance with symbol_sweep). commit: true commits the counted matches as EA markers under the row's own key — one undo step for the whole set-wide sweep, every marker carrying origin.assignment {source: "schedule"} plus the anchor and row citation on origin.symbol.seed. The COUNT is scale-free (EA), but matching is not: where the anchor sheet and a target sheet both carry a scale, the marker is resized by their exact ratio before matching (`scaled` per sheet), and where one does not, the sweep runs at 1:1 and discloses it (`scale_assumed`) rather than reporting…
| Name | Type | Req | Description |
|---|---|---|---|
| commit | boolean | – | Commit every counted match as one EA count marker (excluded/withheld/text_only never commit) |
| mirror | boolean | – | Also match mirrored markers |
| rotations | boolean | – | Also match 90/180/270-rotated markers |
| tag | string | yes | The schedule row's key exactly as drawn, e.g. 'T1', 'TR-2' — it becomes the condition tag on commit |
| tolerance_px | number | – | Endpoint match tolerance in image px (default 2 — CAD jitter, not drift) |
| Name | Type | Req | Description |
|---|---|---|---|
| anchor | – | yes | null when the sweep counted BY LABEL: no repeatable marker geometry sits around the drawn tag (the equipment convention — a device drawn to its own size, tagged by a leader), so nothing was fingerpri… |
| committed | integer | – | commit mode: count shapes committed — one per counted match, the whole sweep ONE undo step |
| complete | boolean | yes | True when every proposed placement was scored on every swept sheet — false means at least one sheet's count is a FLOOR, not a total (#261) |
| condition | string | – | commit mode: the condition minted FROM the row — its key is the tag |
| counted_by | string | yes | How the count was made — "label" means no marker geometry was matched at all; look at each label_only placement before pricing |
| ea_total | number | – | – |
| found | integer | yes | Instances counted across every plan sheet: geometry matches carrying the row's own tag PLUS drawn tags counted by label — see counted_by |
| found_by_geometry | integer | yes | …of which marker-fingerprint matches corroborated by the tag |
| found_by_label | integer | yes | …of which drawn tags amid linework the fingerprint did not (or could not) reach — an installed instance by the equipment convention, disclosed as label-counted |
| note | string | – | – |
| row | object | yes | The schedule row the sweep was seeded from — the condition's source |
| shape_ids | array | – | – |
| sheets | array | yes | One entry per swept PLAN-role sheet, load order |
| skipped | array | yes | Sheets excluded from counting (schedule/detail/legend/unknown), each with its reason |
| tag | string | yes | The row key as normalized (the tag as drawn) |
| warning | string | – | Present when the per-sheet work cap dropped candidates |
No examples provided.
symbol_sweep ~2,312
Find EVERY instance of a repeated plan symbol from ONE example — drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans — both ON by default; turn them off to pin orientation). Score ≥ 0.92 is a match; the 0.75–0.92 band comes back in `withheld` with a reason — a near-match is a question you answer by LOOKING (view_sheet at its `at`), never a silent commit and never a silent drop. RICHER VARIANTS are named, never silent: a placement that reproduces the whole seed but carries >30% extra linework fully inside its footprint (a register against a grille seed — the same outline plus louvers) comes back with its measured `extra` fraction on the row — LOOK at those first, they are the classic mislabel; background lines CROSSING the symbol and coincident duplicate ink never trip this. By default such placements still COUNT, because the contained-seed workflow below depends on supersets matching (seed a bare sub-shape, count the richer symbols that contain it, exclude what you don't mean). Pass `variant_guard: true` when your seed is the WHOLE symbol — grilles, drains, fixtures marqueed complete — and extra-ink placements demote to `withheld` as questions instead of counting; the guard stands down automatically when `exclude` counter-examples are in play, since supplying negatives is manual variant discrimination. The seed's own location is reported in `seed` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is…
| Name | Type | Req | Description |
|---|---|---|---|
| commit | boolean | – | Commit every MATCH center as one EA count marker (withheld placements never commit) |
| commit_seed | boolean | – | Sheet scope + commit only (#296): also commit the SEED instance — in sheet scope the seed is almost always installed work, and a count that excludes it bids one short. Joins the same one-undo-step ba… |
| condition | string | – | Finish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true |
| exclude | array | – | Counter-examples: rects around instances you do NOT mean, same gesture as seed_rect — 'count the triangles, not the keynote ones'. Marquee the LOOKALIKE ITSELF (the flush-floor variant with its box,… |
| luminance_tolerance | integer | – | Stroke-luminance gate, 0–254 (#260): a sheet segment only answers for a seed segment when their stroke luminances (Rec. 709, 0 black – 255 white) are within this. For flattened exports where a black… |
| mirror | boolean | – | Also match mirrored placements |
| rotations | boolean | – | Also match 90/180/270-rotated placements |
| scope | string | – | "sheet" = this sheet only; "set" = every PLAN-role sheet in the working set (needs a text layer for the sheet graph; non-plan sheets are excluded and disclosed) |
| seed_rect | array | yes | Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px — tight: segments fully inside define the symbol |
| sheet | string | yes | The sheet the seed rect sits on — in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted) |
| tolerance_px | number | – | Endpoint match tolerance in image px (default 2 — CAD jitter, not drift) |
| variant_guard | boolean | – | Whole-symbol mode: demote richer-variant placements (>30% extra linework inside the footprint) to withheld instead of counting them with an `extra` disclosure. Use when the seed is a COMPLETE symbol… |
| Name | Type | Req | Description |
|---|---|---|---|
| candidates | object | – | Sheet scope only — set scope accounts per sheet in sheets[] |
| commit_refused | string | – | commit mode (#376): present when the seed was too small and too common to commit on shape alone — fewer than 40 segments of seed linework and more than 50 placements cleared the bar. NOTHING was comm… |
| committed | integer | – | commit mode: count shapes committed — one per match (0 when commit_refused is present) |
| complete | boolean | yes | True when every proposed placement was scored (every swept sheet, in set scope) and the count is a total. FALSE MEANS THE COUNT IS A FLOOR — acknowledge it before trusting found (#261) |
| condition | string | – | commit mode: the finish tag the markers counted under |
| ea_total | number | – | commit mode: the condition's total EA after this call |
| found | integer | yes | Placements that cleared the commit bar — across every swept sheet in set scope |
| lum_gate | object | – | Sheet scope only. The stated stroke-luminance gate's accounting (#260): the tolerance, the seed's own luminance band, and every placement the geometry would have committed that the pen pulled under t… |
| matches | array | – | Sheet scope only. Deterministic reading order (y, then x). The seed's own location is never listed here |
| negatives | array | – | What each `exclude` rect was read as, in the order you passed them (#259) |
| note | string | – | – |
| rejected | array | – | Sheet scope only. Placements the geometry accepted and a counter-example refused (#259) — NEVER counted in found, and never silent: each says which negative did it and what it saw. Reinstate one by h… |
| rejected_total | integer | – | Set scope: placements counter-examples rejected across every swept sheet |
| scope | string | yes | "sheet" = the swept sheet alone (matches/withheld/candidates at top level); "set" = every PLAN-role sheet in the working set (per-sheet results in sheets[], exclusions in skipped[]) |
| seed | object | yes | – |
| seed_committed | boolean | – | Present when commit_seed: true minted the seed instance into the batch (#296) — ea_total then includes it |
| shape_ids | array | – | – |
| sheets | array | – | Set scope only: one entry per swept PLAN-role sheet, load order |
| skipped | array | – | Set scope only: every sheet excluded from counting, with role and reason — including the seed's own sheet when it is not a plan |
| warning | string | – | Present when the work cap dropped candidates — what a tighter seed rect would recover |
| withheld | array | – | Sheet scope only. Near-matches in the [0.75, 0.92) band — reported with a reason, NEVER committed. A withheld placement is a question you can answer with view_sheet; a hidden one is a miscount |
No examples provided.
takeoff_summary ~109
Per-condition totals (floor/wall/border SF, LF, EA, SY, with and without waste) plus grand totals — the Report's numbers, computed by the same rules. Numbers only: the deliverable that SHOWS the work on the drawings is export_marked_pdf. 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.
Input schema present but exposes no named parameters.
| Name | Type | Req | Description |
|---|---|---|---|
| conditions | array | yes | – |
| proposals | array | – | The proposal ledger (#365): per batch, how many shapes are still pending, how many the estimator accepted, whether it was withdrawn, and which batch new commits attach to (current). Present only when… |
| proposed_condition_edits | array | – | Pending condition-edit diffs (#365) beside the current knobs. The rows above are the CURRENT values — nothing changes until the estimator accepts. Present only when any are pending |
| scale_unconfirmed | array | – | Sheets whose scale is agent-set and no human has confirmed — these totals stand on an unverified scale; verify against a stated dimension or confirm in the canvas |
| shared_floor_sf | number | yes | Scope collision (#366): floor claimed by more than one shape across the whole takeoff, counted once per cell (Σ areas − union), in SF through each sheet's scale. Has to read 0 before a total means an… |
| shared_floor_unmeasured | array | – | Floor shapes the collision check could not measure (unscaled sheet, degenerate ring) — left OUT of shared_floor_sf rather than counted as zero; present only when any |
| totals | object | yes | – |
No examples provided.
undo_last ~217
Step back over your OWN last n mutations, newest first — a committed measure_polygon, an edit_shape, a delete_shape, an edit_materials call, an edit_condition call, or an RFI verb (create_rfi / resolve_rfi / delete_rfi). Each step is reversed exactly (a commit is removed, an edit is restored verbatim, a delete is re-inserted where it was, a materials edit's whole array is restored, a condition edit's waste/multiplier pair is restored), so this restores state rather than approximating it. Reads are never journaled, so n counts gestures that changed something, not tool calls you made. Use it when a sweep committed against the wrong condition or a batch went in on the wrong sheet — one call instead of N deletes. Scope: this session's own history only. It is not the browser canvas's undo stack, and load_plan clears it along with the shapes it refers to.
| Name | Type | Req | Description |
|---|---|---|---|
| n | integer | – | How many steps to reverse (1–100) |
| Name | Type | Req | Description |
|---|---|---|---|
| note | string | – | – |
| remaining | integer | yes | Steps still available to undo |
| shape_count | integer | yes | Committed shapes after the undo |
| steps | array | yes | Newest first |
| undone | integer | yes | Steps actually reversed |
No examples provided.
view_sheet ~771
SEE the sheet — render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (≤2000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel — a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px — the same space as every other tool — so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix × (region_x1 − region_x0) / img_w (same for y), and those coordinates go straight into measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) — render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in — thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot — "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". marks (#297) burns DISCLOSURE layers into the render, so what a reply names, the picture shows: pass the coordinate lists a tool disclosed — question: withheld placements (orange ?-circles), struck: rejections a counter-example or luminance gate refused (magenta struck ×), ring: reference points like the sweep's own seed (violet double ring). The colors sit delibera…
| Name | Type | Req | Description |
|---|---|---|---|
| grid | string | – | Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25" |
| marks | object | – | Disclosure marks to burn into the render (#297): what the reply names, the picture shows. Coordinates in image px |
| overlay | boolean | – | Burn committed shapes into the render (solid = human-affirmed, dashed = unreviewed) |
| px | integer | – | Long-side pixel budget of the returned image (default 1400) — small region + high px = readable dimension strings |
| region | object | – | Crop rect in image px (origin top-left, y down); omit for the full sheet |
| sheet | string | yes | – |
No output schema declared.
No examples provided.
withdraw_condition_edit ~52
Drop a pending condition-edit proposal (#365) without touching the condition. undo_last re-seats it.
| Name | Type | Req | Description |
|---|---|---|---|
| proposal_id | string | yes | From propose_condition_edit, or takeoff_summary's proposed_condition_edits |
| Name | Type | Req | Description |
|---|---|---|---|
| condition | string | yes | – |
| proposal_id | string | yes | – |
| withdrawn | boolean | yes | – |
No examples provided.
withdraw_proposal ~115
Take a proposal back (#365): every still-pending shape in the batch is removed in ONE journal step, the record stays marked withdrawn (its label is history the estimator may still read), and new commits stop attaching to it. Shapes the estimator already accepted are ink and stay — the reply counts them. This is the honest exit for "that batch was wrong" — one call instead of N delete_shape calls, and undo_last restores the whole batch.
| Name | Type | Req | Description |
|---|---|---|---|
| proposal_id | string | yes | The batch, from propose_takeoff |
| Name | Type | Req | Description |
|---|---|---|---|
| accepted_kept | integer | yes | Shapes from the batch the estimator had accepted — untouched |
| label | string | yes | – |
| note | string | yes | – |
| proposal_id | string | yes | – |
| withdrawn | integer | yes | Pending shapes removed |
No examples provided.
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 93 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 20 September 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 53 tools: load_plan, sheet_info, set_scale, propose_takeoff, measure_polygon, and 48 more. Their descriptions and schemas cost roughly 18,527 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 20 September 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 Apache-2.0 licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.