@drawpro/mcp 0.1.1 → 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.
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
@@ -1328,7 +1328,7 @@ function api() {
1328
1328
  const token = process.env.DRAWPRO_TOKEN;
1329
1329
  if (!token) {
1330
1330
  throw new ConfigError(
1331
- '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'
1332
1332
  );
1333
1333
  }
1334
1334
  clientInstance = new DrawProClient(BASE_URL, token);
@@ -1551,6 +1551,62 @@ async function login(forget) {
1551
1551
  console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
1552
1552
  console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
1553
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
+ );
1554
1610
  async function main() {
1555
1611
  const command = process.argv[2];
1556
1612
  if (command === "login") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawpro/mcp",
3
- "version": "0.1.1",
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": {