@drawpro/mcp 0.2.1 → 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.
Files changed (3) hide show
  1. package/README.md +61 -2
  2. package/dist/server.js +336 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,6 +76,7 @@ passcode itself.
76
76
  | `create_diagram` | no | returns a link to the new sheet |
77
77
  | `update_diagram` | no | replaces the sheet wholesale — regenerates layout |
78
78
  | `edit_sheet_text` | yes | rewrites named text in place, preserving every other element |
79
+ | `import_sheet` | no | writes a local .excalidraw file into a sheet, coordinates verbatim |
79
80
 
80
81
  ### Editing a sheet a person drew
81
82
 
@@ -97,8 +98,16 @@ and nothing else moves — reflowing neighbours would be the wholesale rewrite
97
98
  this tool exists to avoid. Any box it grows is named in the result, so overlap
98
99
  with a neighbour is easy to spot and one drag to fix.
99
100
 
100
- It cannot add elements. A correction that needs a new box or a new annotation
101
- line is still a manual edit.
101
+ It cannot add elements or move anything. A correction that needs a new box, a
102
+ repositioned column, or a re-anchored arrow is a geometry change, and no spec or
103
+ text edit can express one.
104
+
105
+ `import_sheet` covers that case. Point it at a local `.excalidraw` file and it
106
+ writes the scene in verbatim — every coordinate exactly as in the file. Because
107
+ the server runs on your machine it can simply read the file, so a geometry pass
108
+ can be done with a real editor, or by a model editing the file directly, and
109
+ then pushed without a clipboard round trip. The file's contents go into the
110
+ encrypted blob, never into the model's context.
102
111
 
103
112
  `update_diagram` remains the right tool for sheets this package generated, where
104
113
  regenerating the layout is the point.
@@ -112,6 +121,56 @@ Diagram specs describe *what connects to what*. Layout, sizing, text wrapping,
112
121
  and arrow binding are derived by `@drawpro/diagram` — a spec never contains
113
122
  coordinates.
114
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
+
115
174
  ## Tests
116
175
 
117
176
  ```bash
package/dist/server.js CHANGED
@@ -24,6 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/server.ts
27
+ var import_node_fs3 = require("node:fs");
28
+ var import_node_path3 = require("node:path");
27
29
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28
30
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
31
  var import_zod = require("zod");
@@ -189,11 +191,13 @@ async function decryptSheet(encryptedData, privateKeyBytes) {
189
191
 
190
192
  // ../client/src/api.ts
191
193
  var DrawProClient = class {
192
- constructor(baseUrl, token) {
194
+ constructor(baseUrl, token, onRequest) {
193
195
  this.baseUrl = baseUrl;
194
196
  this.token = token;
197
+ this.onRequest = onRequest;
195
198
  }
196
199
  async request(path, init) {
200
+ const started = Date.now();
197
201
  const res = await fetch(`${this.baseUrl}${path}`, {
198
202
  ...init,
199
203
  headers: {
@@ -202,7 +206,19 @@ var DrawProClient = class {
202
206
  ...init?.headers ?? {}
203
207
  }
204
208
  });
205
- const body = await res.json().catch(() => ({}));
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
+ }
206
222
  if (!res.ok) {
207
223
  throw new Error(`${path} -> ${res.status} ${body.error ?? JSON.stringify(body)}`);
208
224
  }
@@ -338,7 +354,7 @@ function forgetKey(email) {
338
354
 
339
355
  // ../client/src/prompt.ts
340
356
  function askHidden(question) {
341
- return new Promise((resolve) => {
357
+ return new Promise((resolve2) => {
342
358
  const input = process.stdin;
343
359
  process.stdout.write(question);
344
360
  if (!input.isTTY) {
@@ -347,7 +363,7 @@ function askHidden(question) {
347
363
  input.on("data", (chunk) => {
348
364
  buffered += chunk;
349
365
  });
350
- input.on("end", () => resolve(buffered.split("\n")[0].trim()));
366
+ input.on("end", () => resolve2(buffered.split("\n")[0].trim()));
351
367
  return;
352
368
  }
353
369
  const wasRaw = input.isRaw;
@@ -363,7 +379,7 @@ function askHidden(question) {
363
379
  if (result === null) {
364
380
  process.exit(130);
365
381
  }
366
- resolve(result);
382
+ resolve2(result);
367
383
  };
368
384
  const onData = (chunk) => {
369
385
  for (const ch of chunk) {
@@ -385,6 +401,35 @@ function askHidden(question) {
385
401
  });
386
402
  }
387
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
+
388
433
  // ../diagram/src/ids.ts
389
434
  var import_fractional_indexing = require("fractional-indexing");
390
435
  var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
@@ -1356,10 +1401,13 @@ function api() {
1356
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'
1357
1402
  );
1358
1403
  }
1359
- clientInstance = new DrawProClient(BASE_URL, token);
1404
+ clientInstance = new DrawProClient(BASE_URL, token, (t) => inFlight.push(t));
1360
1405
  }
1361
1406
  return clientInstance;
1362
1407
  }
1408
+ var SESSION_ID = Math.random().toString(36).slice(2, 10);
1409
+ var VERSION = "0.4.0";
1410
+ var inFlight = [];
1363
1411
  var cachedUser = null;
1364
1412
  async function currentUser() {
1365
1413
  if (!cachedUser) cachedUser = await api().me();
@@ -1384,8 +1432,83 @@ function text(body) {
1384
1432
  function sheetUrl(workspaceId, sheetId) {
1385
1433
  return `${APP_URL}/workspace/${workspaceId}/sheet/${sheetId}`;
1386
1434
  }
1387
- var server = new import_mcp.McpServer({ name: "drawpro", version: "0.0.1" });
1388
- server.tool(
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(
1389
1512
  "list_workspaces",
1390
1513
  "List the DrawPro workspaces this account can access. Workspace names are encrypted at rest and are only readable once the account is unlocked.",
1391
1514
  {},
@@ -1404,7 +1527,7 @@ Names are encrypted. ${unlocked.error}` : "";
1404
1527
  return text(rows.join("\n") + note);
