@drawpro/mcp 0.3.0 → 0.4.1
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 +69 -0
- package/dist/server.js +313 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,75 @@ Diagram specs describe *what connects to what*. Layout, sizing, text wrapping,
|
|
|
121
121
|
and arrow binding are derived by `@drawpro/diagram` — a spec never contains
|
|
122
122
|
coordinates.
|
|
123
123
|
|
|
124
|
+
## Usage log
|
|
125
|
+
|
|
126
|
+
Turning telemetry on is enough to start recording:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx -y @drawpro/mcp telemetry # show the state and the exact payload
|
|
130
|
+
npx -y @drawpro/mcp telemetry on # records to ~/.drawpro/usage.jsonl and shares an aggregate
|
|
131
|
+
npx -y @drawpro/mcp telemetry off
|
|
132
|
+
npx -y @drawpro/mcp report # send once, without turning anything on
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Consenting to *send* usage implies consent to *record* it — recording is the
|
|
136
|
+
lesser act — so opting in does not also require configuring a log. The
|
|
137
|
+
implication does not run the other way: setting `DRAWPRO_MCP_LOG` records
|
|
138
|
+
locally and shares nothing.
|
|
139
|
+
|
|
140
|
+
To record without ever sharing, set the path yourself:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
claude mcp add drawpro --scope user \
|
|
144
|
+
-e DRAWPRO_TOKEN="dp_live_..." \
|
|
145
|
+
-e DRAWPRO_MCP_LOG="$HOME/.drawpro/usage.jsonl" \
|
|
146
|
+
-- npx -y @drawpro/mcp
|
|
147
|
+
|
|
148
|
+
npx -y @drawpro/mcp stats
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> Run these from anywhere except a checkout of this repository. Inside it, npm
|
|
152
|
+
> resolves `@drawpro/mcp` to the workspace copy, whose bin is not linked, and
|
|
153
|
+
> npx fails with `drawpro-mcp: command not found`. Use
|
|
154
|
+
> `node packages/mcp/dist/server.js <command>` there instead.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
5 calls 2026-08-29 .. 2026-08-29
|
|
158
|
+
|
|
159
|
+
tool calls refused failed median
|
|
160
|
+
read_sheet 2 0 0% 0 881ms
|
|
161
|
+
edit_sheet_text 1 1 100% 0 62ms
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Opt-in, and never transmitted. `logCall` does one thing — append to that file —
|
|
165
|
+
and the only outbound request this package can make is to your own DrawPro
|
|
166
|
+
account's sheets. Unset the variable and nothing is written at all; there is no
|
|
167
|
+
default path and no fallback.
|
|
168
|
+
|
|
169
|
+
### How that feeds back into the package
|
|
170
|
+
|
|
171
|
+
The raw log stays on your machine and carries workspace and sheet ids, so you
|
|
172
|
+
can correlate calls against your own account. `stats` is the aggregate: tool
|
|
173
|
+
names, counts, timings, and nothing that identifies an account, a workspace, a
|
|
174
|
+
sheet, or anything drawn on one.
|
|
175
|
+
|
|
176
|
+
That split is the point. Sharing is a decision you make, not a default the
|
|
177
|
+
package makes for you:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
drawpro-mcp stats --json # paste into an issue
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
There is no telemetry endpoint, and adding one to a product built on the server
|
|
184
|
+
never seeing your diagrams would be the wrong trade. If a tool is refusing
|
|
185
|
+
often, the aggregate says so without anyone learning what you were drawing.
|
|
186
|
+
|
|
187
|
+
The column worth watching is **refused**. A tool that frequently declines is one
|
|
188
|
+
whose description is not steering the model well, and that is cheaper to learn
|
|
189
|
+
from real use than from a synthetic eval — both `edit_sheet_text` and
|
|
190
|
+
`import_sheet` exist because real use found them missing, and no eval written
|
|
191
|
+
beforehand would have predicted either.
|
|
192
|
+
|
|
124
193
|
## Tests
|
|
125
194
|
|
|
126
195
|
```bash
|
package/dist/server.js
CHANGED
|
@@ -24,8 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/server.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var import_node_fs3 = require("node:fs");
|
|
28
|
+
var import_node_os3 = require("node:os");
|
|
29
|
+
var import_node_path3 = require("node:path");
|
|
29
30
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
30
31
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
31
32
|
var import_zod = require("zod");
|
|
@@ -191,11 +192,13 @@ async function decryptSheet(encryptedData, privateKeyBytes) {
|
|
|
191
192
|
|
|
192
193
|
// ../client/src/api.ts
|
|
193
194
|
var DrawProClient = class {
|
|
194
|
-
constructor(baseUrl, token) {
|
|
195
|
+
constructor(baseUrl, token, onRequest) {
|
|
195
196
|
this.baseUrl = baseUrl;
|
|
196
197
|
this.token = token;
|
|
198
|
+
this.onRequest = onRequest;
|
|
197
199
|
}
|
|
198
200
|
async request(path, init) {
|
|
201
|
+
const started = Date.now();
|
|
199
202
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
200
203
|
...init,
|
|
201
204
|
headers: {
|
|
@@ -204,7 +207,19 @@ var DrawProClient = class {
|
|
|
204
207
|
...init?.headers ?? {}
|
|
205
208
|
}
|
|
206
209
|
});
|
|
207
|
-
const
|
|
210
|
+
const raw = await res.text();
|
|
211
|
+
this.onRequest?.({
|
|
212
|
+
method: init?.method ?? "GET",
|
|
213
|
+
path: path.replace(/\/[a-z0-9]{20,}/gi, "/:id"),
|
|
214
|
+
status: res.status,
|
|
215
|
+
ms: Date.now() - started,
|
|
216
|
+
bytes: raw.length
|
|
217
|
+
});
|
|
218
|
+
let body = {};
|
|
219
|
+
try {
|
|
220
|
+
body = JSON.parse(raw);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
208
223
|
if (!res.ok) {
|
|
209
224
|
throw new Error(`${path} -> ${res.status} ${body.error ?? JSON.stringify(body)}`);
|
|
210
225
|
}
|
|
@@ -387,6 +402,35 @@ function askHidden(question) {
|
|
|
387
402
|
});
|
|
388
403
|
}
|
|
389
404
|
|
|
405
|
+
// ../client/src/config.ts
|
|
406
|
+
var import_node_fs2 = require("node:fs");
|
|
407
|
+
var import_node_os2 = require("node:os");
|
|
408
|
+
var import_node_path2 = require("node:path");
|
|
409
|
+
var import_node_crypto3 = require("node:crypto");
|
|
410
|
+
var DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".drawpro");
|
|
411
|
+
var FILE = (0, import_node_path2.join)(DIR, "config.json");
|
|
412
|
+
function readConfig() {
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(FILE, "utf8"));
|
|
415
|
+
} catch {
|
|
416
|
+
return {};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function writeConfig(patch) {
|
|
420
|
+
const next = { ...readConfig(), ...patch };
|
|
421
|
+
(0, import_node_fs2.mkdirSync)(DIR, { recursive: true, mode: 448 });
|
|
422
|
+
(0, import_node_fs2.writeFileSync)(FILE, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
423
|
+
return next;
|
|
424
|
+
}
|
|
425
|
+
function installId() {
|
|
426
|
+
const config = readConfig();
|
|
427
|
+
if (config.installId) return config.installId;
|
|
428
|
+
return writeConfig({ installId: (0, import_node_crypto3.randomUUID)() }).installId;
|
|
429
|
+
}
|
|
430
|
+
function telemetryEnabled() {
|
|
431
|
+
return readConfig().telemetry === "on";
|
|
432
|
+
}
|
|
433
|
+
|
|
390
434
|
// ../diagram/src/ids.ts
|
|
391
435
|
var import_fractional_indexing = require("fractional-indexing");
|
|
392
436
|
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
|
@@ -1358,10 +1402,13 @@ function api() {
|
|
|
1358
1402
|
'DRAWPRO_TOKEN is not set. Create a token in DrawPro under "Connect to Claude Code", then:\n claude mcp add drawpro --scope user -e DRAWPRO_TOKEN="dp_live_..." -- npx -y @drawpro/mcp'
|
|
1359
1403
|
);
|
|
1360
1404
|
}
|
|
1361
|
-
clientInstance = new DrawProClient(BASE_URL, token);
|
|
1405
|
+
clientInstance = new DrawProClient(BASE_URL, token, (t) => inFlight.push(t));
|
|
1362
1406
|
}
|
|
1363
1407
|
return clientInstance;
|
|
1364
1408
|
}
|
|
1409
|
+
var SESSION_ID = Math.random().toString(36).slice(2, 10);
|
|
1410
|
+
var VERSION = "0.4.1";
|
|
1411
|
+
var inFlight = [];
|
|
1365
1412
|
var cachedUser = null;
|
|
1366
1413
|
async function currentUser() {
|
|
1367
1414
|
if (!cachedUser) cachedUser = await api().me();
|
|
@@ -1386,8 +1433,90 @@ function text(body) {
|
|
|
1386
1433
|
function sheetUrl(workspaceId, sheetId) {
|
|
1387
1434
|
return `${APP_URL}/workspace/${workspaceId}/sheet/${sheetId}`;
|
|
1388
1435
|
}
|
|
1389
|
-
var
|
|
1390
|
-
|
|
1436
|
+
var DEFAULT_LOG = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".drawpro", "usage.jsonl");
|
|
1437
|
+
function logPath() {
|
|
1438
|
+
const explicit = process.env.DRAWPRO_MCP_LOG;
|
|
1439
|
+
if (explicit) return explicit;
|
|
1440
|
+
return telemetryEnabled() ? DEFAULT_LOG : void 0;
|
|
1441
|
+
}
|
|
1442
|
+
function logCall(entry) {
|
|
1443
|
+
const path = logPath();
|
|
1444
|
+
if (!path) return;
|
|
1445
|
+
try {
|
|
1446
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)((0, import_node_os3.homedir)(), ".drawpro"), { recursive: true, mode: 448 });
|
|
1447
|
+
(0, import_node_fs3.appendFileSync)(path, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }) + "\n");
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
var REFUSALS = [
|
|
1452
|
+
"Nothing was written",
|
|
1453
|
+
"The diagram was not created",
|
|
1454
|
+
"is locked",
|
|
1455
|
+
"Could not read",
|
|
1456
|
+
'has no "elements" array'
|
|
1457
|
+
];
|
|
1458
|
+
function summarise(args) {
|
|
1459
|
+
const spec = args.spec;
|
|
1460
|
+
const edits = args.edits;
|
|
1461
|
+
return {
|
|
1462
|
+
workspace_id: args.workspace_id,
|
|
1463
|
+
sheet_id: args.sheet_id,
|
|
1464
|
+
...spec ? { nodes: spec.nodes?.length ?? 0, edges: spec.edges?.length ?? 0 } : {},
|
|
1465
|
+
...edits ? { edits: edits.length } : {},
|
|
1466
|
+
...args.file_path ? { from_file: true } : {}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function instrument(name, handler) {
|
|
1470
|
+
return (async (args) => {
|
|
1471
|
+
const started = Date.now();
|
|
1472
|
+
inFlight = [];
|
|
1473
|
+
try {
|
|
1474
|
+
const result = await handler(args);
|
|
1475
|
+
const body = result.content.map((c) => c.text).join("\n");
|
|
1476
|
+
const apiMs = inFlight.reduce((sum, t) => sum + t.ms, 0);
|
|
1477
|
+
const total = Date.now() - started;
|
|
1478
|
+
logCall({
|
|
1479
|
+
session: SESSION_ID,
|
|
1480
|
+
tool: name,
|
|
1481
|
+
ok: true,
|
|
1482
|
+
refused: REFUSALS.some((r) => body.includes(r)),
|
|
1483
|
+
ms: total,
|
|
1484
|
+
api_ms: apiMs,
|
|
1485
|
+
local_ms: total - apiMs,
|
|
1486
|
+
requests: inFlight.map((t) => ({ m: t.method, p: t.path, s: t.status, ms: t.ms, bytes: t.bytes })),
|
|
1487
|
+
args: summarise(args),
|
|
1488
|
+
// Tools word this two ways: "38 elements" and "elements: 38".
|
|
1489
|
+
elements: Number(body.match(/(\d+) elements/)?.[1] ?? body.match(/elements: (\d+)/)?.[1]) || void 0
|
|
1490
|
+
});
|
|
1491
|
+
return result;
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
const apiMs = inFlight.reduce((sum, t) => sum + t.ms, 0);
|
|
1494
|
+
const total = Date.now() - started;
|
|
1495
|
+
logCall({
|
|
1496
|
+
session: SESSION_ID,
|
|
1497
|
+
tool: name,
|
|
1498
|
+
ok: false,
|
|
1499
|
+
ms: total,
|
|
1500
|
+
api_ms: apiMs,
|
|
1501
|
+
local_ms: total - apiMs,
|
|
1502
|
+
requests: inFlight.map((t) => ({ m: t.method, p: t.path, s: t.status, ms: t.ms, bytes: t.bytes })),
|
|
1503
|
+
args: summarise(args),
|
|
1504
|
+
error: err.message.slice(0, 200)
|
|
1505
|
+
});
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
var server = new import_mcp.McpServer({ name: "drawpro", version: VERSION });
|
|
1511
|
+
var tool = ((name, ...rest) => {
|
|
1512
|
+
const handler = rest.pop();
|
|
1513
|
+
return server.tool(
|
|
1514
|
+
name,
|
|
1515
|
+
...rest,
|
|
1516
|
+
instrument(name, handler)
|
|
1517
|
+
);
|
|
1518
|
+
});
|
|
1519
|
+
tool(
|
|
1391
1520
|
"list_workspaces",
|
|
1392
1521
|
"List the DrawPro workspaces this account can access. Workspace names are encrypted at rest and are only readable once the account is unlocked.",
|
|
1393
1522
|
{},
|
|
@@ -1406,7 +1535,7 @@ Names are encrypted. ${unlocked.error}` : "";
|
|
|
1406
1535
|
return text(rows.join("\n") + note);
|
|
1407
1536
|
}
|
|
1408
1537
|
);
|
|
1409
|
-
|
|
1538
|
+
tool(
|
|
1410
1539
|
"list_sheets",
|
|
1411
1540
|
"List the sheets in a DrawPro workspace, with their decrypted names.",
|
|
1412
1541
|
{ workspace_id: import_zod.z.string().describe("Workspace id, from list_workspaces") },
|
|
@@ -1425,7 +1554,7 @@ Names are encrypted. ${unlocked.error}` : "";
|
|
|
1425
1554
|
return text((rows.join("\n") || "(no sheets)") + note);
|
|
1426
1555
|
}
|
|
1427
1556
|
);
|
|
1428
|
-
|
|
1557
|
+
tool(
|
|
1429
1558
|
"read_sheet",
|
|
1430
1559
|
"Read what a DrawPro sheet contains: its shapes, and which arrows connect what. Returns a readable outline rather than raw Excalidraw JSON, which is mostly coordinates and style.",
|
|
1431
1560
|
{
|
|
@@ -1470,7 +1599,7 @@ var specShape = {
|
|
|
1470
1599
|
"What connects to what. Layout, sizing, text wrapping, and arrow binding are derived \u2014 never supply coordinates."
|
|
1471
1600
|
)
|
|
1472
1601
|
};
|
|
1473
|
-
|
|
1602
|
+
tool(
|
|
1474
1603
|
"validate_spec",
|
|
1475
1604
|
"Check a diagram spec without creating anything. Use this before writing if the diagram is large or the spec was assembled programmatically.",
|
|
1476
1605
|
specShape,
|
|
@@ -1492,7 +1621,7 @@ function buildOrExplain(spec) {
|
|
|
1492
1621
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
1493
1622
|
return { ok: true, scene, warnings: warnings.map((w) => w.message) };
|
|
1494
1623
|
}
|
|
1495
|
-
|
|
1624
|
+
tool(
|
|
1496
1625
|
"create_diagram",
|
|
1497
1626
|
"Create a new sheet in DrawPro from a diagram spec. Returns a link to open it.",
|
|
1498
1627
|
{
|
|
@@ -1519,7 +1648,7 @@ ${sheetUrl(workspace_id, sheet.id)}${warned}`
|
|
|
1519
1648
|
);
|
|
1520
1649
|
}
|
|
1521
1650
|
);
|
|
1522
|
-
|
|
1651
|
+
tool(
|
|
1523
1652
|
"update_diagram",
|
|
1524
1653
|
"Replace a sheet\u2019s contents with a new diagram. This overwrites the whole sheet, so read_sheet first if you intend to preserve anything already there.",
|
|
1525
1654
|
{
|
|
@@ -1578,7 +1707,7 @@ async function login(forget) {
|
|
|
1578
1707
|
console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
|
|
1579
1708
|
console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
|
|
1580
1709
|
}
|
|
1581
|
-
|
|
1710
|
+
tool(
|
|
1582
1711
|
"edit_sheet_text",
|
|
1583
1712
|
"Correct the wording on an existing sheet without redrawing it. Use this for any sheet a person drew: update_diagram regenerates layout from a spec, so it discards hand-placed elements, region containers, and unbound annotations. This rewrites only the text you name and leaves every other element, and every coordinate, exactly as it was.",
|
|
1584
1713
|
{
|
|
@@ -1640,7 +1769,7 @@ ${sheetUrl(workspace_id, sheet_id)}`
|
|
|
1640
1769
|
);
|
|
1641
1770
|
}
|
|
1642
1771
|
);
|
|
1643
|
-
|
|
1772
|
+
tool(
|
|
1644
1773
|
"import_sheet",
|
|
1645
1774
|
"Write a local .excalidraw file into a sheet, exactly as it is. Use this when geometry has to change \u2014 repositioning, resizing, re-anchoring arrows \u2014 which no spec can express and edit_sheet_text will not touch. Every coordinate in the file is preserved verbatim.",
|
|
1646
1775
|
{
|
|
@@ -1652,7 +1781,7 @@ server.tool(
|
|
|
1652
1781
|
async ({ workspace_id, file_path, name, sheet_id }) => {
|
|
1653
1782
|
let parsed;
|
|
1654
1783
|
try {
|
|
1655
|
-
parsed = JSON.parse((0,
|
|
1784
|
+
parsed = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(file_path), "utf8"));
|
|
1656
1785
|
} catch (err) {
|
|
1657
1786
|
return text(`Could not read ${file_path}: ${err.message}`);
|
|
1658
1787
|
}
|
|
@@ -1679,16 +1808,184 @@ ${sheetUrl(workspace_id, id)}${noted}`
|
|
|
1679
1808
|
);
|
|
1680
1809
|
}
|
|
1681
1810
|
);
|
|
1811
|
+
var WRITE_TOOLS = ["create_diagram", "update_diagram", "edit_sheet_text", "import_sheet"];
|
|
1812
|
+
function summariseLog(file) {
|
|
1813
|
+
let rows;
|
|
1814
|
+
try {
|
|
1815
|
+
rows = (0, import_node_fs3.readFileSync)(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
1816
|
+
try {
|
|
1817
|
+
return [JSON.parse(line)];
|
|
1818
|
+
} catch {
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
} catch {
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
if (rows.length === 0) return null;
|
|
1826
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
1827
|
+
for (const r of rows) {
|
|
1828
|
+
const name = String(r.tool);
|
|
1829
|
+
const acc = byTool.get(name) ?? { n: 0, refused: 0, failed: 0, ms: [] };
|
|
1830
|
+
acc.n++;
|
|
1831
|
+
if (r.refused) acc.refused++;
|
|
1832
|
+
if (r.ok === false) acc.failed++;
|
|
1833
|
+
if (typeof r.ms === "number") acc.ms.push(r.ms);
|
|
1834
|
+
byTool.set(name, acc);
|
|
1835
|
+
}
|
|
1836
|
+
const median = (xs) => xs.length ? [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] : 0;
|
|
1837
|
+
return {
|
|
1838
|
+
calls: rows.length,
|
|
1839
|
+
writes: rows.filter(
|
|
1840
|
+
(r) => WRITE_TOOLS.includes(String(r.tool)) && !r.refused && r.ok !== false
|
|
1841
|
+
).length,
|
|
1842
|
+
from: String(rows[0].ts).slice(0, 10),
|
|
1843
|
+
to: String(rows[rows.length - 1].ts).slice(0, 10),
|
|
1844
|
+
tools: [...byTool.entries()].sort((x, y) => y[1].n - x[1].n).map(([tool2, a]) => ({
|
|
1845
|
+
tool: tool2,
|
|
1846
|
+
calls: a.n,
|
|
1847
|
+
refused: a.refused,
|
|
1848
|
+
failed: a.failed,
|
|
1849
|
+
median_ms: median(a.ms)
|
|
1850
|
+
}))
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function stats(path, asJson) {
|
|
1854
|
+
const file = path ?? logPath();
|
|
1855
|
+
if (!file) {
|
|
1856
|
+
console.error(
|
|
1857
|
+
"No usage is being recorded. Either set DRAWPRO_MCP_LOG, turn on telemetry\n(which records to ~/.drawpro/usage.jsonl), or pass a path: drawpro-mcp stats <file>"
|
|
1858
|
+
);
|
|
1859
|
+
process.exit(2);
|
|
1860
|
+
}
|
|
1861
|
+
const summary = summariseLog(file);
|
|
1862
|
+
if (!summary) {
|
|
1863
|
+
console.log(asJson ? '{"calls":0}' : "No calls recorded yet.");
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (asJson) {
|
|
1867
|
+
console.log(JSON.stringify({ version: VERSION, ...summary }, null, 2));
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
console.log(`${summary.calls} calls ${summary.from} .. ${summary.to}
|
|
1871
|
+
`);
|
|
1872
|
+
console.log(" tool calls refused failed median");
|
|
1873
|
+
for (const t of summary.tools) {
|
|
1874
|
+
const pct = t.calls ? Math.round(t.refused / t.calls * 100) : 0;
|
|
1875
|
+
console.log(
|
|
1876
|
+
` ${t.tool.padEnd(18)} ${String(t.calls).padStart(5)} ${String(t.refused).padStart(4)} ${String(pct).padStart(3)}% ${String(t.failed).padStart(6)} ${String(t.median_ms).padStart(5)}ms`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
console.log(`
|
|
1880
|
+
${summary.writes} successful writes to sheets`);
|
|
1881
|
+
console.log(
|
|
1882
|
+
"\n No ids, account details, or diagram content above \u2014 safe to paste into a bug report."
|
|
1883
|
+
);
|
|
1884
|
+
console.log(" Add --json for a machine-readable copy.");
|
|
1885
|
+
}
|
|
1886
|
+
function buildReport() {
|
|
1887
|
+
const file = logPath();
|
|
1888
|
+
if (!file) return null;
|
|
1889
|
+
const summary = summariseLog(file);
|
|
1890
|
+
if (!summary) return null;
|
|
1891
|
+
return {
|
|
1892
|
+
installId: installId(),
|
|
1893
|
+
mcpVersion: VERSION,
|
|
1894
|
+
calls: summary.calls,
|
|
1895
|
+
writes: summary.writes,
|
|
1896
|
+
tools: summary.tools
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
async function sendReport(report) {
|
|
1900
|
+
try {
|
|
1901
|
+
const res = await fetch(`${BASE_URL}/telemetry`, {
|
|
1902
|
+
method: "POST",
|
|
1903
|
+
headers: { "Content-Type": "application/json" },
|
|
1904
|
+
body: JSON.stringify(report)
|
|
1905
|
+
});
|
|
1906
|
+
return { ok: res.ok, detail: `HTTP ${res.status}` };
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
return { ok: false, detail: err.message };
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async function telemetry(action) {
|
|
1912
|
+
if (action === "off") {
|
|
1913
|
+
writeConfig({ telemetry: "off" });
|
|
1914
|
+
console.log("Telemetry off. Nothing will be sent.");
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
const report = buildReport();
|
|
1918
|
+
if (action === "on") {
|
|
1919
|
+
writeConfig({ telemetry: "on" });
|
|
1920
|
+
console.log(`Telemetry on. Usage is recorded to ${logPath()}`);
|
|
1921
|
+
console.log("Roughly once a day, an aggregate of it is sent:\n");
|
|
1922
|
+
console.log(
|
|
1923
|
+
report ? JSON.stringify(report, null, 2) : " (no calls recorded yet \u2014 the first report goes out once you have used the tools)"
|
|
1924
|
+
);
|
|
1925
|
+
console.log("\nTurn it off any time with: drawpro-mcp telemetry off");
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
console.log(`Telemetry is ${telemetryEnabled() ? "ON" : "OFF"}.
|
|
1929
|
+
`);
|
|
1930
|
+
console.log("If enabled, this is the entire payload \u2014 tool counts and timings,");
|
|
1931
|
+
console.log("no account, no token, no workspace or sheet ids, nothing drawn:\n");
|
|
1932
|
+
console.log(
|
|
1933
|
+
report ? JSON.stringify(report, null, 2) : ` (no calls recorded yet; recording ${logPath() ? `to ${logPath()}` : "is off"})`
|
|
1934
|
+
);
|
|
1935
|
+
console.log("\n drawpro-mcp telemetry on share it");
|
|
1936
|
+
console.log(" drawpro-mcp telemetry off stop");
|
|
1937
|
+
console.log(" drawpro-mcp report send once now, without turning it on");
|
|
1938
|
+
}
|
|
1939
|
+
async function reportOnce() {
|
|
1940
|
+
const report = buildReport();
|
|
1941
|
+
if (!report) {
|
|
1942
|
+
console.error(
|
|
1943
|
+
"Nothing to report \u2014 no usage has been recorded. Set DRAWPRO_MCP_LOG, or\nturn on telemetry, which records to ~/.drawpro/usage.jsonl."
|
|
1944
|
+
);
|
|
1945
|
+
process.exit(2);
|
|
1946
|
+
}
|
|
1947
|
+
console.log("Sending:\n");
|
|
1948
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1949
|
+
const { ok, detail } = await sendReport(report);
|
|
1950
|
+
console.log(ok ? "\nSent. Thank you." : `
|
|
1951
|
+
Could not send (${detail}). Paste the JSON above into an issue instead.`);
|
|
1952
|
+
}
|
|
1953
|
+
function maybeSendInBackground() {
|
|
1954
|
+
if (!telemetryEnabled()) return;
|
|
1955
|
+
const last = readConfig().lastReportAt;
|
|
1956
|
+
if (last && Date.now() - Date.parse(last) < 24 * 60 * 60 * 1e3) return;
|
|
1957
|
+
const report = buildReport();
|
|
1958
|
+
if (!report) return;
|
|
1959
|
+
void sendReport(report).then(({ ok }) => {
|
|
1960
|
+
if (ok) writeConfig({ lastReportAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1682
1963
|
async function main() {
|
|
1683
1964
|
const command = process.argv[2];
|
|
1684
1965
|
if (command === "login") {
|
|
1685
1966
|
await login(process.argv.includes("--forget"));
|
|
1686
1967
|
return;
|
|
1687
1968
|
}
|
|
1969
|
+
if (command === "telemetry") {
|
|
1970
|
+
await telemetry(process.argv[3]);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (command === "report") {
|
|
1974
|
+
await reportOnce();
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (command === "stats") {
|
|
1978
|
+
const rest = process.argv.slice(3);
|
|
1979
|
+
stats(rest.find((a) => !a.startsWith("--")), rest.includes("--json"));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1688
1982
|
if (command && command !== "serve") {
|
|
1689
|
-
console.error(
|
|
1983
|
+
console.error(
|
|
1984
|
+
`Unknown command "${command}". Use: drawpro-mcp [serve|login|stats|telemetry|report]`
|
|
1985
|
+
);
|
|
1690
1986
|
process.exit(2);
|
|
1691
1987
|
}
|
|
1988
|
+
maybeSendInBackground();
|
|
1692
1989
|
await server.connect(new import_stdio.StdioServerTransport());
|
|
1693
1990
|
}
|
|
1694
1991
|
main().catch((err) => {
|