OpenTakeoff

Construction quantity takeoff and estimates from plan PDFs: scale, measure, count, price and export

Official · Kentucky AI TypeScript Local · stdio DesignData and analytics

70 popularity score ★ 767 ↓ 1.8k/wk

Connect your client

Local (npm)

Add in one click

Opens the app, which asks before it adds the server.

Or add it by command or config

Claude CodeTerminal
claude mcp add opentakeoff -- npx -y opentakeoff-mcp
Claude Desktopclaude_desktop_config.json
{
  "mcpServers": {
    "opentakeoff": {
      "command": "npx",
      "args": [
        "-y",
        "opentakeoff-mcp"
      ]
    }
  }
}
Cursor~/.cursor/mcp.json
{
  "mcpServers": {
    "opentakeoff": {
      "command": "npx",
      "args": [
        "-y",
        "opentakeoff-mcp"
      ]
    }
  }
}
VS Code.vscode/mcp.json
{
  "servers": {
    "opentakeoff": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "-y",
        "opentakeoff-mcp"
      ]
    }
  }
}
Codex~/.codex/config.toml
[mcp_servers.opentakeoff]
command = "npx"
args = ["-y", "opentakeoff-mcp"]
Gemini CLITerminal
gemini mcp add --scope user opentakeoff npx -- -y opentakeoff-mcp
Local (from source)

Add in one click

Opens the app, which asks before it adds the server. Then replace each <NAME> in its settings with your value.

Or add it by command or config

FirstTerminal
git clone https://github.com/Kentucky-ai/opentakeoff.git
cd opentakeoff/web && npm install
cd ../mcp && npm install
Claude CodeTerminal
claude mcp add opentakeoff -- node --import tsx '<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts'
Claude Desktopclaude_desktop_config.json
{
  "mcpServers": {
    "opentakeoff": {
      "command": "node",
      "args": [
        "--import",
        "tsx",
        "<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts"
      ]
    }
  }
}
Cursor~/.cursor/mcp.json
{
  "mcpServers": {
    "opentakeoff": {
      "command": "node",
      "args": [
        "--import",
        "tsx",
        "<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts"
      ]
    }
  }
}
VS Code.vscode/mcp.json
{
  "servers": {
    "opentakeoff": {
      "type": "stdio",
      "command": "node",
      "args": [
        "--import",
        "tsx",
        "<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts"
      ]
    }
  }
}
Codex~/.codex/config.toml
[mcp_servers.opentakeoff]
command = "node"
args = ["--import", "tsx", "<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts"]
Gemini CLITerminal
gemini mcp add --scope user opentakeoff node -- --import tsx '<ABSOLUTE_PATH_TO_CLONE>/mcp/server.ts'

Settings 3

OPENTAKEOFF_ONE_CLICK Optional: Set to 1 to register the gated one_click and detect_rooms tools. Default unset (off). For one_click, detect_rooms.
OPENTAKEOFF_MCP_STAGED_TOOLS Optional: Set to 1 to start with the setup tools only and unlock the rest through open_tool_stage. Default unset (all tools listed). For open_tool_stage.
OPENTAKEOFF_MCP_TRACE Optional: Set to 1 to log one JSON line per tool call to stderr. Default unset (no trace).

What it does

OpenTakeoff is an open-source construction takeoff engine from Kentucky-ai, and opentakeoff-mcp is its local stdio MCP server. An agent opens plan PDFs from disk, reads title blocks and schedules, sets each sheet's scale, and commits area, length, wall-surface and count measurements. It can also sweep a sheet for every copy of a drawn symbol, derive base runs and finish transitions from rooms already measured, and raise RFIs where the drawings contradict themselves. When it's done it can hand off a JSON takeoff, a computed report with a materials buy list, a marked-up PDF planset or a DXF file.

Releases after 0.9.98 also price the work. You enter material and labor rates per condition plus the markups, and the server writes the estimate as an Excel workbook and a bid proposal PDF. It never refuses an unfinished estimate. Instead the reply says which prices are still missing, and every proposal page carries an OpenTakeoff watermark.

