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 +135 -0
- package/dist/server-core.js +1217 -0
- package/dist/server.js +24 -0
- package/package.json +53 -0
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
|
+
```
|