@excaliwow/mcp 0.5.0 → 0.7.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 -16
  2. package/dist/bin.js +55 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ client (Claude Desktop, Claude Code, etc.) can launch it with `npx`.
9
9
 
10
10
  First mint a Personal Access Token at https://excaliwow.com/app/settings
11
11
  (Settings → Developer / API tokens) with **`read` + `write`** capabilities —
12
- enough for six of the eight tools. Add **`delete`** only if you want the agent
12
+ enough for seven of the nine tools. Add **`delete`** only if you want the agent
13
13
  to trash and restore diagrams (see [Security notes](#security-notes)). Pass it as
14
14
  `EXCALIWOW_TOKEN`.
15
15
 
@@ -37,7 +37,7 @@ token to your account:
37
37
  "mcpServers": {
38
38
  "excaliwow": {
39
39
  "command": "npx",
40
- "args": ["-y", "@excaliwow/mcp@0.5.0"],
40
+ "args": ["-y", "@excaliwow/mcp@0.6.0"],
41
41
  "env": {
42
42
  "EXCALIWOW_TOKEN": "excw_pat_…"
43
43
  }
@@ -53,7 +53,7 @@ you've reviewed a new release.
53
53
 
54
54
  ## Troubleshooting
55
55
 
56
- **`npx -y @excaliwow/mcp@0.5.0` fails with `ENOENT … /@excaliwow/mcp@0.5.0/package.json`.**
56
+ **`npx -y @excaliwow/mcp@0.6.0` fails with `ENOENT … /@excaliwow/mcp@0.6.0/package.json`.**
57
57
  On some npm/Node versions, `npx` misreads a scoped package + `@version` spec as a
58
58
  local directory. It's an upstream npm bug (it reproduces with other scoped
59
59
  packages, e.g. `@modelcontextprotocol/server-filesystem@1.0.0`), not an Excaliwow one. Either
@@ -62,7 +62,7 @@ above does), or pin safely by installing once and pointing the client at the
62
62
  binary:
63
63
 
64
64
  ```sh
65
- npm i -g @excaliwow/mcp@0.5.0
65
+ npm i -g @excaliwow/mcp@0.6.0
66
66
  ```
67
67
 
68
68
  ```json
@@ -100,18 +100,19 @@ npx -y @excaliwow/mcp --health
100
100
 
101
101
  ## Tools
102
102
 
103
- Eight tools, scoped to safe agent use:
103
+ Nine tools, scoped to safe agent use:
104
104
 
105
- | Tool | Capability | What it does |
106
- | -------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------- |
107
- | `generate_diagram` | `write` | Create a diagram from the high-level node/edge DSL; returns the editor URL. |
108
- | `read_diagram` | `read` | Compact summary (title + per-type counts) **plus** a rendered PNG; opt into `includeGeometry` for a bounds list. |
109
- | `list_diagrams` | `read` | Page through your diagrams (`filter: active \| trash`). |
110
- | `move_diagram` | `write` | Move a diagram to a folder (or to root). |
111
- | `edit_diagram` | `write` | Additively merge a DSL fragment (add nodes/edges, update node style/label). |
112
- | `regenerate_diagram` | `write` | Replace a diagram's contents in place from a fresh spec (re-layout, same id). |
113
- | `trash_diagram` | `delete` | Soft-delete a diagram to trash. **Reversible** (see `restore_diagram`). |
114
- | `restore_diagram` | `delete` | Restore a trashed diagram, reopening it at its original id and URL. |
105
+ | Tool | Capability | What it does |
106
+ | -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------- |
107
+ | `generate_diagram` | `write` | Create a diagram from the high-level node/edge DSL; returns the editor URL. |
108
+ | `read_diagram` | `read` | Compact summary (title + per-type counts) **plus** a rendered PNG; opt into `includeGeometry` for a bounds list. |
109
+ | `export_diagram` | `read` | Render to full-fidelity bytes to **save** (png base64, or svg as raw text) — the bytes to keep, not a vision image. |
110
+ | `list_diagrams` | `read` | Page through your diagrams (`filter: active \| trash`). |
111
+ | `move_diagram` | `write` | Move a diagram to a folder (or to root). |
112
+ | `edit_diagram` | `write` | Additively merge a DSL fragment (add nodes/edges, update node style/label). |
113
+ | `regenerate_diagram` | `write` | Replace a diagram's contents in place from a fresh spec (re-layout, same id). |
114
+ | `trash_diagram` | `delete` | Soft-delete a diagram to trash. **Reversible** (see `restore_diagram`). |
115
+ | `restore_diagram` | `delete` | Restore a trashed diagram, reopening it at its original id and URL. |
115
116
 
116
117
  `read_diagram` returns a summary + image, **never** the raw scene JSON, to keep
117
118
  context small. Pass `includeGeometry: true` to additionally get a compact,
@@ -119,6 +120,15 @@ bounded `{ id, type, label, x, y, w, h }` list (top-left x/y) so the agent can
119
120
  **detect** label/box collisions or misplaced nodes programmatically instead of
120
121
  eyeballing the PNG — it is derived from the scene, so it is present even when the
121
122
  render fails, and it is a small fixed-field summary, not the raw element dump.
123
+ `export_diagram` returns the rendered **bytes** to save to a file — png as
124
+ base64, svg as raw text — distinct from `read_diagram`, which returns an image
125
+ block for a vision model to look at. An MCP server runs over stdio and cannot
126
+ write to your repo, so a client with filesystem access (e.g. Claude Code) decodes
127
+ and saves the bytes itself. Or skip the round-trip through the model and stream
128
+ straight to disk with the CLI: `excaliwow diagrams render <id> -o
129
+ docs/architecture.png` (or `.svg`) — also the fallback when a render is too large
130
+ to return inline.
131
+
122
132
  `trash_diagram` / `restore_diagram` are a **reversible** pair
123
133
  gated on the `delete` capability — registered always, they return a clean
124
134
  `insufficient_scope` error (changing nothing) unless the token carries `delete`,
@@ -148,7 +158,7 @@ project`) puts the token into git history. Use a user- or local-scoped config
148
158
  (`--scope local`), or reference an environment variable instead of pasting the
149
159
  literal token.
150
160
  - **Mint with `read` + `write` (add `delete` only if you want trash/restore).**
151
- Six of the eight tools need just `read` + `write`; a `read` + `write` PAT can
161
+ Seven of the nine tools need just `read` + `write`; a `read` + `write` PAT can
152
162
  neither expose your diagrams publicly nor delete them, even if the agent is
153
163
  misled (`trash_diagram` / `restore_diagram` simply return `insufficient_scope`
154
164
  and do nothing). Add the `delete` capability only if you want the agent to be
package/dist/bin.js CHANGED
@@ -266,7 +266,8 @@ var COMPACT_GRAMMAR = `DiagramSpec (auto-laid-out node/edge DSL):
266
266
  "width": 140, "height": 60, // optional; treated as a minimum
267
267
  "strokeColor": "#1e1e1e", // optional
268
268
  "backgroundColor": "transparent", // optional
269
- "fillStyle": "solid" } // hachure | cross-hatch | solid | zigzag
269
+ "fillStyle": "solid", // hachure | cross-hatch | solid | zigzag
270
+ "group": "hardware" } // optional layout hint (not a guarantee); clusters same-group nodes; members with edges among them still span ranks
270
271
  ],
271
272
  "edges": [ // optional
272
273
  { "from": "id", "to": "id", // both must name existing node ids
@@ -279,6 +280,26 @@ var COMPACT_GRAMMAR = `DiagramSpec (auto-laid-out node/edge DSL):
279
280
  }