The server shares its quantity math and takeoff format with the OpenTakeoff browser canvas. Work an agent commits shows up there as unreviewed 'pencil' proposals, scales it sets land unconfirmed, and a person accepts or rejects them in the canvas. The takeoff lives in memory for the session, so the export tools are how you keep it.

A default build registers 58 tools, and the main README now lists all 58 by stage. What it no longer names are the two gated room tools. The code still holds one_click and detect_rooms but skips registering them while the room-flood engine is re-checked (mcp/src/gate.ts), and setting OPENTAKEOFF_ONE_CLICK=1 brings both back. With OPENTAKEOFF_MCP_STAGED_TOOLS=1 the server starts with just the setup tools plus open_tool_stage, which opens the other tool groups on request. That opener is counted outside the 58.

What it needs

RuntimeNode.js 20+
Runs onmacOS, Windows, Linux
Also
  • Plan sets as PDF files on the local disk

Rendering (view_sheet, the sheet-image resource, raster tracing of scans) needs the native @napi-rs/canvas package, an optional dependency: npm installs it where a build exists for your platform, and the Claude Desktop .mcpb bundle leaves it out. The source way needs both the web and mcp folders installed, because the engine is imported from web/src/lib through tsx. No account or API key.

Limits

  • Early release one_click and detect_rooms are not registered unless OPENTAKEOFF_ONE_CLICK=1 is set. Until then rooms are traced by hand with measure_polygon.
  • Limit Scales set over MCP stay unconfirmed, and committed shapes stay pending, until a person accepts them in the canvas.
  • Not covered Scanned sheets with no text layer give sheet_graph and resolve_tag nothing to read.
  • Not covered There is no tool to join sheets split at a match line. Stitching is done in the canvas, and import_takeoff drops the stitches field.
  • Limit load_plan replaces the whole session unless merge: true is passed.
  • Limit The takeoff is held in memory and is lost when the server stops unless it was exported.
  • Limit Staged tools need a client that honours tools/list_changed.
  • Limit export_estimate and export_proposal still write when prices are missing, and the reply lists what is unpriced. Every proposal page is watermarked OpenTakeoff.
  • Limit The Docker image is built from the repository. No published image is named.

one_click and detect_rooms are not registered unless OPENTAKEOFF_ONE_CLICK=1 is set; until then rooms are traced with measure_polygon. Scales set over MCP stay unconfirmed until a person confirms them in the canvas. Scanned sheets with no text layer give sheet_graph, resolve_tag and detect_rooms nothing to read. Joining sheets split at a match line is canvas-only, and import_takeoff drops the app's stitches field, so a stitched takeoff loses its stitches on a round trip. load_plan replaces the session unless merge: true is passed. Staged tools need a client that honours tools/list_changed. The Docker image is built from the repository; no published image is named.

Who it suits

Estimators and preconstruction teams who want an agent to do first-pass flooring and finish takeoffs on local PDF plan sets, then price them into a draft estimate and bid proposal, with a person reviewing the result in the OpenTakeoff canvas before it counts. It runs entirely on your machine with no account, so plans stay local. It is not a fit if you need automatic room detection today (that path is gated), stitched multi-sheet floors over MCP, a proposal without the OpenTakeoff watermark, or a hosted endpoint. Agents working without a human reviewer will leave every measurement and scale unconfirmed.

Tools 60 of 61

15 only read, 39 change things, 6 can delete or overwrite. A ? marks an optional parameter.

