@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 +26 -2
- package/dist/server.js +57 -1
- 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
|
@@ -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") {
|