@excaliwow/mcp 0.3.0 → 0.5.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 +22 -16
- package/dist/bin.js +121 -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 six of the eight 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.5.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.5.0` fails with `ENOENT … /@excaliwow/mcp@0.5.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.5.0
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
```json
|
|
@@ -100,20 +100,26 @@ npx -y @excaliwow/mcp --health
|
|
|
100
100
|
|
|
101
101
|
## Tools
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Eight tools, scoped to safe agent use:
|
|
104
104
|
|
|
105
|
-
| Tool
|
|
106
|
-
|
|
|
107
|
-
| `generate_diagram`
|
|
108
|
-
| `read_diagram`
|
|
109
|
-
| `list_diagrams`
|
|
110
|
-
| `move_diagram`
|
|
111
|
-
| `edit_diagram`
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
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. |
|
|
114
115
|
|
|
115
116
|
`read_diagram` returns a summary + image, **never** the raw scene JSON, to keep
|
|
116
|
-
context small. `
|
|
117
|
+
context small. Pass `includeGeometry: true` to additionally get a compact,
|
|
118
|
+
bounded `{ id, type, label, x, y, w, h }` list (top-left x/y) so the agent can
|
|
119
|
+
**detect** label/box collisions or misplaced nodes programmatically instead of
|
|
120
|
+
eyeballing the PNG — it is derived from the scene, so it is present even when the
|
|
121
|
+
render fails, and it is a small fixed-field summary, not the raw element dump.
|
|
122
|
+
`trash_diagram` / `restore_diagram` are a **reversible** pair
|
|
117
123
|
gated on the `delete` capability — registered always, they return a clean
|
|
118
124
|
`insufficient_scope` error (changing nothing) unless the token carries `delete`,
|
|
119
125
|
so a `read` + `write` token can't trash anything. Hard-delete/purge and making a
|
|
@@ -142,7 +148,7 @@ project`) puts the token into git history. Use a user- or local-scoped config
|
|
|
142
148
|
(`--scope local`), or reference an environment variable instead of pasting the
|
|
143
149
|
literal token.
|
|
144
150
|
- **Mint with `read` + `write` (add `delete` only if you want trash/restore).**
|
|
145
|
-
|
|
151
|
+
Six of the eight tools need just `read` + `write`; a `read` + `write` PAT can
|
|
146
152
|
neither expose your diagrams publicly nor delete them, even if the agent is
|
|
147
153
|
misled (`trash_diagram` / `restore_diagram` simply return `insufficient_scope`
|
|
148
154
|
and do nothing). Add the `delete` capability only if you want the agent to be
|
package/dist/bin.js
CHANGED
|
@@ -223,6 +223,18 @@ function restoreDiagram(ctx, id) {
|
|
|
223
223
|
fetchImpl: ctx.fetchImpl
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
|
+
function regenerateDiagram(ctx, id, args) {
|
|
227
|
+
const body = { spec: args.spec };
|
|
228
|
+
if (args.title !== void 0) body.title = args.title;
|
|
229
|
+
return request({
|
|
230
|
+
method: "POST",
|
|
231
|
+
path: `${PREFIX}/diagrams/${encodeURIComponent(id)}/regenerate`,
|
|
232
|
+
token: ctx.token,
|
|
233
|
+
baseUrl: ctx.baseUrl,
|
|
234
|
+
body,
|
|
235
|
+
fetchImpl: ctx.fetchImpl
|
|
236
|
+
});
|
|
237
|
+
}
|
|
226
238
|
|
|
227
239
|
// src/server.ts
|
|
228
240
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -267,6 +279,49 @@ var COMPACT_GRAMMAR = `DiagramSpec (auto-laid-out node/edge DSL):
|
|
|
267
279
|
}
|
|
268
280
|
Caps: <=1000 nodes, <=2000 edges, label <=2000 chars, <=5000 total scene elements.`;
|
|
269
281
|
|
|
282
|
+
// src/geometry.ts
|
|
283
|
+
function num(v) {
|
|
284
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : 0;
|
|
285
|
+
}
|
|
286
|
+
function isObject(v) {
|
|
287
|
+
return typeof v === "object" && v !== null;
|
|
288
|
+
}
|
|
289
|
+
function extractGeometry(elements) {
|
|
290
|
+
const els = Array.isArray(elements) ? elements : [];
|
|
291
|
+
const labelByContainer = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const el of els) {
|
|
293
|
+
if (!isObject(el) || el.isDeleted === true) continue;
|
|
294
|
+
if (el.type === "text" && typeof el.containerId === "string" && el.containerId) {
|
|
295
|
+
labelByContainer.set(el.containerId, typeof el.text === "string" ? el.text : "");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const items = [];
|
|
299
|
+
for (const el of els) {
|
|
300
|
+
if (!isObject(el) || el.isDeleted === true) continue;
|
|
301
|
+
const id = typeof el.id === "string" ? el.id : String(el.id ?? "");
|
|
302
|
+
const type = typeof el.type === "string" ? el.type : "unknown";
|
|
303
|
+
const label = el.type === "text" ? typeof el.text === "string" ? el.text : "" : labelByContainer.get(id) ?? "";
|
|
304
|
+
items.push({
|
|
305
|
+
id,
|
|
306
|
+
type,
|
|
307
|
+
label,
|
|
308
|
+
x: num(el.x),
|
|
309
|
+
y: num(el.y),
|
|
310
|
+
w: num(el.width),
|
|
311
|
+
h: num(el.height)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return items;
|
|
315
|
+
}
|
|
316
|
+
function formatGeometry(items) {
|
|
317
|
+
if (items.length === 0) return "geometry: (none \u2014 empty diagram)";
|
|
318
|
+
const body = items.map((it) => JSON.stringify(it)).join(",\n");
|
|
319
|
+
return `geometry (${items.length} element${items.length === 1 ? "" : "s"}):
|
|
320
|
+
[
|
|
321
|
+
${body}
|
|
322
|
+
]`;
|
|
323
|
+
}
|
|
324
|
+
|
|
270
325
|
// src/reference.ts
|
|
271
326
|
var DSL_REFERENCE_URI = "excaliwow://dsl/reference";
|
|
272
327
|
var DSL_REFERENCE_MARKDOWN = `# Excaliwow DSL reference
|
|
@@ -445,7 +500,7 @@ var EditFragmentZ = z.object({
|
|
|
445
500
|
});
|
|
446
501
|
|
|
447
502
|
// src/version.ts
|
|
448
|
-
var VERSION = "0.
|
|
503
|
+
var VERSION = "0.5.0";
|
|
449
504
|
|
|
450
505
|
// src/server.ts
|
|
451
506
|
function textResult(text) {
|
|
@@ -530,10 +585,10 @@ function createServer(deps) {
|
|
|
530
585
|
"read_diagram",
|
|
531
586
|
{
|
|
532
587
|
title: "Read a diagram",
|
|
533
|
-
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).",
|
|
534
|
-
inputSchema: { id: z2.string() }
|
|
588
|
+
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.",
|
|
589
|
+
inputSchema: { id: z2.string(), includeGeometry: z2.boolean().optional() }
|
|
535
590
|
},
|
|
536
|
-
async ({ id }) => {
|
|
591
|
+
async ({ id, includeGeometry }) => {
|
|
537
592
|
let detail;
|
|
538
593
|
try {
|
|
539
594
|
const ctx = buildCtx(deps);
|
|
@@ -541,28 +596,33 @@ function createServer(deps) {
|
|
|
541
596
|
const { total, byType } = elementBreakdown(detail);
|
|
542
597
|
const breakdown = Object.entries(byType).sort(([a], [b]) => a.localeCompare(b)).map(([t, n]) => `${t}: ${n}`).join(", ");
|
|
543
598
|
const summary = `${detail.title || "(untitled)"} \u2014 ${total} element${total === 1 ? "" : "s"}` + (breakdown ? ` (${breakdown})` : "");
|
|
599
|
+
let summaryNote = "";
|
|
600
|
+
let imageBlock;
|
|
544
601
|
try {
|
|
545
602
|
const png = await renderDiagram(ctx, id, { format: "png" });
|
|
546
603
|
if (png && png.bytes.length > 0) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
mimeType: "image/png"
|
|
556
|
-
}
|
|
557
|
-
]
|
|
604
|
+
if (png.quality && png.quality !== "faithful") {
|
|
605
|
+
summaryNote = `
|
|
606
|
+
preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fonts approximate`;
|
|
607
|
+
}
|
|
608
|
+
imageBlock = {
|
|
609
|
+
type: "image",
|
|
610
|
+
data: Buffer.from(png.bytes).toString("base64"),
|
|
611
|
+
mimeType: "image/png"
|
|
558
612
|
};
|
|
613
|
+
} else {
|
|
614
|
+
summaryNote = "\n(no preview: the diagram renders empty)";
|
|
559
615
|
}
|
|
560
|
-
return textResult(`${summary}
|
|
561
|
-
(no preview: the diagram renders empty)`);
|
|
562
616
|
} catch {
|
|
563
|
-
|
|
564
|
-
|
|
617
|
+
summaryNote = "\n(no preview: render failed)";
|
|
618
|
+
}
|
|
619
|
+
const content = [{ type: "text", text: summary + summaryNote }];
|
|
620
|
+
if (includeGeometry) {
|
|
621
|
+
const elements = Array.isArray(detail.scene?.elements) ? detail.scene.elements : [];
|
|
622
|
+
content.push({ type: "text", text: formatGeometry(extractGeometry(elements)) });
|
|
565
623
|
}
|
|
624
|
+
if (imageBlock) content.push(imageBlock);
|
|
625
|
+
return { content };
|
|
566
626
|
} catch (err) {
|
|
567
627
|
return toErrorResult(err);
|
|
568
628
|
}
|
|
@@ -677,6 +737,48 @@ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fo
|
|
|
677
737
|
}
|
|
678
738
|
}
|
|
679
739
|
);
|
|
740
|
+
server2.registerTool(
|
|
741
|
+
"regenerate_diagram",
|
|
742
|
+
{
|
|
743
|
+
title: "Regenerate a diagram (replace in place)",
|
|
744
|
+
description: [
|
|
745
|
+
"REPLACE an existing diagram's entire contents from a fresh DiagramSpec: the spec is",
|
|
746
|
+
"re-laid-out server-side and swapped in, keeping the SAME id and url. Use this to ITERATE",
|
|
747
|
+
"on one diagram (fix layout, restructure) instead of generate_diagram, which mints a NEW",
|
|
748
|
+
"id+url each time and orphans the old one. Unlike edit_diagram (which only appends/patches),",
|
|
749
|
+
"this discards the old scene entirely. Comments anchored to a shape the new layout no longer",
|
|
750
|
+
"contains are detached (returned as orphanedComments); comments on shapes whose node id you",
|
|
751
|
+
"reuse stay attached. Pass an optional title to rename in the same call. Returns the id, the",
|
|
752
|
+
"new element count (replaced), orphanedComments, and the editor url.",
|
|
753
|
+
"",
|
|
754
|
+
COMPACT_GRAMMAR,
|
|
755
|
+
"",
|
|
756
|
+
"See the `" + DSL_REFERENCE_URI + "` resource for the full DSL reference."
|
|
757
|
+
].join("\n"),
|
|
758
|
+
inputSchema: {
|
|
759
|
+
id: z2.string(),
|
|
760
|
+
title: z2.string().nullish(),
|
|
761
|
+
spec: DiagramSpecZ
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
async ({ id, title, spec }) => {
|
|
765
|
+
try {
|
|
766
|
+
const ctx = buildCtx(deps);
|
|
767
|
+
const result = await regenerateDiagram(ctx, id, { spec, title: title ?? void 0 });
|
|
768
|
+
return textResult(
|
|
769
|
+
JSON.stringify({
|
|
770
|
+
id: result.id,
|
|
771
|
+
replaced: result.replaced,
|
|
772
|
+
orphanedComments: result.orphanedComments,
|
|
773
|
+
...result.title !== void 0 ? { title: result.title } : {},
|
|
774
|
+
url: editorUrl(ctx, result.id)
|
|
775
|
+
})
|
|
776
|
+
);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
return toErrorResult(err);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
);
|
|
680
782
|
server2.registerResource(
|
|
681
783
|
"dsl-reference",
|
|
682
784
|
DSL_REFERENCE_URI,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@excaliwow/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|