opentakeoff-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # OpenTakeoff MCP server
2
+
3
+ ## Run it in 60 seconds (npx)
4
+
5
+ No clone, no build — point your MCP client at the published package:
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "opentakeoff": {
11
+ "command": "npx",
12
+ "args": ["-y", "opentakeoff-mcp"]
13
+ }
14
+ }
15
+ }
16
+ ```
17
+
18
+ Works with Claude Code (`claude mcp add opentakeoff -- npx -y opentakeoff-mcp`), Claude Desktop, Cursor, or any stdio MCP client. Node 20+.
19
+
20
+
21
+ The takeoff engine — One-Click Area, the scale model, conditions, totals — on
22
+ **stdio for your MCP client**. An agent can open a plan, read the title block,
23
+ set the scale, click rooms, and hand back the same takeoff payload the browser
24
+ app autosaves. Same engine, same math: the server imports
25
+ `web/src/lib/{oneclick,sheets,geometry,totals}` directly, so a shape committed
26
+ here is field-identical to one committed on the canvas.
27
+
28
+ ## Quickstart
29
+
30
+ Both `web/` and `mcp/` need their dependencies (the engine's pdf.js lives in
31
+ `web/node_modules`):
32
+
33
+ ```bash
34
+ cd web && npm install
35
+ cd ../mcp && npm install
36
+ node --import tsx server.ts # speaks MCP on stdio
37
+ ```
38
+
39
+ Then register it with your MCP client (any stdio MCP client works):
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "opentakeoff": {
45
+ "command": "node",
46
+ "args": ["--import", "tsx", "/absolute/path/to/opentakeoff/mcp/server.ts"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Point `command` at `node` directly, as above — **never `npm start` in a client
53
+ config**: npm prints its banner to stdout, and stdout is the MCP wire. (Same
54
+ reason the server redirects `console.log` to stderr before pdf.js loads —
55
+ see `src/hush.ts`.)
56
+
57
+ `tsx` is a runtime dependency, not a build tool: the engine is imported
58
+ straight from `web/src/lib` as TypeScript, so plain `node` can't run it.
59
+
60
+ ## Tools
61
+
62
+ | Tool | What it does |
63
+ |---|---|
64
+ | `load_plan` | Open a plan PDF from disk. Replaces the whole session (old doc, scales, conditions, shapes all cleared). Returns per-sheet dims, title-block `sheet_number`, and the detected drawn scale where present. |
65
+ | `sheet_info` | One sheet's dims, vector segment count, scale status, detected suggestion, committed shape count. |
66
+ | `set_scale` | Set a sheet's scale — exactly one of `label`, `upp`, `calibrate {p1, p2, feet}`, `use_detected`. |
67
+ | `one_click` | One-Click Area at (x, y): flood fill bounded by the plan linework, traced, vertices snapped. Pass `condition` to commit; `role: "deduct"` subtracts. |
68
+ | `measure_polygon` | Area + perimeter of a polygon you supply (min 3 verts). Requires scale. |
69
+ | `measure_line` | Length of an open polyline (min 2 points). Requires scale. |
70
+ | `takeoff_summary` | Per-condition totals + grand totals, computed by the Report's rules. |
71
+ | `export_takeoff` | The full `opentakeoff.takeoff_canvas.v1` payload — exactly what the app autosaves. Inline, and to disk with `path`. |
72
+ | `delete_shape` | Remove a committed shape by id. |
73
+ | `read_sheet_text` | Positioned page text (image px), optionally restricted to a region — title blocks, room labels, finish schedules. |
74
+
75
+ Every reply is one compact JSON text item. Failures come back as
76
+ `isError: true` with `{"error": "..."}` — never a dropped connection.
77
+
78
+ ## The coordinate contract
79
+
80
+ All coordinates are **image pixels at render scale 2.0**: PDF points × 2,
81
+ origin **top-left**, y **down**. This is the browser canvas's native space, so
82
+ coordinates round-trip 1:1 with the app. Every sheet payload carries its dims
83
+ in both px and pt; text positions from `read_sheet_text` are in the same
84
+ space, which makes them usable directly as click targets.
85
+
86
+ ## Scale rules
87
+
88
+ - A detected scale is a **suggestion** — it is never applied automatically.
89
+ Adopting it is always an explicit `set_scale { use_detected: true }`.
90
+ - `measure_polygon` and `measure_line` refuse without a scale:
91
+ `Set the scale for <sheet> first — use set_scale (detected: <label>).`
92
+ - `one_click` without a scale returns a **px-only preview**
93
+ (`area_px2`, `perimeter_px`) with a warning, and commits nothing.
94
+ - `upp` is real feet per image px at render scale 2.0, per sheet — the same
95
+ number the app stores as `units_per_px`.
96
+
97
+ ## A whole takeoff, end to end
98
+
99
+ The bundled demo plan, as a copy-pasteable session (this is also the shape of
100
+ `test/e2e.test.ts`):
101
+
102
+ ```
103
+ load_plan { "path": "/absolute/path/to/opentakeoff/demo/sample-plan.pdf" }
104
+ → sheet "sample-plan.pdf", 2448×1584 px, sheet_number "A-101",
105
+ detected_scale "1/4\" = 1'-0\""
106
+ read_sheet_text { "sheet": "sample-plan.pdf", "region": { "x0": 1468, "y0": 871, "x1": 2448, "y1": 1584 } }
107
+ → the title block: A-101, SCALE: 1/4" = 1'-0"
108
+ set_scale { "sheet": "sample-plan.pdf", "use_detected": true }
109
+ one_click { "sheet": "sample-plan.pdf", "x": 600, "y": 1084, "condition": "CPT-1" } → ~438 SF
110
+ one_click { "sheet": "sample-plan.pdf", "x": 1640, "y": 1084, "condition": "CPT-1" } → ~438 SF
111
+ one_click { "sheet": "sample-plan.pdf", "x": 600, "y": 464, "condition": "CPT-1" } → ~438 SF
112
+ one_click { "sheet": "sample-plan.pdf", "x": 1600, "y": 464, "condition": "CPT-1" } → ~438 SF
113
+ takeoff_summary {} → CPT-1, 4 shapes, ~1752 SF
114
+ export_takeoff { "path": "/tmp/takeoff.json" } → the app's save payload
115
+ ```
116
+
117
+ Sheet keys follow the app's codec: page 1 is the bare file name
118
+ (`plan.pdf`), pages 2+ are `plan.pdf#2`. Tools also accept the title-block
119
+ sheet number (`A-101`) wherever a sheet is named.
120
+
121
+ ## Limits (v1)
122
+
123
+ - **Vector + text sheets only.** A scanned sheet has no vector linework, so
124
+ `one_click` reports it plainly; a raster fallback is a planned seam
125
+ (`src/session.ts`, `ensureMask`), not yet built.
126
+ - One document per session; `load_plan` replaces it.
127
+ - The takeoff lives in memory. `export_takeoff` is the way out — and its
128
+ payload is exactly what the app persists, so nothing is lost in translation.
129
+
130
+ ## Tests
131
+
132
+ ```bash
133
+ npm run typecheck
134
+ npm test # session + tool-layer + e2e, against demo/sample-plan.pdf
135
+ ```