@excaliwow/mcp 0.4.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 +19 -14
- package/dist/bin.js +67 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
@@ -102,19 +102,24 @@ npx -y @excaliwow/mcp --health
|
|
|
102
102
|
|
|
103
103
|
Eight 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
|
-
| `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
|
+
| `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. |
|
|
115
115
|
|
|
116
116
|
`read_diagram` returns a summary + image, **never** the raw scene JSON, to keep
|
|
117
|
-
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
|
|
118
123
|
gated on the `delete` capability — registered always, they return a clean
|
|
119
124
|
`insufficient_scope` error (changing nothing) unless the token carries `delete`,
|
|
120
125
|
so a `read` + `write` token can't trash anything. Hard-delete/purge and making a
|
package/dist/bin.js
CHANGED
|
@@ -279,6 +279,49 @@ 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/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
|
+
|
|
282
325
|
// src/reference.ts
|
|
283
326
|
var DSL_REFERENCE_URI = "excaliwow://dsl/reference";
|
|
284
327
|
var DSL_REFERENCE_MARKDOWN = `# Excaliwow DSL reference
|
|
@@ -457,7 +500,7 @@ var EditFragmentZ = z.object({
|
|
|
457
500
|
});
|
|
458
501
|
|
|
459
502
|
// src/version.ts
|
|
460
|
-
var VERSION = "0.
|
|
503
|
+
var VERSION = "0.5.0";
|
|
461
504
|
|
|
462
505
|
// src/server.ts
|
|
463
506
|
function textResult(text) {
|
|
@@ -542,10 +585,10 @@ function createServer(deps) {
|
|
|
542
585
|
"read_diagram",
|
|
543
586
|
{
|
|
544
587
|
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() }
|
|
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() }
|
|
547
590
|
},
|
|
548
|
-
async ({ id }) => {
|
|
591
|
+
async ({ id, includeGeometry }) => {
|
|
549
592
|
let detail;
|
|
550
593
|
try {
|
|
551
594
|
const ctx = buildCtx(deps);
|
|
@@ -553,28 +596,33 @@ function createServer(deps) {
|
|
|
553
596
|
const { total, byType } = elementBreakdown(detail);
|
|
554
597
|
const breakdown = Object.entries(byType).sort(([a], [b]) => a.localeCompare(b)).map(([t, n]) => `${t}: ${n}`).join(", ");
|
|
555
598
|
const summary = `${detail.title || "(untitled)"} \u2014 ${total} element${total === 1 ? "" : "s"}` + (breakdown ? ` (${breakdown})` : "");
|
|
599
|
+
let summaryNote = "";
|
|
600
|
+
let imageBlock;
|
|
556
601
|
try {
|
|
557
602
|
const png = await renderDiagram(ctx, id, { format: "png" });
|
|
558
603
|
if (png && png.bytes.length > 0) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
mimeType: "image/png"
|
|
568
|
-
}
|
|
569
|
-
]
|
|
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"
|
|
570
612
|
};
|
|
613
|
+
} else {
|
|
614
|
+
summaryNote = "\n(no preview: the diagram renders empty)";
|
|
571
615
|
}
|
|
572
|
-
return textResult(`${summary}
|
|
573
|
-
(no preview: the diagram renders empty)`);
|
|
574
616
|
} catch {
|
|
575
|
-
|
|
576
|
-
|
|
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)) });
|
|
577
623
|
}
|
|
624
|
+
if (imageBlock) content.push(imageBlock);
|
|
625
|
+
return { content };
|
|
578
626
|
} catch (err) {
|
|
579
627
|
return toErrorResult(err);
|
|
580
628
|
}
|
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": {
|