280
281
  Caps: <=1000 nodes, <=2000 edges, label <=2000 chars, <=5000 total scene elements.`;
281
282
 
283
+ // src/export.ts
284
+ var MAX_INLINE_EXPORT_BYTES = 1e6;
285
+ function buildExportPayload(id, format, img) {
286
+ const { bytes } = img;
287
+ if (bytes.length > MAX_INLINE_EXPORT_BYTES) {
288
+ return { ok: false, bytes: bytes.length };
289
+ }
290
+ const buf = Buffer.from(bytes);
291
+ const payload = {
292
+ id,
293
+ format,
294
+ encoding: format === "svg" ? "utf8" : "base64",
295
+ bytes: bytes.length,
296
+ ...img.quality ? { quality: img.quality } : {},
297
+ ...img.note ? { note: img.note } : {},
298
+ data: format === "svg" ? buf.toString("utf8") : buf.toString("base64")
299
+ };
300
+ return { ok: true, payload };
301
+ }
302
+
282
303
  // src/geometry.ts
283
304
  function num(v) {
284
305
  return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
@@ -357,7 +378,8 @@ and the **EditFragment** for \`edit_diagram\`.
357
378
  "height": 60,
358
379
  "strokeColor": "#1e1e1e",
359
380
  "backgroundColor": "transparent",
360
- "fillStyle": "solid"
381
+ "fillStyle": "solid",
382
+ "group": "hardware"
361
383
  }
362
384
  \`\`\`
363
385
 
@@ -370,6 +392,7 @@ and the **EditFragment** for \`edit_diagram\`.
370
392
  | \`strokeColor\` | string? | Any CSS color. |
371
393
  | \`backgroundColor\` | string? | Any CSS color, or \`transparent\`. |
372
394
  | \`fillStyle\` | enum? | \`hachure\` \\| \`cross-hatch\` \\| \`solid\` \\| \`zigzag\`. |
395
+ | \`group\` | string? | Layout **hint** (not a guarantee). Nodes sharing a \`group\` are clustered together by auto-layout \u2014 kept in the same region, and a member that an edge would otherwise float onto a separate rank is pulled back toward the group. Sibling nodes with no edges *among themselves* (e.g. a set of sensors hanging off one controller) band into a single row; members that have edges between them still span ranks. Affects auto-layout (generate / regenerate) only \u2014 not \`edit_diagram\`; a one-member group is a no-op. |
373
396
 
374
397
  Default sizes: rectangle 140x60, ellipse 140x80, diamond 160x90. A \`text\` node IS its text.
375
398
 
@@ -462,7 +485,8 @@ var NodeSpecZ = z.object({
462
485
  height: z.number().optional(),
463
486
  strokeColor: z.string().optional(),
464
487
  backgroundColor: z.string().optional(),
465
- fillStyle: FillStyleZ.optional()
488
+ fillStyle: FillStyleZ.optional(),
489
+ group: z.string().optional()
466
490
  });
467
491
  var EdgeObjectZ = z.object({
468
492
  from: z.string(),
@@ -500,7 +524,7 @@ var EditFragmentZ = z.object({
500
524
  });
501
525
 
502
526
  // src/version.ts
503
- var VERSION = "0.5.0";
527
+ var VERSION = "0.7.0";
504
528
 
505
529
  // src/server.ts
506
530
  function textResult(text) {
@@ -779,6 +803,33 @@ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fo
779
803
  }
780
804
  }
781
805
  );
806
+ server2.registerTool(
807
+ "export_diagram",
808
+ {
809
+ title: "Export a diagram to render bytes (for saving)",
810
+ description: "Render a diagram to full-fidelity bytes intended to be SAVED to a file. Returns a JSON text block { id, format, encoding, bytes, data } \u2014 for png, data is base64; for svg, data is the raw SVG text. In a client with filesystem access (e.g. Claude Code) decode `data` and write it yourself (e.g. docs/architecture.png). Defaults to png; pass format='svg' for the resolution-independent faithful vector. This differs from read_diagram, which returns an image block for a VISION model to look at \u2014 export_diagram gives you the bytes to keep. Requires only the `read` capability. An empty diagram returns a clean error (nothing to export); a render too large to return inline returns a clean error pointing at the CLI (`excaliwow diagrams render <id> -o <file>`), which streams straight to disk.",
811
+ inputSchema: { id: z2.string(), format: z2.enum(["png", "svg"]).optional() }
812
+ },
813
+ async ({ id, format }) => {
814
+ try {
815
+ const ctx = buildCtx(deps);
816
+ const fmt = format ?? "png";
817
+ const img = await renderDiagram(ctx, id, { format: fmt });
818
+ if (img == null) {
819
+ return errorResult(`diagram_empty: ${id} renders empty \u2014 there is nothing to export.`);
820
+ }
821
+ const built = buildExportPayload(id, fmt, img);
822
+ if (!built.ok) {
823
+ return errorResult(
824
+ `export_too_large: the ${fmt} render is ${built.bytes} bytes, over the ${MAX_INLINE_EXPORT_BYTES}-byte inline limit. Save it directly with the CLI instead: excaliwow diagrams render ${id} -o <file>.${fmt}`
825
+ );
826
+ }
827
+ return textResult(JSON.stringify(built.payload));
828
+ } catch (err) {
829
+ return toErrorResult(err);
830
+ }
831
+ }
832
+ );
782
833
  server2.registerResource(
783
834
  "dsl-reference",
784
835
  DSL_REFERENCE_URI,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@excaliwow/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Excaliwow Model Context Protocol (MCP) server — lets AI agents create, read, render, and edit Excaliwow diagrams over the public REST API, via stdio.",
5
5
  "private": false,
6
6
  "publishConfig": {