opentakeoff-mcp 0.1.0 → 0.1.2

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 CHANGED
@@ -57,6 +57,16 @@ see `src/hush.ts`.)
57
57
  `tsx` is a runtime dependency, not a build tool: the engine is imported
58
58
  straight from `web/src/lib` as TypeScript, so plain `node` can't run it.
59
59
 
60
+ For tool-call debugging, opt into structured stderr tracing:
61
+
62
+ ```bash
63
+ OPENTAKEOFF_MCP_TRACE=1 node --import tsx server.ts
64
+ ```
65
+
66
+ Each tool call writes one JSON line to stderr with the tool name, duration,
67
+ sheet, result size, and error flag. The trace never writes to stdout and never
68
+ includes document text, shape vertices, or result payload content.
69
+
60
70
  ## Tools
61
71
 
62
72
  | Tool | What it does |
@@ -1114,25 +1114,50 @@ var Session = class {
1114
1114
 
1115
1115
  // src/tools.ts
1116
1116
  import { z } from "zod";
1117
+
1118
+ // src/trace.ts
1119
+ var TRACE_ENV = "OPENTAKEOFF_MCP_TRACE";
1120
+ function traceToolCall(tool, args, startedAt, reply) {
1121
+ if (process.env[TRACE_ENV] !== "1") return;
1122
+ const text = reply.content[0]?.text ?? "";
1123
+ const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
1124
+ const sheet = args && typeof args === "object" && "sheet" in args ? args.sheet : void 0;
1125
+ const event = {
1126
+ event: "opentakeoff_mcp_tool_call",
1127
+ tool,
1128
+ duration_ms: Math.round(durationMs * 100) / 100,
1129
+ sheet: typeof sheet === "string" ? sheet : null,
1130
+ result_size: text.length,
1131
+ is_error: reply.isError === true
1132
+ };
1133
+ process.stderr.write(`${JSON.stringify(event)}
1134
+ `);
1135
+ }
1136
+
1137
+ // src/tools.ts
1117
1138
  var COORDS = "Coordinates are image px at render scale 2.0: PDF pt \xD7 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.";
1118
1139
  var pointSchema = z.tuple([z.number(), z.number()]);
1119
1140
  var roleSchema = z.enum(["floor_area", "deduct"]).default("floor_area");
1120
- var run = (fn) => async (args) => {
1141
+ var run = (tool, fn) => async (args) => {
1142
+ const startedAt = process.hrtime.bigint();
1143
+ let reply;
1121
1144
  try {
1122
- return ok(await fn(args));
1145
+ reply = ok(await fn(args));
1123
1146
  } catch (e) {
1124
- return fail(e);
1147
+ reply = fail(e);
1125
1148
  }
1149
+ traceToolCall(tool, args, startedAt, reply);
1150
+ return reply;
1126
1151
  };
1127
1152
  function registerTools(server, session) {
1128
1153
  server.registerTool("load_plan", {
1129
1154
  description: `Open a plan PDF from disk and replace the whole session (previous document, scales, conditions, and shapes are cleared). Returns file, page_count, and one entry per sheet: dims, title-block sheet_number, and the detected drawn scale where present. ${COORDS}`,
1130
1155
  inputSchema: { path: z.string().describe("Path to a plan PDF on disk") }
1131
- }, run(({ path: path3 }) => session.loadPlan(path3)));
1156
+ }, run("load_plan", ({ path: path3 }) => session.loadPlan(path3)));
1132
1157
  server.registerTool("sheet_info", {
1133
1158
  description: `Sheet detail: dims (px and pt), vector segment count, whether the sheet has vector linework (one_click needs it), scale status, the detected scale suggestion, and this sheet's committed shape count. ${COORDS}`,
1134
1159
  inputSchema: { sheet: z.string().describe('Sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101")') }
1135
- }, run(({ sheet }) => session.sheetInfo(sheet)));
1160
+ }, run("sheet_info", ({ sheet }) => session.sheetInfo(sheet)));
1136
1161
  server.registerTool("set_scale", {
1137
1162
  description: `Set a sheet's scale \u2014 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 \u2014 setting it is always this explicit call. ${COORDS}`,
1138
1163
  inputSchema: {
@@ -1142,7 +1167,7 @@ function registerTools(server, session) {
1142
1167
  calibrate: z.object({ p1: pointSchema, p2: pointSchema, feet: z.number() }).optional().describe("Two points (image px) a known real distance apart, and that distance in feet"),
1143
1168
  use_detected: z.boolean().optional().describe("true = adopt the sheet's detected scale")
1144
1169
  }
1145
- }, run((a) => {
1170
+ }, run("set_scale", (a) => {
1146
1171
  const given = [a.label !== void 0, a.upp !== void 0, a.calibrate !== void 0, a.use_detected !== void 0].filter(Boolean).length;
1147
1172
  if (given !== 1) throw new UserError("Provide exactly one of: label, upp, calibrate, use_detected.");
1148
1173
  return session.setScale(a.sheet, a);
@@ -1157,7 +1182,7 @@ function registerTools(server, session) {
1157
1182
  role: roleSchema,
1158
1183
  return_verts: z.boolean().default(false).describe("Include the traced polygon's vertices (image px)")
1159
1184
  }
1160
- }, run((a) => session.oneClick(a.sheet, a.x, a.y, { condition: a.condition, role: a.role, returnVerts: a.return_verts })));
1185
+ }, run("one_click", (a) => session.oneClick(a.sheet, a.x, a.y, { condition: a.condition, role: a.role, returnVerts: a.return_verts })));
1161
1186
  server.registerTool("measure_polygon", {
1162
1187
  description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. ${COORDS}`,
1163
1188
  inputSchema: {
@@ -1166,7 +1191,7 @@ function registerTools(server, session) {
1166
1191
  condition: z.string().optional(),
1167
1192
  role: roleSchema
1168
1193
  }
1169
- }, run((a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role })));
1194
+ }, run("measure_polygon", (a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role })));
1170
1195
  server.registerTool("measure_line", {
1171
1196
  description: `Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips). ${COORDS}`,
1172
1197
  inputSchema: {
@@ -1174,15 +1199,15 @@ function registerTools(server, session) {
1174
1199
  pts: z.array(pointSchema).min(2),
1175
1200
  condition: z.string().optional()
1176
1201
  }
1177
- }, run((a) => session.measureLine(a.sheet, a.pts, { condition: a.condition })));
1202
+ }, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition })));
1178
1203
  server.registerTool("takeoff_summary", {
1179
1204
  description: `Per-condition totals (floor/wall/border SF, LF, EA, SY, with and without waste) plus grand totals \u2014 the Report's numbers, computed by the same rules. ${COORDS}`,
1180
1205
  inputSchema: {}
1181
- }, run(() => session.summary()));
1206
+ }, run("takeoff_summary", () => session.summary()));
1182
1207
  server.registerTool("export_takeoff", {
1183
1208
  description: `The full "opentakeoff.takeoff_canvas.v1" annotations payload \u2014 exactly what the app autosaves, importable by it. Returned inline; pass path to also write it to disk as JSON. ${COORDS}`,
1184
1209
  inputSchema: { path: z.string().optional().describe("File path to write the payload to") }
1185
- }, run(async ({ path: outPath }) => {
1210
+ }, run("export_takeoff", async ({ path: outPath }) => {
1186
1211
  const payload = session.exportPayload();
1187
1212
  if (outPath) {
1188
1213
  const { writeFile } = await import("node:fs/promises");
@@ -1193,19 +1218,75 @@ function registerTools(server, session) {
1193
1218
  server.registerTool("delete_shape", {
1194
1219
  description: `Remove a committed shape by the id returned when it was committed. ${COORDS}`,
1195
1220
  inputSchema: { shape_id: z.string() }
1196
- }, run(({ shape_id }) => session.deleteShape(shape_id)));
1221
+ }, run("delete_shape", ({ shape_id }) => session.deleteShape(shape_id)));
1197
1222
  server.registerTool("read_sheet_text", {
1198
1223
  description: `The sheet's text with positions \u2014 items [{str, x, y}] in image px plus the joined text. Optionally restrict to a region {x0, y0, x1, y1}. Use it to read title blocks, room labels, finish schedules, and scale notes. ${COORDS}`,
1199
1224
  inputSchema: {
1200
1225
  sheet: z.string(),
1201
1226
  region: z.object({ x0: z.number(), y0: z.number(), x1: z.number(), y1: z.number() }).optional()
1202
1227
  }
1203
- }, run((a) => session.readSheetText(a.sheet, a.region)));
1228
+ }, run("read_sheet_text", (a) => session.readSheetText(a.sheet, a.region)));
1204
1229
  }
1205
1230
 
1231
+ // package.json
1232
+ var package_default = {
1233
+ name: "opentakeoff-mcp",
1234
+ version: "0.1.2",
1235
+ mcpName: "io.github.kentucky-ai/opentakeoff",
1236
+ type: "module",
1237
+ description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
1238
+ license: "Apache-2.0",
1239
+ engines: {
1240
+ node: ">=20"
1241
+ },
1242
+ scripts: {
1243
+ dev: "node --import tsx server.ts",
1244
+ start: "node dist/server.js",
1245
+ build: "esbuild server.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/server-core.js && node scripts/finish-build.mjs",
1246
+ prepublishOnly: "npm run typecheck && npm test && npm run build",
1247
+ typecheck: "tsc --noEmit",
1248
+ test: "node --import tsx --test test/*.test.ts"
1249
+ },
1250
+ dependencies: {
1251
+ "@modelcontextprotocol/sdk": "^1.12.0",
1252
+ "pdfjs-dist": "^4.10.38",
1253
+ zod: "^3.24.1"
1254
+ },
1255
+ devDependencies: {
1256
+ "@types/node": "^22.19.21",
1257
+ typescript: "^5.7.2",
1258
+ tsx: "^4.19.2",
1259
+ esbuild: "^0.24.2"
1260
+ },
1261
+ bin: {
1262
+ "opentakeoff-mcp": "dist/server.js"
1263
+ },
1264
+ main: "dist/server.js",
1265
+ files: [
1266
+ "dist",
1267
+ "README.md"
1268
+ ],
1269
+ repository: {
1270
+ type: "git",
1271
+ url: "git+https://github.com/Kentucky-ai/opentakeoff.git",
1272
+ directory: "mcp"
1273
+ },
1274
+ homepage: "https://github.com/Kentucky-ai/opentakeoff/tree/main/mcp",
1275
+ keywords: [
1276
+ "mcp",
1277
+ "model-context-protocol",
1278
+ "takeoff",
1279
+ "construction",
1280
+ "estimating",
1281
+ "pdf",
1282
+ "quantity-takeoff",
1283
+ "flooring"
1284
+ ]
1285
+ };
1286
+
1206
1287
  // server.ts
1207
1288
  function buildServer(session = new Session()) {
1208
- const server = new McpServer({ name: "opentakeoff", version: "0.1.0" });
1289
+ const server = new McpServer({ name: "opentakeoff", version: package_default.version });
1209
1290
  registerTools(server, session);
1210
1291
  return server;
1211
1292
  }
package/dist/server.js CHANGED
@@ -5,7 +5,6 @@
5
5
  // module that defers the server (and its hoisted externals) behind a dynamic
6
6
  // import. Same belt as src/hush.ts, one module earlier.
7
7
  console.log = console.error.bind(console);
8
-
9
8
  // pdf.js 4.x needs Promise.withResolvers (Node 22+); polyfill here so the
10
9
  // engines floor stays Node 20 — must exist before pdfjs-dist evaluates.
11
10
  if (typeof Promise.withResolvers !== "function") {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "mcpName": "io.github.kentucky-ai/opentakeoff",
4
5
  "type": "module",
5
6
  "description": "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
6
7
  "license": "Apache-2.0",
@@ -10,7 +11,7 @@
10
11
  "scripts": {
11
12
  "dev": "node --import tsx server.ts",
12
13
  "start": "node dist/server.js",
13
- "build": "esbuild server.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/server-core.js && cp bin.js dist/server.js && chmod +x dist/server.js",
14
+ "build": "esbuild server.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/server-core.js && node scripts/finish-build.mjs",
14
15
  "prepublishOnly": "npm run typecheck && npm test && npm run build",
15
16
  "typecheck": "tsc --noEmit",
16
17
  "test": "node --import tsx --test test/*.test.ts"
@@ -50,4 +51,4 @@
50
51
  "quantity-takeoff",
51
52
  "flooring"
52
53
  ]
53
- }
54
+ }