@drawpro/mcp 0.2.0 → 0.2.1

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
@@ -85,10 +85,20 @@ containers, and unbound annotation arrows all disappear, and the sheet comes
85
85
  back as an auto-laid-out graph. Correct content, different diagram.
86
86
 
87
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.
88
+ strings you name, and writes every other element back — so coordinates,
89
+ groupings, and annotations are untouched. Edits apply all-or-nothing: if any
90
+ `find` matches nothing, the sheet is left alone rather than half-updated.
91
+
92
+ The one geometry change it makes is growing a box whose text no longer fits.
93
+ Excalidraw regrows a container around text *bound* to it, but hand-drawn
94
+ diagrams usually have text merely sitting on top of a shape, so longer text
95
+ would otherwise spill out the bottom. Boxes are only ever grown, never shrunk,
96
+ and nothing else moves — reflowing neighbours would be the wholesale rewrite
97
+ this tool exists to avoid. Any box it grows is named in the result, so overlap
98
+ with a neighbour is easy to spot and one drag to fix.
99
+
100
+ It cannot add elements. A correction that needs a new box or a new annotation
101
+ line is still a manual edit.
92
102
 
93
103
  `update_diagram` remains the right tool for sheets this package generated, where
94
104
  regenerating the layout is the point.
package/dist/server.js CHANGED
@@ -1084,8 +1084,33 @@ function validateScene(elements) {
1084
1084
  return issues;
1085
1085
  }
1086
1086
 
1087
- // ../diagram/src/describe.ts
1087
+ // ../diagram/src/fit.ts
1088
1088
  var SHAPES = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond"]);
1089
+ var PADDING = 16;
1090
+ function growBoxesToFitText(elements, changedTextIds) {
1091
+ const shapes = elements.filter((el) => SHAPES.has(el.type));
1092
+ const resizes = [];
1093
+ for (const text2 of elements) {
1094
+ if (text2.type !== "text" || !changedTextIds.has(text2.id)) continue;
1095
+ if (text2.containerId) continue;
1096
+ const cx = text2.x + text2.width / 2;
1097
+ const cy = text2.y + text2.height / 2;
1098
+ const box = shapes.filter((sh) => cx >= sh.x && cx <= sh.x + sh.width && cy >= sh.y && cy <= sh.y + sh.height).sort((a, b) => a.width * a.height - b.width * b.height)[0];
1099
+ if (!box) continue;
1100
+ const needed = Math.ceil(text2.y - box.y + text2.height + PADDING);
1101
+ if (needed > box.height) {
1102
+ resizes.push({ shapeId: box.id, shapeType: box.type, from: box.height, to: needed });
1103
+ box.height = needed;
1104
+ box.version = (box.version ?? 1) + 1;
1105
+ box.versionNonce = Math.floor(Math.random() * 2 ** 31);
1106
+ box.updated = Date.now();
1107
+ }
1108
+ }
1109
+ return resizes;
1110
+ }
1111
+
1112
+ // ../diagram/src/describe.ts
1113
+ var SHAPES2 = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond"]);
1089
1114
  var ARROW_LABEL_RADIUS = 60;
1090
1115
  function centre(b) {
1091
1116
  return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
@@ -1126,7 +1151,7 @@ function describeScene(elements) {
1126
1151
  if (bound) return bound;
1127
1152
  const owner = byId.get(id);
1128
1153
  if (!owner) return "";
1129
- if (SHAPES.has(owner.type)) {
1154
+ if (SHAPES2.has(owner.type)) {
1130
1155
  const hit = floating.find((t) => !adopted.has(t.id) && contains(owner, centre(t)));
1131
1156
  if (hit) {
1132
1157
  adopted.add(hit.id);
@@ -1157,7 +1182,7 @@ function describeScene(elements) {
1157
1182
  };
1158
1183
  const counts = {};
1159
1184
  for (const el of elements) counts[el.type] = (counts[el.type] ?? 0) + 1;
1160
- const shapes = elements.filter((el) => SHAPES.has(el.type)).map((el) => ({ id: el.id, type: el.type, label: labelFor(el.id) }));
1185
+ const shapes = elements.filter((el) => SHAPES2.has(el.type)).map((el) => ({ id: el.id, type: el.type, label: labelFor(el.id) }));
1161
1186
  const shapeLabel = new Map(shapes.map((s) => [s.id, s.label]));
1162
1187
  const edges = elements.filter((el) => el.type === "arrow").map((el) => {
1163
1188
  const start = el.startBinding;
@@ -1570,6 +1595,7 @@ server.tool(
1570
1595
  const scene = await api().readSheet(workspace_id, sheet_id, unlocked.key);
1571
1596
  const elements = scene.elements;
1572
1597
  const counts = /* @__PURE__ */ new Map();
1598
+ const changedTextIds = /* @__PURE__ */ new Set();
1573
1599
  for (const el of elements) {
1574
1600
  if (el.type !== "text") continue;
1575
1601
  const current = (el.originalText ?? el.text)?.trim();
@@ -1578,14 +1604,18 @@ server.tool(
1578
1604
  if (!edit) continue;
1579
1605
  el.text = edit.replace;
1580
1606
  el.originalText = edit.replace;
1581
- const metrics = measureText(edit.replace, el.fontSize ?? 20, 1e3);
1607
+ const fontSize = el.fontSize ?? 20;
1608
+ const wrapWidth = el.containerId ? el.width : 1e3;
1609
+ const metrics = measureText(edit.replace, fontSize, wrapWidth);
1582
1610
  el.width = metrics.width;
1583
1611
  el.height = metrics.height;
1612
+ changedTextIds.add(el.id);
1584
1613
  el.version = (el.version ?? 1) + 1;
1585
1614
  el.versionNonce = Math.floor(Math.random() * 2 ** 31);
1586
1615
  el.updated = Date.now();
1587
1616
  counts.set(edit.find, (counts.get(edit.find) ?? 0) + 1);
1588
1617
  }
1618
+ const resizes = growBoxesToFitText(elements, changedTextIds);
1589
1619
  const missed = edits.filter((e) => !counts.has(e.find));
1590
1620
  if (missed.length > 0) {
1591
1621
  return text(
@@ -1600,9 +1630,10 @@ server.tool(
1600
1630
  user.publicKey
1601
1631
  );
1602
1632
  const applied = [...counts.entries()].map(([find, n]) => ` ${JSON.stringify(find)} -> ${n} element${n === 1 ? "" : "s"}`).join("\n");
1633
+ const grewNote = resizes.length ? "\n\nBoxes grown so the new text fits (nothing else moved, so check for overlap):\n" + resizes.map((r) => ` a ${r.shapeType} grew ${Math.round(r.from)}px -> ${Math.round(r.to)}px`).join("\n") : "";
1603
1634
  return text(
1604
1635
  `Updated "${scene.name}". ${elements.length} elements preserved; only the text below changed.
1605
- ${applied}
1636
+ ${applied}${grewNote}
1606
1637
  ${sheetUrl(workspace_id, sheet_id)}`
1607
1638
  );
1608
1639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawpro/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": {