@excaliwow/mcp 0.2.0 → 0.3.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 +104 -25
  2. package/dist/bin.js +138 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,26 @@ client (Claude Desktop, Claude Code, etc.) can launch it with `npx`.
7
7
 
8
8
  ## Install
9
9
 
10
- Add it to your MCP client's config file (e.g. Claude Desktop's
10
+ First mint a Personal Access Token at https://excaliwow.com/app/settings
11
+ (Settings → Developer / API tokens) with **`read` + `write`** capabilities —
12
+ enough for five of the seven tools. Add **`delete`** only if you want the agent
13
+ to trash and restore diagrams (see [Security notes](#security-notes)). Pass it as
14
+ `EXCALIWOW_TOKEN`.
15
+
16
+ ### Claude Code (CLI)
17
+
18
+ One command. `--scope local` stores the server in your own settings, so the
19
+ token never lands in a file you might commit:
20
+
21
+ ```sh
22
+ claude mcp add excaliwow --scope local \
23
+ --env EXCALIWOW_TOKEN=excw_pat_… \
24
+ -- npx -y @excaliwow/mcp
25
+ ```
26
+
27
+ ### Claude Desktop (and other JSON-config clients)
28
+
29
+ Add it to your client's config file (e.g. Claude Desktop's
11
30
  `claude_desktop_config.json`, or your client's MCP settings — see your client's
12
31
  MCP setup docs). **Pin the version** — `npx -y` otherwise always pulls the newest
13
32
  release, which is an avoidable supply-chain surface for a process that holds a
@@ -18,7 +37,7 @@ token to your account:
18
37
  "mcpServers": {
19
38
  "excaliwow": {
20
39
  "command": "npx",
21
- "args": ["-y", "@excaliwow/mcp@0.1.1"],
40
+ "args": ["-y", "@excaliwow/mcp@0.3.0"],
22
41
  "env": {
23
42
  "EXCALIWOW_TOKEN": "excw_pat_…"
24
43
  }
@@ -27,30 +46,80 @@ token to your account:
27
46
  }
28
47
  ```
29
48
 
30
- `EXCALIWOW_TOKEN` is a Personal Access Token created at
31
- https://excaliwow.com/app/settings (Settings Developer / API tokens). The
32
- server reads it per call from the environment (or, if you also use
33
- `@excaliwow/cli` and have run `excaliwow auth login`, that stored login) and never
34
- writes the token to disk itself. For a standalone MCP install, set
35
- `EXCALIWOW_TOKEN` as shown. Bump the pinned version deliberately when you've
36
- reviewed a new release.
49
+ The server reads `EXCALIWOW_TOKEN` per call from the environment (or, if you also
50
+ use `@excaliwow/cli` and have run `excaliwow auth login`, that stored login) and
51
+ never writes the token to disk itself. Bump the pinned version deliberately when
52
+ you've reviewed a new release.
53
+
54
+ ## Troubleshooting
55
+
56
+ **`npx -y @excaliwow/mcp@0.3.0` fails with `ENOENT … /@excaliwow/mcp@0.3.0/package.json`.**
57
+ On some npm/Node versions, `npx` misreads a scoped package + `@version` spec as a
58
+ local directory. It's an upstream npm bug (it reproduces with other scoped
59
+ packages, e.g. `@modelcontextprotocol/server-filesystem@1.0.0`), not an Excaliwow one. Either
60
+ use the unversioned spec `npx -y @excaliwow/mcp` (as the Claude Code command
61
+ above does), or pin safely by installing once and pointing the client at the
62
+ binary:
63
+
64
+ ```sh
65
+ npm i -g @excaliwow/mcp@0.3.0
66
+ ```
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "excaliwow": {
72
+ "command": "excaliwow-mcp",
73
+ "env": { "EXCALIWOW_TOKEN": "excw_pat_…" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ With Claude Code: `claude mcp add excaliwow --scope local --env EXCALIWOW_TOKEN=… -- excaliwow-mcp`.
80
+
81
+ **"Not authenticated" / 401 / the agent's tool calls fail.** The server starts
82
+ even without a token (so it can list its tools), so a missing or invalid
83
+ `EXCALIWOW_TOKEN` only surfaces when the agent first calls a tool. Starting with
84
+ no token prints a one-line `EXCALIWOW_TOKEN is not set` warning to **stderr**. To
85
+ check a token directly, run the health probe — it makes one authenticated read
86
+ and prints a clear verdict (`ok`, `401 — token is invalid or expired`, or
87
+ unreachable):
88
+
89
+ ```sh
90
+ npx -y @excaliwow/mcp --health
91
+ ```
92
+
93
+ ### CLI flags
94
+
95
+ | Flag | Effect |
96
+ | ----------- | ------------------------------------------------------------------ |
97
+ | `--health` | Check the token + API reachability, then exit (0 ok, 1 bad token). |
98
+ | `--version` | Print the installed version and exit. |
99
+ | `--help` | Print usage (flags + env vars) and exit. |
37
100
 
38
101
  ## Tools
39
102
 
40
- Five tools, scoped to safe agent use:
103
+ Seven tools, scoped to safe agent use:
41
104
 
42
- | Tool | What it does |
43
- | ------------------ | --------------------------------------------------------------------------- |
44
- | `generate_diagram` | Create a diagram from the high-level node/edge DSL; returns the editor URL. |
45
- | `read_diagram` | Compact summary (title + per-type element counts) **plus** a rendered PNG. |
46
- | `list_diagrams` | Page through your diagrams. |
47
- | `move_diagram` | Move a diagram to a folder (or to root). |
48
- | `edit_diagram` | Additively merge a DSL fragment (add nodes/edges, update node style/label). |
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 element counts) **plus** a rendered PNG. |
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
+ | `trash_diagram` | `delete` | Soft-delete a diagram to trash. **Reversible** (see `restore_diagram`). |
113
+ | `restore_diagram` | `delete` | Restore a trashed diagram, reopening it at its original id and URL. |
49
114
 
50
115
  `read_diagram` returns a summary + image, **never** the raw scene JSON, to keep
51
- context small. Making a diagram publicly shareable is deliberately **not** an
52
- agent tool so a misled agent can't be tricked into exposing your diagram. Use
53
- the dashboard or the CLI to publish.
116
+ context small. `trash_diagram` / `restore_diagram` are a **reversible** pair
117
+ gated on the `delete` capability registered always, they return a clean
118
+ `insufficient_scope` error (changing nothing) unless the token carries `delete`,
119
+ so a `read` + `write` token can't trash anything. Hard-delete/purge and making a
120
+ diagram publicly shareable are deliberately **not** agent tools — those are
121
+ irreversible, so a misled agent can't destroy or expose your diagram. Purge or
122
+ publish from the dashboard or the CLI.
54
123
 
55
124
  ### DSL discovery
56
125
 
@@ -67,14 +136,24 @@ resource at **`excaliwow://dsl/reference`**.
67
136
 
68
137
  ## Security notes
69
138
 
70
- - **Pin the version** in your client config (above) rather than floating on
71
- `@latest`, and review release notes before bumping.
139
+ - **Keep the token out of anything you commit.** A project-scoped config that
140
+ lives in the repo (a committed `.mcp.json`, or `claude mcp add --scope
141
+ project`) puts the token into git history. Use a user- or local-scoped config
142
+ (`--scope local`), or reference an environment variable instead of pasting the
143
+ literal token.
144
+ - **Mint with `read` + `write` (add `delete` only if you want trash/restore).**
145
+ Five of the seven tools need just `read` + `write`; a `read` + `write` PAT can
146
+ neither expose your diagrams publicly nor delete them, even if the agent is
147
+ misled (`trash_diagram` / `restore_diagram` simply return `insufficient_scope`
148
+ and do nothing). Add the `delete` capability only if you want the agent to be
149
+ able to soft-delete and restore — it's reversible, but it's still a capability
150
+ to grant deliberately. Pick capabilities specifically rather than the coarse
151
+ `read-write` preset, which additionally grants `publish` (and `delete`).
152
+ - **Pin the version** in your client config rather than floating on `@latest`,
153
+ and review release notes before bumping.
72
154
  - The token is a credential to your account. It lives only in your MCP client
73
155
  config / environment; this server does not persist it. Treat that config the
74
156
  way you'd treat any secrets file.
75
- - The token authorizes the same operations as a `read-write` PAT used by the
76
- REST API and CLI — there is no separate per-capability scope, so treat it as
77
- full read-write access to your account.
78
157
 
79
158
  ## License
80
159
 
package/dist/bin.js CHANGED
@@ -3,10 +3,6 @@
3
3
  // src/bin.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
- // src/server.ts
7
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z as z2 } from "zod";
9
-
10
6
  // ../core/src/config.ts
11
7
  import { chmodSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
12
8
  import { homedir } from "os";
@@ -209,6 +205,28 @@ function moveDiagram(ctx, id, folderId) {
209
205
  fetchImpl: ctx.fetchImpl
210
206
  });
211
207
  }
