@excaliwow/mcp 0.4.0 → 0.6.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 +32 -17
- package/dist/bin.js +114 -19
- 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
|
|
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.
|
|
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.
|
|
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.
|
|
65
|
+
npm i -g @excaliwow/mcp@0.6.0
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
```json
|
|
@@ -100,21 +100,36 @@ npx -y @excaliwow/mcp --health
|
|
|
100
100
|
|
|
101
101
|
## Tools
|
|
102
102
|
|
|
103
|
-
|
|
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
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
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
|
-
context small. `
|
|
118
|
+
context small. Pass `includeGeometry: true` to additionally get a compact,
|
|
119
|
+
bounded `{ id, type, label, x, y, w, h }` list (top-left x/y) so the agent can
|
|
120
|
+
**detect** label/box collisions or misplaced nodes programmatically instead of
|
|
121
|
+
eyeballing the PNG — it is derived from the scene, so it is present even when the
|
|
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
|
+
|
|
132
|
+
`trash_diagram` / `restore_diagram` are a **reversible** pair
|
|
118
133
|
gated on the `delete` capability — registered always, they return a clean
|
|
119
134
|
`insufficient_scope` error (changing nothing) unless the token carries `delete`,
|
|
120
135
|
so a `read` + `write` token can't trash anything. Hard-delete/purge and making a
|
|
@@ -143,7 +158,7 @@ project`) puts the token into git history. Use a user- or local-scoped config
|
|
|
143
158
|
(`--scope local`), or reference an environment variable instead of pasting the
|
|
144
159
|
literal token.
|
|
145
160
|
- **Mint with `read` + `write` (add `delete` only if you want trash/restore).**
|
|
146
|
-
|
|
161
|
+
Seven of the nine tools need just `read` + `write`; a `read` + `write` PAT can
|
|
147
162
|
neither expose your diagrams publicly nor delete them, even if the agent is
|
|
148
163
|
misled (`trash_diagram` / `restore_diagram` simply return `insufficient_scope`
|
|
149
164
|
and do nothing). Add the `delete` capability only if you want the agent to be
|
package/dist/bin.js
CHANGED
|
@@ -279,6 +279,69 @@ var COMPACT_GRAMMAR = `DiagramSpec (auto-laid-out node/edge DSL):
|
|
|
279
279
|
}
|
|
280
280
|
Caps: <=1000 nodes, <=2000 edges, label <=2000 chars, <=5000 total scene elements.`;
|
|
281
281
|
|
|
282
|
+
// src/export.ts
|
|
283
|
+
var MAX_INLINE_EXPORT_BYTES = 1e6;
|
|
284
|
+
function buildExportPayload(id, format, img) {
|
|
285
|
+
const { bytes } = img;
|
|
286
|
+
if (bytes.length > MAX_INLINE_EXPORT_BYTES) {
|
|
287
|
+
return { ok: false, bytes: bytes.length };
|
|
288
|
+
}
|
|
289
|
+
const buf = Buffer.from(bytes);
|
|
290
|
+
const payload = {
|
|
291
|
+
id,
|
|
292
|
+
format,
|
|
293
|
+
encoding: format === "svg" ? "utf8" : "base64",
|
|
294
|
+
bytes: bytes.length,
|
|
295
|
+
...img.quality ? { quality: img.quality } : {},
|
|
296
|
+
...img.note ? { note: img.note } : {},
|
|
297
|
+
data: format === "svg" ? buf.toString("utf8") : buf.toString("base64")
|
|
298
|
+
};
|
|
299
|
+
return { ok: true, payload };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/geometry.ts
|
|
303
|
+
function num(v) {
|
|
304
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
|
|
305
|
+
}
|
|
306
|
+
function isObject(v) {
|
|
307
|
+
return typeof v === "object" && v !== null;
|
|
308
|
+
}
|
|
309
|
+
function extractGeometry(elements) {
|
|
310
|
+
const els = Array.isArray(elements) ? elements : [];
|
|
311
|
+
const labelByContainer = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const el of els) {
|
|
313
|
+
if (!isObject(el) || el.isDeleted === true) continue;
|
|
314
|
+
if (el.type === "text" && typeof el.containerId === "string" && el.containerId) {
|
|
315
|
+
labelByContainer.set(el.containerId, typeof el.text === "string" ? el.text : "");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const items = [];
|
|
319
|
+
for (const el of els) {
|
|
320
|
+
if (!isObject(el) || el.isDeleted === true) continue;
|
|
321
|
+
const id = typeof el.id === "string" ? el.id : String(el.id ?? "");
|
|
322
|
+
const type = typeof el.type === "string" ? el.type : "unknown";
|
|
323
|
+
const label = el.type === "text" ? typeof el.text === "string" ? el.text : "" : labelByContainer.get(id) ?? "";
|
|
324
|
+
items.push({
|
|
325
|
+
id,
|
|
326
|
+
type,
|
|
327
|
+
label,
|
|
328
|
+
x: num(el.x),
|
|
329
|
+
y: num(el.y),
|
|
330
|
+
w: num(el.width),
|
|
331
|
+
h: num(el.height)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return items;
|
|
335
|
+
}
|
|
336
|
+
function formatGeometry(items) {
|
|
337
|
+
if (items.length === 0) return "geometry: (none \u2014 empty diagram)";
|
|
338
|
+
const body = items.map((it) => JSON.stringify(it)).join(",\n");
|
|
339
|
+
return `geometry (${items.length} element${items.length === 1 ? "" : "s"}):
|
|
340
|
+
[
|
|
341
|
+
${body}
|
|
342
|
+
]`;
|
|
343
|
+
}
|
|
344
|
+
|
|
282
345
|
// src/reference.ts
|
|
283
346
|
var DSL_REFERENCE_URI = "excaliwow://dsl/reference";
|
|
284
347
|
var DSL_REFERENCE_MARKDOWN = `# Excaliwow DSL reference
|
|
@@ -457,7 +520,7 @@ var EditFragmentZ = z.object({
|
|
|
457
520
|
});
|
|
458
521
|
|
|
459
522
|
// src/version.ts
|
|
460
|
-
var VERSION = "0.
|
|
523
|
+
var VERSION = "0.6.0";
|
|
461
524
|
|
|
462
525
|
// src/server.ts
|
|
463
526
|
function textResult(text) {
|
|
@@ -542,10 +605,10 @@ function createServer(deps) {
|
|
|
542
605
|
"read_diagram",
|
|
543
606
|
{
|
|
544
607
|
title: "Read a diagram",
|
|
545
|
-
description: "Read a diagram as a compact text summary (title + element counts) plus a PNG image block so a vision model can see it. Never returns the raw scene JSON. A failed or empty render degrades to text-only with a note (the read never fails on a render error).",
|
|
546
|
-
inputSchema: { id: z2.string() }
|
|
608
|
+
description: "Read a diagram as a compact text summary (title + element counts) plus a PNG image block so a vision model can see it. Never returns the raw scene JSON. A failed or empty render degrades to text-only with a note (the read never fails on a render error). Pass includeGeometry=true to also get a compact, bounded list of element bounds ({ id, type, label, x, y, w, h }, top-left x/y) so you can DETECT label/box collisions or misplaced nodes programmatically \u2014 it comes from the scene, so it is present even when the render fails.",
|
|
609
|
+
inputSchema: { id: z2.string(), includeGeometry: z2.boolean().optional() }
|
|
547
610
|
},
|
|
548
|
-
async ({ id }) => {
|
|
611
|
+
async ({ id, includeGeometry }) => {
|
|
549
612
|
let detail;
|
|
550
613
|
try {
|
|
551
614
|
const ctx = buildCtx(deps);
|
|
@@ -553,28 +616,33 @@ function createServer(deps) {
|
|
|
553
616
|
const { total, byType } = elementBreakdown(detail);
|
|
554
617
|
const breakdown = Object.entries(byType).sort(([a], [b]) => a.localeCompare(b)).map(([t, n]) => `${t}: ${n}`).join(", ");
|
|
555
618
|
const summary = `${detail.title || "(untitled)"} \u2014 ${total} element${total === 1 ? "" : "s"}` + (breakdown ? ` (${breakdown})` : "");
|
|
619
|
+
let summaryNote = "";
|
|
620
|
+
let imageBlock;
|
|
556
621
|
try {
|
|
557
622
|
const png = await renderDiagram(ctx, id, { format: "png" });
|
|
558
623
|
if (png && png.bytes.length > 0) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
mimeType: "image/png"
|
|
568
|
-
}
|
|
569
|
-
]
|
|
624
|
+
if (png.quality && png.quality !== "faithful") {
|
|
625
|
+
summaryNote = `
|
|
626
|
+
preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fonts approximate`;
|
|
627
|
+
}
|
|
628
|
+
imageBlock = {
|
|
629
|
+
type: "image",
|
|
630
|
+
data: Buffer.from(png.bytes).toString("base64"),
|
|
631
|
+
mimeType: "image/png"
|
|
570
632
|
};
|
|
633
|
+
} else {
|
|
634
|
+
summaryNote = "\n(no preview: the diagram renders empty)";
|
|
571
635
|
}
|
|
572
|
-
return textResult(`${summary}
|
|
573
|
-
(no preview: the diagram renders empty)`);
|
|
574
636
|
} catch {
|
|
575
|
-
|
|
576
|
-
(no preview: render failed)`);
|
|
637
|
+
summaryNote = "\n(no preview: render failed)";
|
|
577
638
|
}
|
|
639
|
+
const content = [{ type: "text", text: summary + summaryNote }];
|
|
640
|
+
if (includeGeometry) {
|
|
641
|
+
const elements = Array.isArray(detail.scene?.elements) ? detail.scene.elements : [];
|
|
642
|
+
content.push({ type: "text", text: formatGeometry(extractGeometry(elements)) });
|
|
643
|
+
}
|
|
644
|
+
if (imageBlock) content.push(imageBlock);
|
|
645
|
+
return { content };
|
|
578
646
|
} catch (err) {
|
|
579
647
|
return toErrorResult(err);
|
|
580
648
|
}
|
|
@@ -731,6 +799,33 @@ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fo
|
|
|
731
799
|
}
|
|
732
800
|
}
|
|
733
801
|
);
|
|
802
|
+
server2.registerTool(
|
|
803
|
+
"export_diagram",
|
|
804
|
+
{
|
|
805
|
+
title: "Export a diagram to render bytes (for saving)",
|
|
806
|
+
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.",
|
|
807
|
+
inputSchema: { id: z2.string(), format: z2.enum(["png", "svg"]).optional() }
|
|
808
|
+
},
|
|
809
|
+
async ({ id, format }) => {
|
|
810
|
+
try {
|
|
811
|
+
const ctx = buildCtx(deps);
|
|
812
|
+
const fmt = format ?? "png";
|
|
813
|
+
const img = await renderDiagram(ctx, id, { format: fmt });
|
|
814
|
+
if (img == null) {
|
|
815
|
+
return errorResult(`diagram_empty: ${id} renders empty \u2014 there is nothing to export.`);
|
|
816
|
+
}
|
|
817
|
+
const built = buildExportPayload(id, fmt, img);
|
|
818
|
+
if (!built.ok) {
|
|
819
|
+
return errorResult(
|
|
820
|
+
`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}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
return textResult(JSON.stringify(built.payload));
|
|
824
|
+
} catch (err) {
|
|
825
|
+
return toErrorResult(err);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
);
|
|
734
829
|
server2.registerResource(
|
|
735
830
|
"dsl-reference",
|
|
736
831
|
DSL_REFERENCE_URI,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@excaliwow/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|