@drawpro/mcp 0.3.0 → 0.4.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 +50 -0
- package/dist/server.js +296 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,56 @@ 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
|
+
Set `DRAWPRO_MCP_LOG` to record every tool call as JSONL:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
claude mcp add drawpro --scope user \
|
|
130
|
+
-e DRAWPRO_TOKEN="dp_live_..." \
|
|
131
|
+
-e DRAWPRO_MCP_LOG="$HOME/.drawpro/usage.jsonl" \
|
|
132
|
+
-- npx -y @drawpro/mcp
|
|
133
|
+
|
|
134
|
+
drawpro-mcp stats # or: npx -y @drawpro/mcp stats
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
5 calls 2026-08-29 .. 2026-08-29
|
|
139
|
+
|
|
140
|
+
tool calls refused failed median
|
|
141
|
+
read_sheet 2 0 0% 0 881ms
|
|
142
|
+
edit_sheet_text 1 1 100% 0 62ms
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Opt-in, and never transmitted. `logCall` does one thing — append to that file —
|
|
146
|
+
and the only outbound request this package can make is to your own DrawPro
|
|
147
|
+
account's sheets. Unset the variable and nothing is written at all; there is no
|
|
148
|
+
default path and no fallback.
|
|
149
|
+
|
|
150
|
+
### How that feeds back into the package
|
|
151
|
+
|
|
152
|
+
The raw log stays on your machine and carries workspace and sheet ids, so you
|
|
153
|
+
can correlate calls against your own account. `stats` is the aggregate: tool
|
|
154
|
+
names, counts, timings, and nothing that identifies an account, a workspace, a
|
|
155
|
+
sheet, or anything drawn on one.
|
|
156
|
+
|
|
157
|
+
That split is the point. Sharing is a decision you make, not a default the
|
|
158
|
+
package makes for you:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
drawpro-mcp stats --json # paste into an issue
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
There is no telemetry endpoint, and adding one to a product built on the server
|
|
165
|
+
never seeing your diagrams would be the wrong trade. If a tool is refusing
|
|
166
|
+
often, the aggregate says so without anyone learning what you were drawing.
|
|
167
|
+
|
|
168
|
+
The column worth watching is **refused**. A tool that frequently declines is one
|
|
169
|
+
whose description is not steering the model well, and that is cheaper to learn
|
|
170
|
+
from real use than from a synthetic eval — both `edit_sheet_text` and
|
|
171
|
+
`import_sheet` exist because real use found them missing, and no eval written
|
|
172
|
+
beforehand would have predicted either.
|
|
173
|
+
|
|
124
174
|
## Tests
|
|
125
175
|
|
|
126
176
|
```bash
|
package/dist/server.js
CHANGED
|
@@ -24,8 +24,8 @@ 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_path3 = require("node:path");
|
|
29
29
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
30
30
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
31
31
|
var import_zod = require("zod");
|
|
@@ -191,11 +191,13 @@ async function decryptSheet(encryptedData, privateKeyBytes) {
|
|
|
191
191
|
|
|
192
192
|
// ../client/src/api.ts
|
|
193
193
|
var DrawProClient = class {
|
|
194
|
-
constructor(baseUrl, token) {
|
|
194
|
+
constructor(baseUrl, token, onRequest) {
|
|
195
195
|
this.baseUrl = baseUrl;
|
|
196
196
|
this.token = token;
|
|
197
|
+
this.onRequest = onRequest;
|
|
197
198
|
}
|
|
198
199
|
async request(path, init) {
|
|
200
|
+
const started = Date.now();
|
|
199
201
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
200
202
|
...init,
|
|
201
203
|
headers: {
|
|
@@ -204,7 +206,19 @@ var DrawProClient = class {
|
|
|
204
206
|
...init?.headers ?? {}
|
|
205
207
|
}
|
|
206
208
|
});
|
|
207
|
-
const
|
|
209
|
+
const raw = await res.text();
|
|
210
|
+
this.onRequest?.({
|
|
211
|
+
method: init?.method ?? "GET",
|
|
212
|
+
path: path.replace(/\/[a-z0-9]{20,}/gi, "/:id"),
|
|
213
|
+
status: res.status,
|
|
214
|
+
ms: Date.now() - started,
|
|
215
|
+
bytes: raw.length
|
|
216
|
+
});
|
|
217
|
+
let body = {};
|
|
218
|
+
try {
|
|
219
|
+
body = JSON.parse(raw);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
208
222
|
if (!res.ok) {
|
|
209
223
|
throw new Error(`${path} -> ${res.status} ${body.error ?? JSON.stringify(body)}`);
|
|
210
224
|
}
|
|
@@ -387,6 +401,35 @@ function askHidden(question) {
|
|
|
387
401
|
});
|
|
388
402
|
}
|
|
389
403
|
|
|
404
|
+
// ../client/src/config.ts
|
|
405
|
+
var import_node_fs2 = require("node:fs");
|
|
406
|
+
var import_node_os2 = require("node:os");
|
|
407
|
+
var import_node_path2 = require("node:path");
|
|
408
|
+
var import_node_crypto3 = require("node:crypto");
|
|
409
|
+
var DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".drawpro");
|
|
410
|
+
var FILE = (0, import_node_path2.join)(DIR, "config.json");
|
|
411
|
+
function readConfig() {
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(FILE, "utf8"));
|
|
414
|
+
} catch {
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function writeConfig(patch) {
|
|
419
|
+
const next = { ...readConfig(), ...patch };
|
|
420
|
+
(0, import_node_fs2.mkdirSync)(DIR, { recursive: true, mode: 448 });
|
|
421
|
+
(0, import_node_fs2.writeFileSync)(FILE, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
422
|
+
return next;
|
|
423
|
+
}
|
|
424
|
+
function installId() {
|
|
425
|
+
const config = readConfig();
|
|
426
|
+
if (config.installId) return config.installId;
|
|
427
|
+
return writeConfig({ installId: (0, import_node_crypto3.randomUUID)() }).installId;
|
|
428
|
+
}
|
|
429
|
+
function telemetryEnabled() {
|
|
430
|
+
return readConfig().telemetry === "on";
|
|
431
|
+
}
|
|
432
|
+
|
|
390
433
|
// ../diagram/src/ids.ts
|
|
391
434
|
var import_fractional_indexing = require("fractional-indexing");
|
|
392
435
|
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
|
@@ -1358,10 +1401,13 @@ function api() {
|
|
|
1358
1401
|
'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
1402
|
);
|
|
1360
1403
|
}
|
|
1361
|
-
clientInstance = new DrawProClient(BASE_URL, token);
|
|
1404
|
+
clientInstance = new DrawProClient(BASE_URL, token, (t) => inFlight.push(t));
|
|
1362
1405
|
}
|
|
1363
1406
|
return clientInstance;
|
|
1364
1407
|
}
|
|
1408
|
+
var SESSION_ID = Math.random().toString(36).slice(2, 10);
|
|
1409
|
+
var VERSION = "0.4.0";
|
|
1410
|
+
var inFlight = [];
|
|
1365
1411
|
var cachedUser = null;
|
|
1366
1412
|
async function currentUser() {
|
|
1367
1413
|
if (!cachedUser) cachedUser = await api().me();
|
|
@@ -1386,8 +1432,83 @@ function text(body) {
|
|
|
1386
1432
|
function sheetUrl(workspaceId, sheetId) {
|
|
1387
1433
|
return `${APP_URL}/workspace/${workspaceId}/sheet/${sheetId}`;
|
|
1388
1434
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1435
|
+
function logCall(entry) {
|
|
1436
|
+
const path = process.env.DRAWPRO_MCP_LOG;
|
|
1437
|
+
if (!path) return;
|
|
1438
|
+
try {
|
|
1439
|
+
(0, import_node_fs3.appendFileSync)(path, JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }) + "\n");
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
var REFUSALS = [
|
|
1444
|
+
"Nothing was written",
|
|
1445
|
+
"The diagram was not created",
|
|
1446
|
+
"is locked",
|
|
1447
|
+
"Could not read",
|
|
1448
|
+
'has no "elements" array'
|
|
1449
|
+
];
|
|
1450
|
+
function summarise(args) {
|
|
1451
|
+
const spec = args.spec;
|
|
1452
|
+
const edits = args.edits;
|
|
1453
|
+
return {
|
|
1454
|
+
workspace_id: args.workspace_id,
|
|
1455
|
+
sheet_id: args.sheet_id,
|
|
1456
|
+
...spec ? { nodes: spec.nodes?.length ?? 0, edges: spec.edges?.length ?? 0 } : {},
|
|
1457
|
+
...edits ? { edits: edits.length } : {},
|
|
1458
|
+
...args.file_path ? { from_file: true } : {}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function instrument(name, handler) {
|
|
1462
|
+
return (async (args) => {
|
|
1463
|
+
const started = Date.now();
|
|
1464
|
+
inFlight = [];
|
|
1465
|
+
try {
|
|
1466
|
+
const result = await handler(args);
|
|
1467
|
+
const body = result.content.map((c) => c.text).join("\n");
|
|
1468
|
+
const apiMs = inFlight.reduce((sum, t) => sum + t.ms, 0);
|
|
1469
|
+
const total = Date.now() - started;
|
|
1470
|
+
logCall({
|
|
1471
|
+
session: SESSION_ID,
|
|
1472
|
+
tool: name,
|
|
1473
|
+
ok: true,
|
|
1474
|
+
refused: REFUSALS.some((r) => body.includes(r)),
|
|
1475
|
+
ms: total,
|
|
1476
|
+
api_ms: apiMs,
|
|
1477
|
+
local_ms: total - apiMs,
|
|
1478
|
+
requests: inFlight.map((t) => ({ m: t.method, p: t.path, s: t.status, ms: t.ms, bytes: t.bytes })),
|
|
1479
|
+
args: summarise(args),
|
|
1480
|
+
// Tools word this two ways: "38 elements" and "elements: 38".
|
|
1481
|
+
elements: Number(body.match(/(\d+) elements/)?.[1] ?? body.match(/elements: (\d+)/)?.[1]) || void 0
|
|
1482
|
+
});
|
|
1483
|
+
return result;
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
const apiMs = inFlight.reduce((sum, t) => sum + t.ms, 0);
|
|
1486
|
+
const total = Date.now() - started;
|
|
1487
|
+
logCall({
|
|
1488
|
+
session: SESSION_ID,
|
|
1489
|
+
tool: name,
|
|
1490
|
+
ok: false,
|
|
1491
|
+
ms: total,
|
|
1492
|
+
api_ms: apiMs,
|
|
1493
|
+
local_ms: total - apiMs,
|
|
1494
|
+
requests: inFlight.map((t) => ({ m: t.method, p: t.path, s: t.status, ms: t.ms, bytes: t.bytes })),
|
|
1495
|
+
args: summarise(args),
|
|
1496
|
+
error: err.message.slice(0, 200)
|
|
1497
|
+
});
|
|
1498
|
+
throw err;
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
var server = new import_mcp.McpServer({ name: "drawpro", version: VERSION });
|
|
1503
|
+
var tool = ((name, ...rest) => {
|
|
1504
|
+
const handler = rest.pop();
|
|
1505
|
+
return server.tool(
|
|
1506
|
+
name,
|
|
1507
|
+
...rest,
|
|
1508
|
+
instrument(name, handler)
|
|
1509
|
+
);
|
|
1510
|
+
});
|
|
1511
|
+
tool(
|
|
1391
1512
|
"list_workspaces",
|
|
1392
1513
|
"List the DrawPro workspaces this account can access. Workspace names are encrypted at rest and are only readable once the account is unlocked.",
|
|
1393
1514
|
{},
|
|
@@ -1406,7 +1527,7 @@ Names are encrypted. ${unlocked.error}` : "";
|
|
|
1406
1527
|
return text(rows.join("\n") + note);
|
|
1407
1528
|
}
|
|
1408
1529
|
);
|
|
1409
|
-
|
|
1530
|
+
tool(
|
|
1410
1531
|
"list_sheets",
|
|
1411
1532
|
"List the sheets in a DrawPro workspace, with their decrypted names.",
|
|
1412
1533
|
{ workspace_id: import_zod.z.string().describe("Workspace id, from list_workspaces") },
|
|
@@ -1425,7 +1546,7 @@ Names are encrypted. ${unlocked.error}` : "";
|
|
|
1425
1546
|
return text((rows.join("\n") || "(no sheets)") + note);
|
|
1426
1547
|
}
|
|
1427
1548
|
);
|
|
1428
|
-
|
|
1549
|
+
tool(
|
|
1429
1550
|
"read_sheet",
|
|
1430
1551
|
"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
1552
|
{
|
|
@@ -1470,7 +1591,7 @@ var specShape = {
|
|
|
1470
1591
|
"What connects to what. Layout, sizing, text wrapping, and arrow binding are derived \u2014 never supply coordinates."
|
|
1471
1592
|
)
|
|
1472
1593
|
};
|
|
1473
|
-
|
|
1594
|
+
tool(
|
|
1474
1595
|
"validate_spec",
|
|
1475
1596
|
"Check a diagram spec without creating anything. Use this before writing if the diagram is large or the spec was assembled programmatically.",
|
|
1476
1597
|
specShape,
|
|
@@ -1492,7 +1613,7 @@ function buildOrExplain(spec) {
|
|
|
1492
1613
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
1493
1614
|
return { ok: true, scene, warnings: warnings.map((w) => w.message) };
|
|
1494
1615
|
}
|
|
1495
|
-
|
|
1616
|
+
tool(
|
|
1496
1617
|
"create_diagram",
|
|
1497
1618
|
"Create a new sheet in DrawPro from a diagram spec. Returns a link to open it.",
|
|
1498
1619
|
{
|
|
@@ -1519,7 +1640,7 @@ ${sheetUrl(workspace_id, sheet.id)}${warned}`
|
|
|
1519
1640
|
);
|
|
1520
1641
|
}
|
|
1521
1642
|
);
|
|
1522
|
-
|
|
1643
|
+
tool(
|
|
1523
1644
|
"update_diagram",
|
|
1524
1645
|
"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
1646
|
{
|
|
@@ -1578,7 +1699,7 @@ async function login(forget) {
|
|
|
1578
1699
|
console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
|
|
1579
1700
|
console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
|
|
1580
1701
|
}
|
|
1581
|
-
|
|
1702
|
+
tool(
|
|
1582
1703
|
"edit_sheet_text",
|
|
1583
1704
|
"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
1705
|
{
|
|
@@ -1640,7 +1761,7 @@ ${sheetUrl(workspace_id, sheet_id)}`
|
|
|
1640
1761
|
);
|
|
1641
1762
|
}
|
|
1642
1763
|
);
|
|
1643
|
-
|
|
1764
|
+
tool(
|
|
1644
1765
|
"import_sheet",
|
|
1645
1766
|
"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
1767
|
{
|
|
@@ -1652,7 +1773,7 @@ server.tool(
|
|
|
1652
1773
|
async ({ workspace_id, file_path, name, sheet_id }) => {
|
|
1653
1774
|
let parsed;
|
|
1654
1775
|
try {
|
|
1655
|
-
parsed = JSON.parse((0,
|
|
1776
|
+
parsed = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(file_path), "utf8"));
|
|
1656
1777
|
} catch (err) {
|
|
1657
1778
|
return text(`Could not read ${file_path}: ${err.message}`);
|
|
1658
1779
|
}
|
|
@@ -1679,16 +1800,175 @@ ${sheetUrl(workspace_id, id)}${noted}`
|
|
|
1679
1800
|
);
|
|
1680
1801
|
}
|
|
1681
1802
|
);
|
|
1803
|
+
var WRITE_TOOLS = ["create_diagram", "update_diagram", "edit_sheet_text", "import_sheet"];
|
|
1804
|
+
function summariseLog(file) {
|
|
1805
|
+
let rows;
|
|
1806
|
+
try {
|
|
1807
|
+
rows = (0, import_node_fs3.readFileSync)(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
1808
|
+
try {
|
|
1809
|
+
return [JSON.parse(line)];
|
|
1810
|
+
} catch {
|
|
1811
|
+
return [];
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
} catch {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
if (rows.length === 0) return null;
|
|
1818
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
1819
|
+
for (const r of rows) {
|
|
1820
|
+
const name = String(r.tool);
|
|
1821
|
+
const acc = byTool.get(name) ?? { n: 0, refused: 0, failed: 0, ms: [] };
|
|
1822
|
+
acc.n++;
|
|
1823
|
+
if (r.refused) acc.refused++;
|
|
1824
|
+
if (r.ok === false) acc.failed++;
|
|
1825
|
+
if (typeof r.ms === "number") acc.ms.push(r.ms);
|
|
1826
|
+
byTool.set(name, acc);
|
|
1827
|
+
}
|
|
1828
|
+
const median = (xs) => xs.length ? [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] : 0;
|
|
1829
|
+
return {
|
|
1830
|
+
calls: rows.length,
|
|
1831
|
+
writes: rows.filter(
|
|
1832
|
+
(r) => WRITE_TOOLS.includes(String(r.tool)) && !r.refused && r.ok !== false
|
|
1833
|
+
).length,
|
|
1834
|
+
from: String(rows[0].ts).slice(0, 10),
|
|
1835
|
+
to: String(rows[rows.length - 1].ts).slice(0, 10),
|
|
1836
|
+
tools: [...byTool.entries()].sort((x, y) => y[1].n - x[1].n).map(([tool2, a]) => ({
|
|
1837
|
+
tool: tool2,
|
|
1838
|
+
calls: a.n,
|
|
1839
|
+
refused: a.refused,
|
|
1840
|
+
failed: a.failed,
|
|
1841
|
+
median_ms: median(a.ms)
|
|
1842
|
+
}))
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function stats(path, asJson) {
|
|
1846
|
+
const file = path ?? process.env.DRAWPRO_MCP_LOG;
|
|
1847
|
+
if (!file) {
|
|
1848
|
+
console.error("Set DRAWPRO_MCP_LOG, or pass a path: drawpro-mcp stats <file>");
|
|
1849
|
+
process.exit(2);
|
|
1850
|
+
}
|
|
1851
|
+
const summary = summariseLog(file);
|
|
1852
|
+
if (!summary) {
|
|
1853
|
+
console.log(asJson ? '{"calls":0}' : "No calls recorded yet.");
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (asJson) {
|
|
1857
|
+
console.log(JSON.stringify({ version: VERSION, ...summary }, null, 2));
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
console.log(`${summary.calls} calls ${summary.from} .. ${summary.to}
|
|
1861
|
+
`);
|
|
1862
|
+
console.log(" tool calls refused failed median");
|
|
1863
|
+
for (const t of summary.tools) {
|
|
1864
|
+
const pct = t.calls ? Math.round(t.refused / t.calls * 100) : 0;
|
|
1865
|
+
console.log(
|
|
1866
|
+
` ${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`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
console.log(`
|
|
1870
|
+
${summary.writes} successful writes to sheets`);
|
|
1871
|
+
console.log(
|
|
1872
|
+
"\n No ids, account details, or diagram content above \u2014 safe to paste into a bug report."
|
|
1873
|
+
);
|
|
1874
|
+
console.log(" Add --json for a machine-readable copy.");
|
|
1875
|
+
}
|
|
1876
|
+
function buildReport() {
|
|
1877
|
+
const file = process.env.DRAWPRO_MCP_LOG;
|
|
1878
|
+
if (!file) return null;
|
|
1879
|
+
const summary = summariseLog(file);
|
|
1880
|
+
if (!summary) return null;
|
|
1881
|
+
return {
|
|
1882
|
+
installId: installId(),
|
|
1883
|
+
mcpVersion: VERSION,
|
|
1884
|
+
calls: summary.calls,
|
|
1885
|
+
writes: summary.writes,
|
|
1886
|
+
tools: summary.tools
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
async function sendReport(report) {
|
|
1890
|
+
try {
|
|
1891
|
+
const res = await fetch(`${BASE_URL}/telemetry`, {
|
|
1892
|
+
method: "POST",
|
|
1893
|
+
headers: { "Content-Type": "application/json" },
|
|
1894
|
+
body: JSON.stringify(report)
|
|
1895
|
+
});
|
|
1896
|
+
return { ok: res.ok, detail: `HTTP ${res.status}` };
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
return { ok: false, detail: err.message };
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async function telemetry(action) {
|
|
1902
|
+
if (action === "off") {
|
|
1903
|
+
writeConfig({ telemetry: "off" });
|
|
1904
|
+
console.log("Telemetry off. Nothing will be sent.");
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const report = buildReport();
|
|
1908
|
+
if (action === "on") {
|
|
1909
|
+
writeConfig({ telemetry: "on" });
|
|
1910
|
+
console.log("Telemetry on. Roughly once a day, this is sent:\n");
|
|
1911
|
+
console.log(report ? JSON.stringify(report, null, 2) : " (nothing yet \u2014 set DRAWPRO_MCP_LOG to record usage)");
|
|
1912
|
+
console.log("\nTurn it off any time with: drawpro-mcp telemetry off");
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
console.log(`Telemetry is ${telemetryEnabled() ? "ON" : "OFF"}.
|
|
1916
|
+
`);
|
|
1917
|
+
console.log("If enabled, this is the entire payload \u2014 tool counts and timings,");
|
|
1918
|
+
console.log("no account, no token, no workspace or sheet ids, nothing drawn:\n");
|
|
1919
|
+
console.log(report ? JSON.stringify(report, null, 2) : " (nothing recorded yet \u2014 set DRAWPRO_MCP_LOG first)");
|
|
1920
|
+
console.log("\n drawpro-mcp telemetry on share it");
|
|
1921
|
+
console.log(" drawpro-mcp telemetry off stop");
|
|
1922
|
+
console.log(" drawpro-mcp report send once now, without turning it on");
|
|
1923
|
+
}
|
|
1924
|
+
async function reportOnce() {
|
|
1925
|
+
const report = buildReport();
|
|
1926
|
+
if (!report) {
|
|
1927
|
+
console.error("Nothing to report. Set DRAWPRO_MCP_LOG to record usage first.");
|
|
1928
|
+
process.exit(2);
|
|
1929
|
+
}
|
|
1930
|
+
console.log("Sending:\n");
|
|
1931
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1932
|
+
const { ok, detail } = await sendReport(report);
|
|
1933
|
+
console.log(ok ? "\nSent. Thank you." : `
|
|
1934
|
+
Could not send (${detail}). Paste the JSON above into an issue instead.`);
|
|
1935
|
+
}
|
|
1936
|
+
function maybeSendInBackground() {
|
|
1937
|
+
if (!telemetryEnabled()) return;
|
|
1938
|
+
const last = readConfig().lastReportAt;
|
|
1939
|
+
if (last && Date.now() - Date.parse(last) < 24 * 60 * 60 * 1e3) return;
|
|
1940
|
+
const report = buildReport();
|
|
1941
|
+
if (!report) return;
|
|
1942
|
+
void sendReport(report).then(({ ok }) => {
|
|
1943
|
+
if (ok) writeConfig({ lastReportAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1682
1946
|
async function main() {
|
|
1683
1947
|
const command = process.argv[2];
|
|
1684
1948
|
if (command === "login") {
|
|
1685
1949
|
await login(process.argv.includes("--forget"));
|
|
1686
1950
|
return;
|
|
1687
1951
|
}
|
|
1952
|
+
if (command === "telemetry") {
|
|
1953
|
+
await telemetry(process.argv[3]);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (command === "report") {
|
|
1957
|
+
await reportOnce();
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (command === "stats") {
|
|
1961
|
+
const rest = process.argv.slice(3);
|
|
1962
|
+
stats(rest.find((a) => !a.startsWith("--")), rest.includes("--json"));
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1688
1965
|
if (command && command !== "serve") {
|
|
1689
|
-
console.error(
|
|
1966
|
+
console.error(
|
|
1967
|
+
`Unknown command "${command}". Use: drawpro-mcp [serve|login|stats|telemetry|report]`
|
|
1968
|
+
);
|
|
1690
1969
|
process.exit(2);
|
|
1691
1970
|
}
|
|
1971
|
+
maybeSendInBackground();
|
|
1692
1972
|
await server.connect(new import_stdio.StdioServerTransport());
|
|
1693
1973
|
}
|
|
1694
1974
|
main().catch((err) => {
|