@drawpro/mcp 0.1.0 → 0.2.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 +26 -2
  2. package/dist/server.js +98 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,11 +6,18 @@ account from Claude Code, Claude Desktop, or any MCP client.
6
6
  ## Setup
7
7
 
8
8
  ```bash
9
- claude mcp add drawpro -e DRAWPRO_TOKEN="dp_live_..." -- npx -y @drawpro/mcp
9
+ claude mcp add drawpro --scope user -e DRAWPRO_TOKEN="dp_live_..." -- npx -y @drawpro/mcp
10
10
  ```
11
11
 
12
12
  Mint the token in DrawPro under **Connect to Claude Code**.
13
13
 
14
+ `--scope user` registers it for every project on your machine. The scope
15
+ otherwise defaults to `local`, which loads the server only in the directory the
16
+ command was run in — diagramming is not a property of one repo, so that default
17
+ is almost never what you want here. Avoid `--scope project`: it writes
18
+ `.mcp.json` into whichever repo you are standing in, token included, where it
19
+ invites being committed.
20
+
14
21
  To read existing sheets, unlock the account once:
15
22
 
16
23
  ```bash
@@ -67,7 +74,24 @@ passcode itself.
67
74
  | `read_sheet` | yes | returns a readable outline, not raw scene JSON |
68
75
  | `validate_spec` | no | no side effects; check before writing |
69
76
  | `create_diagram` | no | returns a link to the new sheet |
70
- | `update_diagram` | no | replaces the sheet wholesale |
77
+ | `update_diagram` | no | replaces the sheet wholesale — regenerates layout |
78
+ | `edit_sheet_text` | yes | rewrites named text in place, preserving every other element |
79
+
80
+ ### Editing a sheet a person drew
81
+
82
+ `update_diagram` builds the scene from a spec, so layout is derived and
83
+ hand-placed content cannot survive it: text elements positioned by hand, region
84
+ containers, and unbound annotation arrows all disappear, and the sheet comes
85
+ back as an auto-laid-out graph. Correct content, different diagram.
86
+
87
+ Use `edit_sheet_text` for those sheets. It reads the scene, rewrites only the
88
+ strings you name, and writes every other element back byte for byte — so
89
+ coordinates, groupings, and annotations are untouched. Edits apply
90
+ all-or-nothing: if any `find` matches nothing, the sheet is left alone rather
91
+ than half-updated.
92
+
93
+ `update_diagram` remains the right tool for sheets this package generated, where
94
+ regenerating the layout is the point.
71
95
 
72
96
  `read_sheet` deliberately returns shapes and edges rather than Excalidraw JSON.
73
97
  A real sheet's raw scene runs to tens of thousands of characters of coordinates,
package/dist/server.js CHANGED
@@ -337,23 +337,51 @@ function forgetKey(email) {
337
337
  }
338
338
 
339
339
  // ../client/src/prompt.ts
340
- var import_node_readline = require("node:readline");
341
- var CLEAR_LINE = "\x1B[2K\x1B[200D";
342
340
  function askHidden(question) {
343
341
  return new Promise((resolve) => {
344
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
342
+ const input = process.stdin;
345
343
  process.stdout.write(question);
346
- const onData = () => {
347
- const typed = rl.line ?? "";
348
- process.stdout.write(CLEAR_LINE + question + "*".repeat(typed.length));
349
- };
350
- process.stdin.on("data", onData);
351
- rl.question("", (answer) => {
352
- process.stdin.removeListener("data", onData);
353
- rl.close();
344
+ if (!input.isTTY) {
345
+ let buffered = "";
346
+ input.setEncoding("utf8");
347
+ input.on("data", (chunk) => {
348
+ buffered += chunk;
349
+ });
350
+ input.on("end", () => resolve(buffered.split("\n")[0].trim()));
351
+ return;
352
+ }
353
+ const wasRaw = input.isRaw;
354
+ input.setRawMode(true);
355
+ input.resume();
356
+ input.setEncoding("utf8");
357
+ let value = "";
358
+ const finish = (result) => {
359
+ input.removeListener("data", onData);
360
+ input.setRawMode(wasRaw);
361
+ input.pause();
354
362
  process.stdout.write("\n");
355
- resolve(answer);
356
- });
363
+ if (result === null) {
364
+ process.exit(130);
365
+ }
366
+ resolve(result);
367
+ };
368
+ const onData = (chunk) => {
369
+ for (const ch of chunk) {
370
+ if (ch === "\r" || ch === "\n") return finish(value);
371
+ if (ch === "") return finish(null);
372
+ if (ch === "\x7F" || ch === "\b") {
373
+ if (value.length > 0) {
374
+ value = value.slice(0, -1);
375
+ process.stdout.write("\b \b");
376
+ }
377
+ continue;
378
+ }
379
+ if (ch < " ") continue;
380
+ value += ch;
381
+ process.stdout.write("*");
382
+ }
383
+ };
384
+ input.on("data", onData);
357
385
  });
358
386
  }
359
387
 
@@ -1300,7 +1328,7 @@ function api() {
1300
1328
  const token = process.env.DRAWPRO_TOKEN;
1301
1329
  if (!token) {
1302
1330
  throw new ConfigError(
1303
- 'DRAWPRO_TOKEN is not set. Create a token in DrawPro under "Connect to Claude Code", then:\n claude mcp add drawpro -e DRAWPRO_TOKEN="dp_live_..." -- npx -y @drawpro/mcp'
1331
+ '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'
1304
1332
  );
1305
1333
  }
1306
1334
  clientInstance = new DrawProClient(BASE_URL, token);
@@ -1523,6 +1551,62 @@ async function login(forget) {
1523
1551
  console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
1524
1552
  console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
1525
1553
  }
1554
+ server.tool(
1555
+ "edit_sheet_text",
1556
+ "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.",
1557
+ {
1558
+ workspace_id: import_zod.z.string(),
1559
+ sheet_id: import_zod.z.string(),
1560
+ edits: import_zod.z.array(
1561
+ import_zod.z.object({
1562
+ find: import_zod.z.string().describe("The element's exact current text, as read_sheet reports it"),
1563
+ replace: import_zod.z.string()
1564
+ })
1565
+ ).describe("Applied all-or-nothing: if any find has no match, nothing is written.")
1566
+ },
1567
+ async ({ workspace_id, sheet_id, edits }) => {
1568
+ const unlocked = await unlockedKey();
1569
+ if ("error" in unlocked) return text(unlocked.error);
1570
+ const scene = await api().readSheet(workspace_id, sheet_id, unlocked.key);
1571
+ const elements = scene.elements;
1572
+ const counts = /* @__PURE__ */ new Map();
1573
+ for (const el of elements) {
1574
+ if (el.type !== "text") continue;
1575
+ const current = (el.originalText ?? el.text)?.trim();
1576
+ if (current === void 0) continue;
1577
+ const edit = edits.find((e) => e.find.trim() === current);
1578
+ if (!edit) continue;
1579
+ el.text = edit.replace;
1580
+ el.originalText = edit.replace;
1581
+ const metrics = measureText(edit.replace, el.fontSize ?? 20, 1e3);
1582
+ el.width = metrics.width;
1583
+ el.height = metrics.height;
1584
+ el.version = (el.version ?? 1) + 1;
1585
+ el.versionNonce = Math.floor(Math.random() * 2 ** 31);
1586
+ el.updated = Date.now();
1587
+ counts.set(edit.find, (counts.get(edit.find) ?? 0) + 1);
1588
+ }
1589
+ const missed = edits.filter((e) => !counts.has(e.find));
1590
+ if (missed.length > 0) {
1591
+ return text(
1592
+ "Nothing was written. These strings matched no text element:\n" + missed.map((e) => ` ${JSON.stringify(e.find)}`).join("\n") + "\n\nRun read_sheet and copy the text exactly as it appears there."
1593
+ );
1594
+ }
1595
+ const user = await currentUser();
1596
+ await api().updateSheet(
1597
+ workspace_id,
1598
+ sheet_id,
1599
+ { name: scene.name, elements, appState: scene.appState },
1600
+ user.publicKey
1601
+ );
1602
+ const applied = [...counts.entries()].map(([find, n]) => ` ${JSON.stringify(find)} -> ${n} element${n === 1 ? "" : "s"}`).join("\n");
1603
+ return text(
1604
+ `Updated "${scene.name}". ${elements.length} elements preserved; only the text below changed.
1605
+ ${applied}
1606
+ ${sheetUrl(workspace_id, sheet_id)}`
1607
+ );
1608
+ }
1609
+ );
1526
1610
  async function main() {
1527
1611
  const command = process.argv[2];
1528
1612
  if (command === "login") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawpro/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {