Public leaderboard

Public assessment

Kentucky-ai/opentakeoff (opentakeoff-mcp)

opentakeoff-mcp · v0.9.31 · scanned

What changed in the harness

Selection accuracy 100→100, token cost up 2%, unconfirmed writes 0%→0%.

Category breakdown

Where the score comes from.

Earned points across the four signals Gradable measures. Safety and Legibility are scored out of 30; Economics and Discoverability are scored out of 20.

01Safety

0.0 / 30

0.0 out of 30
02Legibility

24.1 / 30

24.1 out of 30
03Economics

4.0 / 20

4.0 out of 20
04Discoverability

14.1 / 20

14.1 out of 20

Highest-impact fix

Estimated gain +30 points

Add explicit identity and permission preflight tools

Expose machine-readable principal/tenant confirmation and a non-mutating permission check so agents can verify both before destructive actions.

Description evidence

Defects and rewrites.

12 defects found across the exposed tool descriptions. Suggested rewrites make purpose, inputs, boundaries, and returns easier for an agent to understand.

Tool Defect types Suggested rewrite
set_scale
params_unexplained
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. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") whose scale is being set. Returns the sheet's resulting upp (real feet per image px at render scale 2.0), the source that set it (label | upp | calibrate | detected), the standard scale label when set by label or the detected note, and a warning when the sheet carries several distinct scale notes. 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.
one_click
params_unexplained
One-Click Area: click inside a room (image px) and the plan's vector linework bounds it — the sealed flood engine (RFC #60), contour trace, vertices snapped to true PDF endpoints. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") being clicked on; `x` and `y` are the click point in image px (origin top-left, y down) — the seed inside the room to flood; `role` (default "floor_area") sets what an eventually committed shape does — "deduct" makes it subtract instead of add. The engine's arguments are FEET-TRUE through the sheet's scale, exactly the canvas's: gap sealing bridges up to a door-width opening (disclosed as gap_sealed_px — that much boundary is synthetic), door-swing wedges annex the swing a doorway sweeps (door_wedges), and the minimum-passage rule keeps sub-half-foot slits from conjoining two rooms (min_pass_px/min_pass_delta). Every trace carries the engine's own account of itself: confidence (0..1, with confidence_factors naming what deducted) — a review PRIORITIZER, never a verification. 1.0 means every signal ran clean, not that the trace is right; a LOW confidence is a view_sheet {overlay: true} audit prompt, not a fact to bid from — put eyes on the flagged edge before the total means anything. SCANNED sheets work too (#154): where vectors can't bound the room (an image-only scan, or a scan wrapper whose only linework is the title block), the flood falls back automatically to the sheet's rendered pixels — same engine the canvas uses — and the reply plus the committed shape's origin carry raster_traced: true so a pixel-bounded ring is never mistaken for a vector-snapped one. Vector always wins where it works; a raster ring's corners are unsnapped, so audit it with view_sheet {overlay: true} before trusting the total. With the sheet's scale set, returns area_sf / perimeter_lf; pass condition (a finish tag, e.g. "CPT-1") to commit the traced shape to the takeoff — the full engine account rides the committed shape's origin, so the export tells the truth about how each shape was made. Without a scale it returns px-only quantities with a warning and commits nothing (the engine also degrades to its scale-blind fallbacks — a weaker measurement, one more reason set_scale comes first). After committing, LOOK at what landed — view_sheet {overlay: true} — and fix an overshot ring with edit_shape before trusting any total. 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.
detect_rooms
params_unexplained
Batch room detection: reads every room-number label off the sheet's text layer (e.g. "134", "OFFICE 101") and runs One-Click at each — one call instead of read_sheet_text + reasoning + N one_click calls. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") whose labels to sweep; `role` (default "floor_area") sets what the committed rooms do — "deduct" makes each committed room subtract rather than add. An OCR'd scan (text layer, no vector linework) floods the rendered pixels instead (#154), disclosed per room and on origin as raster_traced. A seed is only reported as a room once it survives three gates, and everything skipped is counted and reasoned in `withheld` — never dropped silently, because a room the tool tells you it skipped is a question you can ask, while one it hides is a hole in a bid. The gates: a flood that leaked or landed in dense linework never becomes a region; two labels flooding the SAME region commit once (the extra labels ride on `merged_labels` — double-counting an area is the worst failure an estimating tool has); and a flood that is enclosed and clean but smaller than min_area_sf is a room-number bubble, a door swing, or a wall cavity rather than a room. Every room floods through the SAME sealed engine a single one_click runs (RFC #60 — feet-true gap sealing, door-swing wedges, the minimum-passage rule), so a batch detection and a click at the same seed measure the same square footage; each room carries the engine's account of its own trace (confidence + confidence_factors, gap_sealed_px, door_wedges, min_pass_px/min_pass_delta), and the same account rides origin on everything committed. Confidence is a review prioritizer, never a verification — a low-confidence room is a view_sheet {overlay: true} audit prompt, not a fact to bid from. With the sheet's scale set, returns area_sf/perimeter_lf per room. TO COMMIT, choose the honest source of the finish tag: assign_from_schedule: true routes every room through its OWN room-finish schedule row and commits each under the FLOOR finish that row states — when a schedule exists in the set, THIS is the default move, because one agent-chosen tag across N rooms flattens real finish variety into a wrong bid; condition commits every room under that one stated tag (only right when the rooms genuinely share it). Without a scale, returns px-only quantities per room and commits nothing — the plausibility floor needs real units, so it only applies once a scale is set. A batch commit is NOT finished until you have LOOKED at it: view_sheet {overlay: true}, audit every ring against the walls, fix misses with edit_shape / delete_shape — before the totals mean anything. 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.
measure_polygon
params_unexplained
Measure a closed polygon you supply — area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") the polygon is drawn on; `verts` is the closed polygon as [[x, y], ...] in image px, min 3 vertices, origin top-left, y down; `condition` is a finish tag (minted on first use) to commit the measured polygon under — omit it to measure without committing; `role` (default "floor_area") sets what the committed shape does — "deduct" subtracts its area from the takeoff. Returns the polygon's area_sf and perimeter_lf, its vertex count, and the committed shape's id when condition was passed; a mixed-scale warning can appear when a scale note disagreeing with the sheet's sits in the measured region — verify before trusting these numbers. 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.
measure_line
params_unexplained
Measure an open polyline — length_lf at the sheet's scale. Requires the scale to be set. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") the polyline is drawn on; `pts` is the open polyline as [[x, y], ...] in image px, min 2 points, origin top-left, y down; `condition` is a finish tag (minted on first use) to commit the measured line as a linear shape (base, transitions, feature strips) — omit it to measure without committing. Returns length_lf, the point count, and the committed shape's id when condition was passed. 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.
measure_surface
params_unexplained
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 one_click and 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. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") the wall run is drawn on. Returns the condition, the height the shape was quantified at, the traced run's open length, the committed wall SF (length × height), the point count, and the committed shape id. 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.
place_count
params_unexplained
Count markers — EA (#146): one point, one each. Thresholds, stair nosings, floor boxes, entrance mats — the scale-free quantity family. Commits one count shape per point (computed {count: 1}, exactly the canvas's Count tool), NO scale required, and the whole call is ONE undo step like a detect_rooms sweep. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") the markers are placed on; `points` carries one [x, y] marker position per count (image px, origin top-left, y down), each committing its own count shape; `condition` is the finish tag (minted on first use) to commit the markers under, e.g. 'TR-1'. Returns how many count shapes were committed, their shape ids, and the condition's total EA after the call. takeoff_summary reports them as ea; the marked set draws each marker. 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.
delete_shape
params_unexplained
Remove a committed shape by its id. `shape_id` is the id returned when the shape was committed (also listed by list_shapes). Returns the removed shape's id and the count of committed shapes remaining. 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.
sheet_context
params_unexplained
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. `sheet` is the sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101") to inspect. 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.
read_sheet_text
params_unexplained
The sheet's text with positions — items [{str, x, y}] in image px plus the joined text. `sheet` names the sheet to read (name or number, as sheet_info or the graph payloads report it). `region` optionally restricts the read to an image-px crop rect {x0, y0, x1, y1} (origin top-left, y down); omit it for the full sheet. Use it to read title blocks, room labels, finish schedules, and scale notes. Returns the sheet name, the positioned text items, and the text joined with spaces. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down. Sheet payloads carry dims in both px and pt.
find_text
params_unexplained
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). `sheet` names the sheet to search (name or number, as sheet_info reports it). 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's center feeds straight into one_click as the seed — the locate-then-trace workflow: find_text the room number, one_click at (or just past) its center. 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.
view_sheet
params_unexplained
SEE the sheet — `sheet` names the page to render (name or number, as sheet_info reports it): 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 one_click, 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". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. 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.

Selection evidence

Confusable tool pairs.

12 pairs where similar names or overlapping descriptions may send an agent toward the wrong tool.

Tool A Tool B Confidence Why they collide
measure_polygon measure_line medium A task like 'measure the run/perimeter of this space' is ambiguous: measure_polygon returns perimeter_lf of a closed polygon while measure_line returns length_lf of an open polyline, and both descriptions open with 'Measure...' with no field telling the agent which closedness the user means.
measure_line measure_surface high Both trace an open polyline along a wall; measure_line yields length_lf, measure_surface yields wall SF (LF × height). A bare 'measure this wall run' gives no cue whether the user wants a length or a wall area, so either tool looks correct from the definitions.
measure_polygon measure_surface medium Both output SF and share the words area/measure/px/sf. A task like 'measure the surface area of this region' plausibly lands on measure_surface by its name even though floor regions need measure_polygon and surface-area tracing is wall-systems-only.
delete_shape delete_verdict low Both are the only delete_* tools and take an id. A request like 'delete the mark/diamond I put on room 101' could route to delete_shape unless the agent notices delete_verdict is the approval-family remover; the definitions do disambiguate, so confabulation is weak.
derive_base derive_transitions low Both are finish-derivation tools that mint linear shapes under a condition. A task like 'derive the base at the transitions' conflates the wall base (derive_base) with finish-to-finish transitions (derive_transitions), leaving the intended tool unclear.
edit_materials edit_condition low Both edit per-condition quantity config on an existing finish tag: edit_materials adds coverage rows that turn totals into order quantities, while edit_condition sets waste/multiplier/height knobs. A task to 'set up the condition's quantities/materials' gives no clear owner.
export_takeoff export_report medium A task like 'export the takeoff for pricing' is genuinely ambiguous: export_takeoff is the raw JSON annotations payload with no computed quantities, while export_report is the computed Report the definition says to consume when numbers leave for pricing.
export_takeoff export_marked_pdf medium export_marked_pdf is billed as 'the deliverable of every takeoff' yet export_takeoff shares the literal name and is importable by the app. A request to 'export/finish the takeoff' can land on the raw canvas payload instead of the marked planset PDF.
export_report export_marked_pdf medium Both are final exports presented as two halves of one deliverable ('numbers for pricing' vs 'the evidence on the drawings'). A bare 'export the takeoff report/PDF' request gives the agent no signal which half is intended.
sheet_context sheet_graph medium A task like 'understand what's on sheet A-101' fits both: sheet_context exposes that region's vector/hatch/text structure, while sheet_graph returns the sheet's role, schedule tables, and room tags. Neither description earmarks a whole-sheet 'what is this?' question, so an agent can pick wrong.
sheet_info sheet_context medium sheet_info bills itself as 'Sheet detail' (dims, segment count, vector-linework, scale status) while sheet_context is 'The sheet's STRUCTURE in one call'. A vague 'get me the info/structure of this sheet' can hit the wrong member.
symbol_sweep sweep_schedule_row medium Both sweep every PLAN-role sheet for a repeated marker and commit EA count markers under a condition; the only difference is seeding (marquee one instance vs give a schedule row's tag). A task like 'count every T1/transition/drain marker' is ambiguous between them.

Compare the field

One score is useful.
The evidence makes it actionable.

Back to the leaderboard