load_plan changes
Open a plan PDF from disk, replacing the session or adding to it with mergepath: stringmerge?: boolean
sheet_info
Report one sheet's size, vector count, scale status and shape countsheet: string
set_scale changes
Set a sheet's scale from a label, units, a calibration line or the detected scalesheet: stringlabel?: stringupp?: numbercalibrate?: objectuse_detected?: boolean
one_click changes
Trace a room's area from one seed point (only with OPENTAKEOFF_ONE_CLICK=1)sheet: stringx: numbery: numbercondition?: stringrole?: enumreturn_verts?: booleansensitivity?: numberlayers?: object
detect_rooms changes
Trace every labelled room on a sheet (only with OPENTAKEOFF_ONE_CLICK=1)sheet: stringcondition?: stringassign_from_schedule?: booleanrole?: enumreturn_verts?: booleanmin_area_sf?: numbersensitivity?: numberlayers?: object
propose_takeoff changes
Open a named batch so committed shapes go to review as one proposallabel: stringrationale: string
measure_polygon changes
Commit an area and perimeter from supplied polygon pointssheet: stringverts: arraycondition?: stringrole?: enumarc_through?: array
cut_out changes
Cut a hole in a committed floor shape and recompute its quantitiesparent_shape_id: stringverts: array
measure_line changes
Commit the length of an open polylinesheet: stringpts: arraycondition?: stringrise_ft?: numberdrop_ft?: numberarc_through?: array
measure_surface changes
Commit wall square footage from a traced run times the condition's heightsheet: stringpts: arraycondition: stringheight_ft?: numberarc_through?: array
place_count changes
Place each-count markers at given pointssheet: stringpoints: arraycondition: string
symbol_sweep changes
Find every other placement of a symbol from one example on the sheetsheet: stringseed_rect: arraycondition?: stringcommit?: booleancommit_seed?: booleanscope?: enumrotations?: booleanmirror?: booleantolerance_px?: numbervariant_guard?: booleanexclude?: arrayluminance_tolerance?: number
sweep_schedule_row changes
Find and count a schedule row's mark across the plan sheetstag: stringcommit?: booleanrotations?: booleanmirror?: booleantolerance_px?: number
count_marks changes
Count device or equipment marks and optionally commit them as countsmarks?: arraycommit?: boolean
derive_base changes
Commit base runs around measured rooms, less stated door openingssource_condition: stringcondition: stringopenings?: array
derive_transitions changes
Commit transition strips where two finishes meetcondition_a: stringcondition_b: stringcondition: stringmax_gap_in?: numbermin_run_in?: number
takeoff_summary
Return per-condition and grand totals
export_takeoff deletes
Export the full takeoff JSON inline or to a filepath?: stringoverwrite?: boolean
export_dxf deletes
Write one sheet's shapes as a DXF drawing for CAD toolspath: stringsheet?: stringunits?: enumoverwrite?: boolean
export_report deletes
Export the computed report with quantities and materials buy listpath?: stringproject_name?: stringoverwrite?: boolean
set_pricing changes
Enter material and labor prices per unit for one condition and its supporting materialscondition: stringunit?: enummaterial?: numberlabor?: numbermaterials?: array
set_estimate_settings changes
Set tax, overhead and profit markups, proposal wording and client detailstax_pct?: numberoverhead_pct?: numberprofit_pct?: numberprofit_mode?: enumproposal?: objectclient?: object
estimate_summary
Return the priced estimate by condition, with what is still unpriced
export_estimate deletes
Write the priced estimate as an Excel workbook, beside the plan by defaultpath?: stringproject_name?: stringcompany_name?: stringoverwrite?: boolean
export_proposal deletes
Write a bid proposal PDF with scope, base bid and signature blockspath?: stringproject_name?: stringcompany?: objectoverwrite?: boolean
import_takeoff changes
Load a saved takeoff file and merge it into the sessionpath: string
apply_rules changes
Re-run the correction rules saved with the takeoffsheet?: string
export_marked_pdf deletes
Write a marked-up PDF planset with a legend coverpath?: stringproject_name?: stringoverwrite?: boolean
list_shapes
List committed shapes with ids, conditions, quantities and review statesheet?: stringcondition?: string
delete_shape changes
Remove a committed shape by idshape_id: string
scope_duplicates
List pairs of floor shapes from different conditions that overlapsheet?: stringmin_fraction?: number
scope_merge changes
Settle one overlap by trimming or deleting the losing shapeshape_a: stringshape_b: stringwinner?: string
revise_proposal changes
Replace every pending shape in a batch with a new setproposal_id: stringshapes: array
withdraw_proposal changes
Remove a batch's pending shapesproposal_id: string
sheet_context
Return a region's classified linework, text spans and hatch patterns in one readsheet: stringregion?: objectmin_len_px?: numbermax_segments?: number
get_sheet_vectors
Return the sheet's raw vector strokes as the engine sees themsheet: stringregion?: objectlimit?: numbercursor?: number
edit_shape changes
Change a committed shape's points, condition, role or room labelshape_id: stringverts?: arraycondition?: stringrole?: enumlabel?: stringrise_ft?: numberdrop_ft?: number
edit_materials changes
Add, remove or change supporting-material rows on a conditioncondition: stringadd?: arrayremove?: arraypatch?: array
edit_condition changes
Set a condition's waste, multiplier, height or roll setupcondition: stringwaste_pct?: numbermultiplier?: numberheight_ft?: numberrise_ft?: numberdrop_ft?: numberroll_setup?: any
propose_condition_edit changes
Propose a condition change that waits for the estimator to acceptcondition: stringfinish_tag?: stringwaste_pct?: numbermultiplier?: numberheight_ft?: numberrise_ft?: numberdrop_ft?: numberroll_setup?: anyrationale: string
withdraw_condition_edit changes
Drop a pending condition-change proposalproposal_id: string
duplicate_condition changes
Create a twin of a condition for the same finish in another areacondition: stringlabel: string
split_condition changes
Stop a twin condition inheriting material edits from its originalcondition: string
undo_last changes
Undo your own last mutationsn?: number
sheet_graph
Index the plan set: sheet roles, schedules, room tags, callouts and revision marks
resolve_tag
Follow a room tag to its finish schedule row and material definitionstag: string
find_schedule
Locate a room finish, material or equipment schedule tablekind: string
read_sheet_text
Read positioned text from a sheet or one region of itsheet: stringregion?: object
find_text
Find where a given string appears on a sheetsheet: stringq: stringregion?: objectlimit?: number
view_sheet
Render a sheet or crop to PNG, optionally with shapes and a scale grid drawn onsheet: stringregion?: objectpx?: numberoverlay?: booleangrid?: stringmarks?: object
annotate changes
Place a note, cloud, arrow, bubble or dimension on a sheetsheet: stringtype: enumtext?: stringcondition?: stringat?: arraytarget?: arrayrect?: arrayfrom?: arrayto?: arrayr?: number
list_annotations
List annotations and verdict marks with their linked conditionssheet?: stringcondition?: string
edit_annotation changes
Replace or clear an annotation's textannotation_id: stringtext: string
link_annotation changes
Attach an annotation to a condition or detach itannotation_id: stringcondition: string
mark_verdict changes
Place the agent's check mark on reviewed workshape_id?: stringsheet?: stringat?: arraytext?: string
delete_verdict changes
Remove an agent verdict mark by idverdict_id: string
create_rfi changes
Raise a request for information in the RFI registertitle: stringquestion: stringsheet: stringmarkup_ids?: array
list_rfis
List RFIs with status, sheet and linked markups
resolve_rfi changes
Record an answer to an open RFIrfi_id: stringanswer: string
delete_rfi changes
Withdraw an RFI, keeping its number reservedrfi_id: string

Resources 7

takeoff://wiki
Index of the packaged OpenTakeoff knowledge pages
takeoff://wiki/{page}
One knowledge page: status, architecture, protocol, workflows, MCP routing
takeoff://protocol
Index of the draft Takeoff Protocol JSON schemas
takeoff://sheets
The sheets of the loaded plan set
takeoff://sheet/{page}
One sheet's metadata as JSON
takeoff://sheet/{page}/text
One sheet's text layer
takeoff://sheet/{page}/image
One sheet rendered as PNG

Similar servers