208
+ function deleteDiagram(ctx, id) {
209
+ return request({
210
+ method: "DELETE",
211
+ path: `${PREFIX}/diagrams/${encodeURIComponent(id)}`,
212
+ token: ctx.token,
213
+ baseUrl: ctx.baseUrl,
214
+ fetchImpl: ctx.fetchImpl
215
+ });
216
+ }
217
+ function restoreDiagram(ctx, id) {
218
+ return request({
219
+ method: "POST",
220
+ path: `${PREFIX}/diagrams/${encodeURIComponent(id)}/restore`,
221
+ token: ctx.token,
222
+ baseUrl: ctx.baseUrl,
223
+ fetchImpl: ctx.fetchImpl
224
+ });
225
+ }
226
+
227
+ // src/server.ts
228
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
229
+ import { z as z2 } from "zod";
212
230
 
213
231
  // src/bootstrap.ts
214
232
  var DSL_BOOTSTRAP_EXAMPLE = {
@@ -426,6 +444,9 @@ var EditFragmentZ = z.object({
426
444
  updateNodes: z.array(UpdateNodePatchZ).optional()
427
445
  });
428
446
 
447
+ // src/version.ts
448
+ var VERSION = "0.3.0";
449
+
429
450
  // src/server.ts
430
451
  function textResult(text) {
431
452
  return { content: [{ type: "text", text }] };
@@ -467,7 +488,7 @@ function elementBreakdown(detail) {
467
488
  return { total: elements.length, byType };
468
489
  }
469
490
  function createServer(deps) {
470
- const server2 = new McpServer({ name: "excaliwow", version: "0.1.1" });
491
+ const server2 = new McpServer({ name: "excaliwow", version: VERSION });
471
492
  server2.registerTool(
472
493
  "generate_diagram",
473
494
  {
@@ -622,6 +643,40 @@ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fo
622
643
  }
623
644
  }
624
645
  );
646
+ server2.registerTool(
647
+ "trash_diagram",
648
+ {
649
+ title: "Trash a diagram (soft delete)",
650
+ description: "Soft-delete a diagram to trash. REVERSIBLE \u2014 restore_diagram brings it back, and it stays visible under list_diagrams with filter='trash'. Use this to clean up diagrams you no longer want (e.g. earlier drafts from iterating). Requires a Personal Access Token with the `delete` capability; without it the call returns a clean 'insufficient_scope' error and changes nothing. Idempotent on an already-trashed id.",
651
+ inputSchema: { id: z2.string() }
652
+ },
653
+ async ({ id }) => {
654
+ try {
655
+ const ctx = buildCtx(deps);
656
+ await deleteDiagram(ctx, id);
657
+ return textResult(JSON.stringify({ id, trashed: true }));
658
+ } catch (err) {
659
+ return toErrorResult(err);
660
+ }
661
+ }
662
+ );
663
+ server2.registerTool(
664
+ "restore_diagram",
665
+ {
666
+ title: "Restore a diagram from trash",
667
+ description: "Restore a soft-deleted diagram from trash, reopening it in the editor at its original id and url. The inverse of trash_diagram. Requires a Personal Access Token with the `delete` capability; without it the call returns a clean 'insufficient_scope' error and changes nothing. Idempotent on an already-active id.",
668
+ inputSchema: { id: z2.string() }
669
+ },
670
+ async ({ id }) => {
671
+ try {
672
+ const ctx = buildCtx(deps);
673
+ await restoreDiagram(ctx, id);
674
+ return textResult(JSON.stringify({ id, restored: true }));
675
+ } catch (err) {
676
+ return toErrorResult(err);
677
+ }
678
+ }
679
+ );
625
680
  server2.registerResource(
626
681
  "dsl-reference",
627
682
  DSL_REFERENCE_URI,
@@ -640,5 +695,83 @@ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fo
640
695
  }
641
696
 
642
697
  // src/bin.ts
698
+ var argv = process.argv.slice(2);
699
+ var has = (...flags) => argv.some((a) => flags.includes(a));
700
+ if (has("--version", "-v")) {
701
+ process.stdout.write(`${VERSION}
702
+ `);
703
+ process.exit(0);
704
+ }
705
+ if (has("--help", "-h")) {
706
+ process.stdout.write(
707
+ [
708
+ `excaliwow-mcp ${VERSION} \u2014 Excaliwow Model Context Protocol server (stdio).`,
709
+ "",
710
+ "An MCP client (Claude Desktop, Claude Code, \u2026) launches this on demand; you",
711
+ "do not normally run it by hand. The flags below are for setup/debugging.",
712
+ "",
713
+ "Usage:",
714
+ " excaliwow-mcp Start the MCP server on stdio (what a client runs).",
715
+ " excaliwow-mcp --health Check the token + API reachability, then exit.",
716
+ " excaliwow-mcp --version Print the version and exit.",
717
+ " excaliwow-mcp --help Print this help and exit.",
718
+ "",
719
+ "Environment:",
720
+ " EXCALIWOW_TOKEN Personal Access Token with read + write. Required.",
721
+ " Mint one at https://excaliwow.com/app/settings.",
722
+ " EXCALIWOW_API_URL API origin. Defaults to https://excaliwow.com.",
723
+ ""
724
+ ].join("\n")
725
+ );
726
+ process.exit(0);
727
+ }
728
+ if (has("--health")) {
729
+ process.exit(await health());
730
+ }
731
+ if (!resolveToken()) {
732
+ process.stderr.write(
733
+ "[excaliwow-mcp] warning: EXCALIWOW_TOKEN is not set (and no `excaliwow auth login` config was found). The server will start, but every tool call will fail with an auth error until you set a Personal Access Token. See https://excaliwow.com/docs/mcp\n"
734
+ );
735
+ }
643
736
  var server = createServer();
644
737
  await server.connect(new StdioServerTransport());
738
+ async function health() {
739
+ const token = resolveToken();
740
+ const baseUrl = resolveBaseUrl({});
741
+ if (!token) {
742
+ process.stderr.write(
743
+ "[excaliwow-mcp] health: EXCALIWOW_TOKEN is not set \u2014 no token to check.\n"
744
+ );
745
+ return 1;
746
+ }
747
+ try {
748
+ await listDiagrams({ token, baseUrl }, { limit: 1 });
749
+ process.stderr.write(`[excaliwow-mcp] health: ok \u2014 token authenticates against ${baseUrl}.
750
+ `);
751
+ return 0;
752
+ } catch (err) {
753
+ if (err instanceof ApiError && err.status === 401) {
754
+ process.stderr.write(
755
+ `[excaliwow-mcp] health: 401 \u2014 token is invalid or expired (${baseUrl}).
756
+ `
757
+ );
758
+ return 1;
759
+ }
760
+ if (err instanceof ApiError && err.status === 403) {
761
+ process.stderr.write(
762
+ `[excaliwow-mcp] health: token authenticates against ${baseUrl}, but lacks the \`read\` capability. Mint a token with read + write.
763
+ `
764
+ );
765
+ return 0;
766
+ }
767
+ if (err instanceof NotAuthenticatedError) {
768
+ process.stderr.write(`[excaliwow-mcp] health: ${err.message}
769
+ `);
770
+ return 1;
771
+ }
772
+ const msg = err instanceof Error ? err.message : String(err);
773
+ process.stderr.write(`[excaliwow-mcp] health: could not reach ${baseUrl} \u2014 ${msg}
774
+ `);
775
+ return 2;
776
+ }
777
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@excaliwow/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {