opentakeoff-mcp 0.5.0 → 0.6.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 +26 -17
- package/dist/server-core.js +168 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,9 +27,9 @@ No Node, no npm: download **`opentakeoff-mcp.mcpb`** from the
|
|
|
27
27
|
double-click it — Claude Desktop installs the server with its dependencies
|
|
28
28
|
bundled. Built by `npm run mcpb` and attached automatically to every `mcp-v*`
|
|
29
29
|
release. The bundle is platform-neutral on purpose: it excludes the optional
|
|
30
|
-
native canvas, so every tool and the text/metadata resources work
|
|
31
|
-
|
|
32
|
-
available.
|
|
30
|
+
native canvas, so every JSON tool and the text/metadata resources work
|
|
31
|
+
everywhere; the sheet-image resource and the `view_sheet` tool say exactly
|
|
32
|
+
what's missing where rendering isn't available.
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
The takeoff engine — One-Click Area, the scale model, conditions, totals — on
|
|
@@ -115,12 +115,15 @@ includes document text, shape vertices, or result payload content.
|
|
|
115
115
|
| `export_takeoff` | The full `opentakeoff.takeoff_canvas.v1` payload — exactly what the app autosaves. Inline, and to disk with `path`. |
|
|
116
116
|
| `delete_shape` | Remove a committed shape by id. |
|
|
117
117
|
| `read_sheet_text` | Positioned page text (image px), optionally restricted to a region — title blocks, room labels, finish schedules. |
|
|
118
|
+
| `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed. |
|
|
118
119
|
|
|
119
|
-
Every tool declares an **`outputSchema`**, and every reply carries the
|
|
120
|
-
as **`structuredContent`** — typed, machine-validated on every call —
|
|
121
|
-
the same compact JSON in a single text item for clients that predate
|
|
122
|
-
output.
|
|
123
|
-
|
|
120
|
+
Every JSON tool declares an **`outputSchema`**, and every reply carries the
|
|
121
|
+
payload as **`structuredContent`** — typed, machine-validated on every call —
|
|
122
|
+
alongside the same compact JSON in a single text item for clients that predate
|
|
123
|
+
structured output. `view_sheet` is the one image tool: its reply is a PNG
|
|
124
|
+
content item plus a JSON meta text item (image replies aren't structured
|
|
125
|
+
output, so it declares no schema by design). Failures come back as
|
|
126
|
+
`isError: true` with `{"error": "..."}` — never a dropped connection.
|
|
124
127
|
|
|
125
128
|
## Resources — browse before you measure
|
|
126
129
|
|
|
@@ -209,19 +212,25 @@ npm test # session + tool-layer + e2e, against demo/sample-plan.pdf
|
|
|
209
212
|
## Releasing (maintainers)
|
|
210
213
|
|
|
211
214
|
MCP releases live in the **`mcp-v*`** tag namespace — bare `v*` tags belong to
|
|
212
|
-
the app (v0.2.0, v0.3.0 are app releases).
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
the app (v0.2.0, v0.3.0 are app releases). Releases publish via **npm trusted
|
|
216
|
+
publishing**: the tag push fires `.github/workflows/publish-mcp.yml`, which
|
|
217
|
+
pauses at the `release` environment for maintainer approval, then publishes
|
|
218
|
+
the npm artifact over OIDC with a **provenance attestation** (no npm token
|
|
219
|
+
exists anywhere — the npm package designates that exact repo + workflow as
|
|
220
|
+
its trusted publisher), followed by the MCP registry entry, the GitHub
|
|
221
|
+
release, and the MCPB bundle.
|
|
215
222
|
|
|
216
223
|
```bash
|
|
217
224
|
# 1. bump the version — all three fields together:
|
|
218
225
|
# package.json .version, server.json .version, server.json .packages[0].version
|
|
219
|
-
# 2.
|
|
220
|
-
npm publish
|
|
221
|
-
# 3. tag and push — this fires .github/workflows/publish-mcp.yml:
|
|
226
|
+
# 2. tag and push — this fires the whole release:
|
|
222
227
|
git tag mcp-v<version> && git push origin mcp-v<version>
|
|
228
|
+
# 3. approve the run (GitHub → Actions → the paused "Publish to MCP Registry" run)
|
|
223
229
|
```
|
|
224
230
|
|
|
225
|
-
The workflow checks version consistency,
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
The workflow checks version consistency, runs the full publish gate
|
|
232
|
+
(`prepublishOnly` = typecheck + tests + build), publishes to npm and the
|
|
233
|
+
official MCP registry, verifies the registry listing, and creates the GitHub
|
|
234
|
+
release (titled `opentakeoff-mcp <version>`). A re-run skips the npm publish
|
|
235
|
+
if that version already shipped, so a transient failure downstream is safe to
|
|
236
|
+
retry.
|
package/dist/server-core.js
CHANGED
|
@@ -178,6 +178,28 @@ async function openPdf(filePath) {
|
|
|
178
178
|
} finally {
|
|
179
179
|
factory.destroy(target);
|
|
180
180
|
}
|
|
181
|
+
},
|
|
182
|
+
async renderRegionPng(region, longEdge, draw) {
|
|
183
|
+
await ensureCanvasGlobals();
|
|
184
|
+
const w = region.x1 - region.x0;
|
|
185
|
+
const h = region.y1 - region.y0;
|
|
186
|
+
const zoom = longEdge / Math.max(w, h);
|
|
187
|
+
const width = Math.max(1, Math.round(w * zoom));
|
|
188
|
+
const height = Math.max(1, Math.round(h * zoom));
|
|
189
|
+
const rvp = page.getViewport({
|
|
190
|
+
scale: RENDER_SCALE * zoom,
|
|
191
|
+
offsetX: -region.x0 * zoom,
|
|
192
|
+
offsetY: -region.y0 * zoom
|
|
193
|
+
});
|
|
194
|
+
const factory = doc.canvasFactory;
|
|
195
|
+
const target = factory.create(width, height);
|
|
196
|
+
try {
|
|
197
|
+
await page.render({ canvasContext: target.context, viewport: rvp }).promise;
|
|
198
|
+
draw?.(target.context, (x, y) => [(x - region.x0) * zoom, (y - region.y0) * zoom]);
|
|
199
|
+
return { png: new Uint8Array(target.canvas.toBuffer("image/png")), width, height, zoom };
|
|
200
|
+
} finally {
|
|
201
|
+
factory.destroy(target);
|
|
202
|
+
}
|
|
181
203
|
}
|
|
182
204
|
};
|
|
183
205
|
},
|
|
@@ -202,6 +224,12 @@ var ok = (payload) => ({
|
|
|
202
224
|
structuredContent: payload,
|
|
203
225
|
content: [{ type: "text", text: JSON.stringify(payload) }]
|
|
204
226
|
});
|
|
227
|
+
var okImage = (png, meta) => ({
|
|
228
|
+
content: [
|
|
229
|
+
{ type: "image", data: Buffer.from(png.buffer, png.byteOffset, png.byteLength).toString("base64"), mimeType: "image/png" },
|
|
230
|
+
{ type: "text", text: JSON.stringify(meta) }
|
|
231
|
+
]
|
|
232
|
+
});
|
|
205
233
|
var fail = (err) => ({
|
|
206
234
|
isError: true,
|
|
207
235
|
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }]
|
|
@@ -900,6 +928,86 @@ function grandTotals(rows) {
|
|
|
900
928
|
};
|
|
901
929
|
}
|
|
902
930
|
|
|
931
|
+
// src/view.ts
|
|
932
|
+
var INK = "#d91a1a";
|
|
933
|
+
var PENCIL = "#2659e6";
|
|
934
|
+
var GRID_MINOR = "rgba(184, 191, 217, 0.55)";
|
|
935
|
+
var GRID_MAJOR = "rgba(51, 107, 242, 0.6)";
|
|
936
|
+
var GRID_LABEL = "#336bf2";
|
|
937
|
+
function gridPxPerFoot(spec, upp) {
|
|
938
|
+
const s = (spec || "").trim().toLowerCase();
|
|
939
|
+
if (!s) return null;
|
|
940
|
+
if (s === "auto") {
|
|
941
|
+
if (upp == null) {
|
|
942
|
+
throw new UserError(`grid "auto" needs this sheet's scale set \u2014 call set_scale first, or pass the drawing scale read off the title block instead (e.g. grid: "1/4").`);
|
|
943
|
+
}
|
|
944
|
+
return 1 / upp;
|
|
945
|
+
}
|
|
946
|
+
let inPerFt;
|
|
947
|
+
if (s.includes("/")) {
|
|
948
|
+
const [num, den] = s.split("/", 2);
|
|
949
|
+
inPerFt = Number(num) / Number(den);
|
|
950
|
+
} else {
|
|
951
|
+
inPerFt = Number(s);
|
|
952
|
+
}
|
|
953
|
+
if (!Number.isFinite(inPerFt)) {
|
|
954
|
+
throw new UserError(`Bad grid scale ${JSON.stringify(spec)} \u2014 use inches-per-foot like "1/4", "3/16", or "0.25" (or "auto" once the scale is set).`);
|
|
955
|
+
}
|
|
956
|
+
if (inPerFt < 0.01 || inPerFt > 12) {
|
|
957
|
+
throw new UserError(`Grid scale out of range: ${JSON.stringify(spec)} \u2014 inches-per-foot must be between 0.01 and 12.`);
|
|
958
|
+
}
|
|
959
|
+
return inPerFt * 72 * RENDER_SCALE;
|
|
960
|
+
}
|
|
961
|
+
function drawGrid(ctx, toCanvas, region, ppf) {
|
|
962
|
+
const [ox, oy] = toCanvas(region.x0, region.y0);
|
|
963
|
+
const step = toCanvas(region.x0 + ppf, region.y0)[0] - ox;
|
|
964
|
+
const [ex, ey] = toCanvas(region.x1, region.y1);
|
|
965
|
+
const nx = Math.ceil((region.x1 - region.x0) / ppf);
|
|
966
|
+
const ny = Math.ceil((region.y1 - region.y0) / ppf);
|
|
967
|
+
ctx.setLineDash([]);
|
|
968
|
+
for (const major of [false, true]) {
|
|
969
|
+
ctx.strokeStyle = major ? GRID_MAJOR : GRID_MINOR;
|
|
970
|
+
ctx.lineWidth = major ? 1.5 : 0.75;
|
|
971
|
+
if (!major && step < 3) continue;
|
|
972
|
+
ctx.beginPath();
|
|
973
|
+
for (let i = 0; i <= nx; i++) {
|
|
974
|
+
if (i % 5 === 0 !== major) continue;
|
|
975
|
+
const x = ox + i * step;
|
|
976
|
+
ctx.moveTo(x, oy);
|
|
977
|
+
ctx.lineTo(x, ey);
|
|
978
|
+
}
|
|
979
|
+
for (let j = 0; j <= ny; j++) {
|
|
980
|
+
if (j % 5 === 0 !== major) continue;
|
|
981
|
+
const y = oy + j * step;
|
|
982
|
+
ctx.moveTo(ox, y);
|
|
983
|
+
ctx.lineTo(ex, y);
|
|
984
|
+
}
|
|
985
|
+
ctx.stroke();
|
|
986
|
+
}
|
|
987
|
+
const size = Math.max(9, Math.min(36, step * 0.38));
|
|
988
|
+
ctx.font = `${size}px sans-serif`;
|
|
989
|
+
ctx.fillStyle = GRID_LABEL;
|
|
990
|
+
for (let i = 0; i <= nx; i += 5) ctx.fillText(String(i), ox + i * step + 3, oy + size + 2);
|
|
991
|
+
for (let j = 5; j <= ny; j += 5) ctx.fillText(String(j), ox + 3, oy + j * step - 3);
|
|
992
|
+
}
|
|
993
|
+
function drawShapes(ctx, toCanvas, shapes, sheetW, sheetH, longEdge) {
|
|
994
|
+
const w = Math.max(1.4, longEdge / 700);
|
|
995
|
+
for (const s of shapes) {
|
|
996
|
+
const pts = s.verts_norm.map(([nx, ny]) => toCanvas(nx * sheetW, ny * sheetH));
|
|
997
|
+
if (pts.length < 2) continue;
|
|
998
|
+
const pending = s.origin?.reviewed === false;
|
|
999
|
+
ctx.strokeStyle = pending ? PENCIL : INK;
|
|
1000
|
+
ctx.lineWidth = w;
|
|
1001
|
+
ctx.setLineDash(pending ? [w * 4, w * 3] : []);
|
|
1002
|
+
ctx.beginPath();
|
|
1003
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1004
|
+
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
1005
|
+
if (s.measure_role !== "linear") ctx.closePath();
|
|
1006
|
+
ctx.stroke();
|
|
1007
|
+
}
|
|
1008
|
+
ctx.setLineDash([]);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
903
1011
|
// src/session.ts
|
|
904
1012
|
var SNAP_CELL = 24;
|
|
905
1013
|
var SNAP_TOL = 7;
|
|
@@ -909,6 +1017,9 @@ var mintUuid = () => globalThis.crypto && typeof globalThis.crypto.randomUUID ==
|
|
|
909
1017
|
var uid = (p) => `${p}-${mintUuid()}`;
|
|
910
1018
|
var ANN_SCHEMA = "opentakeoff.takeoff_canvas.v1";
|
|
911
1019
|
var IMAGE_MAX_EDGE = 1568;
|
|
1020
|
+
var VIEW_MIN_PX = 200;
|
|
1021
|
+
var VIEW_DEFAULT_PX = 1400;
|
|
1022
|
+
var VIEW_MAX_PX = 2e3;
|
|
912
1023
|
var sheetSummary = (s) => ({
|
|
913
1024
|
sheet: s.key,
|
|
914
1025
|
page: s.pageNum,
|
|
@@ -1005,6 +1116,39 @@ var Session = class {
|
|
|
1005
1116
|
}
|
|
1006
1117
|
return s.png;
|
|
1007
1118
|
}
|
|
1119
|
+
/** view_sheet: render a sheet (or an image-px crop of it) to PNG, with an
|
|
1120
|
+
* optional committed-shapes overlay and calibrated measuring grid. The grid
|
|
1121
|
+
* draws under the overlay, both in canvas space after the page rasterizes. */
|
|
1122
|
+
async viewSheet(name, opts) {
|
|
1123
|
+
const s = this.sheet(name);
|
|
1124
|
+
const px = Math.max(VIEW_MIN_PX, Math.min(VIEW_MAX_PX, Math.round(opts.px ?? VIEW_DEFAULT_PX)));
|
|
1125
|
+
const clampX = (v) => Math.max(0, Math.min(v, s.widthPx));
|
|
1126
|
+
const clampY = (v) => Math.max(0, Math.min(v, s.heightPx));
|
|
1127
|
+
const r = opts.region ? { x0: clampX(opts.region.x0), y0: clampY(opts.region.y0), x1: clampX(opts.region.x1), y1: clampY(opts.region.y1) } : { x0: 0, y0: 0, x1: s.widthPx, y1: s.heightPx };
|
|
1128
|
+
if (!(r.x1 - r.x0 >= 1 && r.y1 - r.y0 >= 1)) {
|
|
1129
|
+
throw new UserError(`Empty view region \u2014 need x1 > x0 and y1 > y0 in image px inside the sheet (${s.widthPx} \xD7 ${s.heightPx}).`);
|
|
1130
|
+
}
|
|
1131
|
+
const ppf = gridPxPerFoot(opts.grid, s.upp);
|
|
1132
|
+
const sheetShapes = this.shapes.filter((x) => x.sheet_id === s.key);
|
|
1133
|
+
const { png, width, height, zoom } = await s.page.renderRegionPng(r, px, (ctx, toCanvas) => {
|
|
1134
|
+
if (ppf) drawGrid(ctx, toCanvas, r, ppf);
|
|
1135
|
+
if (opts.overlay) drawShapes(ctx, toCanvas, sheetShapes, s.widthPx, s.heightPx, px);
|
|
1136
|
+
});
|
|
1137
|
+
return {
|
|
1138
|
+
png,
|
|
1139
|
+
meta: {
|
|
1140
|
+
sheet: s.key,
|
|
1141
|
+
page: s.pageNum,
|
|
1142
|
+
sheet_px: [s.widthPx, s.heightPx],
|
|
1143
|
+
region: [round1(r.x0), round1(r.y0), round1(r.x1), round1(r.y1)],
|
|
1144
|
+
img_px: [width, height],
|
|
1145
|
+
zoom: +zoom.toFixed(4),
|
|
1146
|
+
overlay: !!opts.overlay,
|
|
1147
|
+
...opts.overlay ? { shapes_drawn: sheetShapes.length } : {},
|
|
1148
|
+
grid_px_per_foot: ppf ? round2(ppf) : 0
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1008
1152
|
async ensureGeometry(s) {
|
|
1009
1153
|
if (!s.geo) {
|
|
1010
1154
|
const opList = await s.page.operatorList();
|
|
@@ -1255,7 +1399,7 @@ import { z as z2 } from "zod";
|
|
|
1255
1399
|
var TRACE_ENV = "OPENTAKEOFF_MCP_TRACE";
|
|
1256
1400
|
function traceToolCall(tool, args, startedAt, reply) {
|
|
1257
1401
|
if (process.env[TRACE_ENV] !== "1") return;
|
|
1258
|
-
const text = reply.content
|
|
1402
|
+
const text = reply.content.map((c) => "text" in c ? c.text : c.data).join("");
|
|
1259
1403
|
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
1260
1404
|
const sheet = args && typeof args === "object" && "sheet" in args ? args.sheet : void 0;
|
|
1261
1405
|
const event = {
|
|
@@ -1528,6 +1672,27 @@ function registerTools(server, session) {
|
|
|
1528
1672
|
},
|
|
1529
1673
|
outputSchema: readSheetTextOutput
|
|
1530
1674
|
}, run("read_sheet_text", (a) => session.readSheetText(a.sheet, a.region)));
|
|
1675
|
+
server.registerTool("view_sheet", {
|
|
1676
|
+
description: `SEE the sheet \u2014 render the page (or a crop of it) to a PNG image. This is your eyes on the plan: full-sheet overview first, then tight crops at higher px until dimension strings and room labels read cleanly. region is in image px \u2014 the same space as every other tool \u2014 so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix \xD7 (region_x1 \u2212 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) \u2014 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 \u2014 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 \u2014 "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. ${COORDS}`,
|
|
1677
|
+
inputSchema: {
|
|
1678
|
+
sheet: z2.string(),
|
|
1679
|
+
region: z2.object({ x0: z2.number(), y0: z2.number(), x1: z2.number(), y1: z2.number() }).optional().describe("Crop rect in image px (origin top-left, y down); omit for the full sheet"),
|
|
1680
|
+
px: z2.number().int().min(200).max(2e3).optional().describe("Long-side pixel budget of the returned image (default 1400) \u2014 small region + high px = readable dimension strings"),
|
|
1681
|
+
overlay: z2.boolean().optional().describe("Burn committed shapes into the render (solid = human-affirmed, dashed = unreviewed)"),
|
|
1682
|
+
grid: z2.string().optional().describe(`Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"`)
|
|
1683
|
+
}
|
|
1684
|
+
}, async (a) => {
|
|
1685
|
+
const startedAt = process.hrtime.bigint();
|
|
1686
|
+
let reply;
|
|
1687
|
+
try {
|
|
1688
|
+
const { png, meta } = await session.viewSheet(a.sheet, { region: a.region, px: a.px, overlay: a.overlay, grid: a.grid });
|
|
1689
|
+
reply = okImage(png, meta);
|
|
1690
|
+
} catch (e) {
|
|
1691
|
+
reply = fail(e);
|
|
1692
|
+
}
|
|
1693
|
+
traceToolCall("view_sheet", a, startedAt, reply);
|
|
1694
|
+
return reply;
|
|
1695
|
+
});
|
|
1531
1696
|
}
|
|
1532
1697
|
|
|
1533
1698
|
// src/resources.ts
|
|
@@ -1609,7 +1774,7 @@ function registerResources(server, session) {
|
|
|
1609
1774
|
// package.json
|
|
1610
1775
|
var package_default = {
|
|
1611
1776
|
name: "opentakeoff-mcp",
|
|
1612
|
-
version: "0.
|
|
1777
|
+
version: "0.6.0",
|
|
1613
1778
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
1614
1779
|
type: "module",
|
|
1615
1780
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -1625,7 +1790,7 @@ var package_default = {
|
|
|
1625
1790
|
mcpb: "npm run build && node scripts/build-mcpb.mjs",
|
|
1626
1791
|
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
1627
1792
|
typecheck: "tsc --noEmit",
|
|
1628
|
-
test: "node --import tsx --test test/conformance.test.ts test/e2e.test.ts test/resources.test.ts test/session.test.ts test/tools.test.ts"
|
|
1793
|
+
test: "node --import tsx --test test/conformance.test.ts test/e2e.test.ts test/resources.test.ts test/session.test.ts test/tools.test.ts test/view.test.ts"
|
|
1629
1794
|
},
|
|
1630
1795
|
dependencies: {
|
|
1631
1796
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"mcpb": "npm run build && node scripts/build-mcpb.mjs",
|
|
17
17
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"test": "node --import tsx --test test/conformance.test.ts test/e2e.test.ts test/resources.test.ts test/session.test.ts test/tools.test.ts"
|
|
19
|
+
"test": "node --import tsx --test test/conformance.test.ts test/e2e.test.ts test/resources.test.ts test/session.test.ts test/tools.test.ts test/view.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|