1405
1528
  }
1406
1529
  );
1407
- server.tool(
1530
+ tool(
1408
1531
  "list_sheets",
1409
1532
  "List the sheets in a DrawPro workspace, with their decrypted names.",
1410
1533
  { workspace_id: import_zod.z.string().describe("Workspace id, from list_workspaces") },
@@ -1423,7 +1546,7 @@ Names are encrypted. ${unlocked.error}` : "";
1423
1546
  return text((rows.join("\n") || "(no sheets)") + note);
1424
1547
  }
1425
1548
  );
1426
- server.tool(
1549
+ tool(
1427
1550
  "read_sheet",
1428
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.",
1429
1552
  {
@@ -1468,7 +1591,7 @@ var specShape = {
1468
1591
  "What connects to what. Layout, sizing, text wrapping, and arrow binding are derived \u2014 never supply coordinates."
1469
1592
  )
1470
1593
  };
1471
- server.tool(
1594
+ tool(
1472
1595
  "validate_spec",
1473
1596
  "Check a diagram spec without creating anything. Use this before writing if the diagram is large or the spec was assembled programmatically.",
1474
1597
  specShape,
@@ -1490,7 +1613,7 @@ function buildOrExplain(spec) {
1490
1613
  const warnings = issues.filter((i) => i.level === "warning");
1491
1614
  return { ok: true, scene, warnings: warnings.map((w) => w.message) };
1492
1615
  }
1493
- server.tool(
1616
+ tool(
1494
1617
  "create_diagram",
1495
1618
  "Create a new sheet in DrawPro from a diagram spec. Returns a link to open it.",
1496
1619
  {
@@ -1517,7 +1640,7 @@ ${sheetUrl(workspace_id, sheet.id)}${warned}`
1517
1640
  );
1518
1641
  }
1519
1642
  );
1520
- server.tool(
1643
+ tool(
1521
1644
  "update_diagram",
1522
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.",
1523
1646
  {
@@ -1576,7 +1699,7 @@ async function login(forget) {
1576
1699
  console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
1577
1700
  console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
1578
1701
  }
1579
- server.tool(
1702
+ tool(
1580
1703
  "edit_sheet_text",
1581
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.",
1582
1705
  {
@@ -1638,16 +1761,214 @@ ${sheetUrl(workspace_id, sheet_id)}`
1638
1761
  );
1639
1762
  }
1640
1763
  );
1764
+ tool(
1765
+ "import_sheet",
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.",
1767
+ {
1768
+ workspace_id: import_zod.z.string(),
1769
+ file_path: import_zod.z.string().describe("Path to a .excalidraw file on this machine"),
1770
+ name: import_zod.z.string().describe("Sheet name"),
1771
+ sheet_id: import_zod.z.string().optional().describe("Omit to create a new sheet; supply it to replace an existing one")
1772
+ },
1773
+ async ({ workspace_id, file_path, name, sheet_id }) => {
1774
+ let parsed;
1775
+ try {
1776
+ parsed = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(file_path), "utf8"));
1777
+ } catch (err) {
1778
+ return text(`Could not read ${file_path}: ${err.message}`);
1779
+ }
1780
+ const elements = parsed.elements;
1781
+ if (!Array.isArray(elements) || elements.length === 0) {
1782
+ return text(`${file_path} has no "elements" array \u2014 is it an .excalidraw file?`);
1783
+ }
1784
+ const issues = validateScene(elements);
1785
+ const noted = issues.length ? `
1786
+
1787
+ ${issues.length} thing(s) worth a look (imported anyway):
1788
+ ` + issues.slice(0, 5).map((i) => ` ${i.level}: ${i.message}`).join("\n") : "";
1789
+ const user = await currentUser();
1790
+ const payload = {
1791
+ name,
1792
+ elements,
1793
+ appState: parsed.appState ?? {}
1794
+ };
1795
+ const sheet = sheet_id ? await api().updateSheet(workspace_id, sheet_id, payload, user.publicKey) : await api().createSheet(workspace_id, payload, user.publicKey);
1796
+ const id = sheet_id ?? sheet.id;
1797
+ return text(
1798
+ `${sheet_id ? "Replaced" : "Created"} "${name}" from ${file_path} \u2014 ${elements.length} elements, every coordinate as in the file.
1799
+ ${sheetUrl(workspace_id, id)}${noted}`
1800
+ );
1801
+ }
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
+ }
1641
1946
  async function main() {
1642
1947
  const command = process.argv[2];
1643
1948
  if (command === "login") {
1644
1949
  await login(process.argv.includes("--forget"));
1645
1950
  return;
1646
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
+ }
1647
1965
  if (command && command !== "serve") {
1648
- console.error(`Unknown command "${command}". Use: drawpro-mcp [serve|login] [--forget]`);
1966
+ console.error(
1967
+ `Unknown command "${command}". Use: drawpro-mcp [serve|login|stats|telemetry|report]`
1968
+ );
1649
1969
  process.exit(2);
1650
1970
  }
1971
+ maybeSendInBackground();
1651
1972
  await server.connect(new import_stdio.StdioServerTransport());
1652
1973
  }
1653
1974
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawpro/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for DrawPro \u2014 read and create end-to-end encrypted Excalidraw diagrams from Claude",
5
5
  "license": "MIT",
6
6
  "repository": {