@excaliwow/mcp 0.1.1 → 0.2.1

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 +90 -21
  2. package/dist/bin.js +98 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,25 @@ 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 — that
12
+ is everything the server's tools need (see [Security notes](#security-notes)).
13
+ Pass it as `EXCALIWOW_TOKEN`.
14
+
15
+ ### Claude Code (CLI)
16
+
17
+ One command. `--scope local` stores the server in your own settings, so the
18
+ token never lands in a file you might commit:
19
+
20
+ ```sh
21
+ claude mcp add excaliwow --scope local \
22
+ --env EXCALIWOW_TOKEN=excw_pat_… \
23
+ -- npx -y @excaliwow/mcp
24
+ ```
25
+
26
+ ### Claude Desktop (and other JSON-config clients)
27
+
28
+ Add it to your client's config file (e.g. Claude Desktop's
11
29
  `claude_desktop_config.json`, or your client's MCP settings — see your client's
12
30
  MCP setup docs). **Pin the version** — `npx -y` otherwise always pulls the newest
13
31
  release, which is an avoidable supply-chain surface for a process that holds a
@@ -18,7 +36,7 @@ token to your account:
18
36
  "mcpServers": {
19
37
  "excaliwow": {
20
38
  "command": "npx",
21
- "args": ["-y", "@excaliwow/mcp@0.1.1"],
39
+ "args": ["-y", "@excaliwow/mcp@0.2.1"],
22
40
  "env": {
23
41
  "EXCALIWOW_TOKEN": "excw_pat_…"
24
42
  }
@@ -27,25 +45,69 @@ token to your account:
27
45
  }
28
46
  ```
29
47
 
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.
48
+ The server reads `EXCALIWOW_TOKEN` per call from the environment (or, if you also
49
+ use `@excaliwow/cli` and have run `excaliwow auth login`, that stored login) and
50
+ never writes the token to disk itself. Bump the pinned version deliberately when
51
+ you've reviewed a new release.
52
+
53
+ ## Troubleshooting
54
+
55
+ **`npx -y @excaliwow/mcp@0.2.1` fails with `ENOENT … /@excaliwow/mcp@0.2.1/package.json`.**
56
+ On some npm/Node versions, `npx` misreads a scoped package + `@version` spec as a
57
+ local directory. It's an upstream npm bug (it reproduces with other scoped
58
+ packages, e.g. `@modelcontextprotocol/server-filesystem@1.0.0`), not an Excaliwow one. Either
59
+ use the unversioned spec `npx -y @excaliwow/mcp` (as the Claude Code command
60
+ above does), or pin safely by installing once and pointing the client at the
61
+ binary:
62
+
63
+ ```sh
64
+ npm i -g @excaliwow/mcp@0.2.1
65
+ ```
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "excaliwow": {
71
+ "command": "excaliwow-mcp",
72
+ "env": { "EXCALIWOW_TOKEN": "excw_pat_…" }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ With Claude Code: `claude mcp add excaliwow --scope local --env EXCALIWOW_TOKEN=… -- excaliwow-mcp`.
79
+
80
+ **"Not authenticated" / 401 / the agent's tool calls fail.** The server starts
81
+ even without a token (so it can list its tools), so a missing or invalid
82
+ `EXCALIWOW_TOKEN` only surfaces when the agent first calls a tool. Starting with
83
+ no token prints a one-line `EXCALIWOW_TOKEN is not set` warning to **stderr**. To
84
+ check a token directly, run the health probe — it makes one authenticated read
85
+ and prints a clear verdict (`ok`, `401 — token is invalid or expired`, or
86
+ unreachable):
87
+
88
+ ```sh
89
+ npx -y @excaliwow/mcp --health
90
+ ```
91
+
92
+ ### CLI flags
93
+
94
+ | Flag | Effect |
95
+ | ----------- | ------------------------------------------------------------------ |
96
+ | `--health` | Check the token + API reachability, then exit (0 ok, 1 bad token). |
97
+ | `--version` | Print the installed version and exit. |
98
+ | `--help` | Print usage (flags + env vars) and exit. |
37
99
 
38
100
  ## Tools
39
101
 
40
102
  Five tools, scoped to safe agent use:
41
103
 
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). |
104
+ | Tool | Capability | What it does |
105
+ | ------------------ | ---------- | --------------------------------------------------------------------------- |
106
+ | `generate_diagram` | `write` | Create a diagram from the high-level node/edge DSL; returns the editor URL. |
107
+ | `read_diagram` | `read` | Compact summary (title + per-type element counts) **plus** a rendered PNG. |
108
+ | `list_diagrams` | `read` | Page through your diagrams. |
109
+ | `move_diagram` | `write` | Move a diagram to a folder (or to root). |
110
+ | `edit_diagram` | `write` | Additively merge a DSL fragment (add nodes/edges, update node style/label). |
49
111
 
50
112
  `read_diagram` returns a summary + image, **never** the raw scene JSON, to keep
51
113
  context small. Making a diagram publicly shareable is deliberately **not** an
@@ -67,14 +129,21 @@ resource at **`excaliwow://dsl/reference`**.
67
129
 
68
130
  ## Security notes
69
131
 
70
- - **Pin the version** in your client config (above) rather than floating on
71
- `@latest`, and review release notes before bumping.
132
+ - **Keep the token out of anything you commit.** A project-scoped config that
133
+ lives in the repo (a committed `.mcp.json`, or `claude mcp add --scope
134
+ project`) puts the token into git history. Use a user- or local-scoped config
135
+ (`--scope local`), or reference an environment variable instead of pasting the
136
+ literal token.
137
+ - **Mint with only `read` + `write`.** Those are the only capabilities the five
138
+ tools use (see the table above); none publish or delete. So a `read` + `write`
139
+ PAT can neither expose your diagrams publicly nor delete them, even if the
140
+ agent is misled. Pick those two capabilities specifically — the coarse
141
+ `read-write` preset additionally grants `publish` and `delete`.
142
+ - **Pin the version** in your client config rather than floating on `@latest`,
143
+ and review release notes before bumping.
72
144
  - The token is a credential to your account. It lives only in your MCP client
73
145
  config / environment; this server does not persist it. Treat that config the
74
146
  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
147
 
79
148
  ## License
80
149
 
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";
@@ -100,7 +96,12 @@ async function request(opts) {
100
96
  if (res.status === 204) return null;
101
97
  if (contentType.includes("image/")) {
102
98
  const buf = new Uint8Array(await res.arrayBuffer());
103
- return { bytes: buf, contentType };
99
+ return {
100
+ bytes: buf,
101
+ contentType,
102
+ quality: res.headers.get("x-render-quality") ?? void 0,
103
+ note: res.headers.get("x-render-note") ?? void 0
104
+ };
104
105
  }
105
106
  const text = await res.text();
106
107
  if (text === "") return null;
@@ -177,7 +178,9 @@ function renderDiagram(ctx, id, opts) {
177
178
  path: `${PREFIX}/diagrams/${encodeURIComponent(id)}/render`,
178
179
  token: ctx.token,
179
180
  baseUrl: ctx.baseUrl,
180
- query: { format: opts.format },
181
+ // `quality` (#220) — omitted ⇒ the server default (faithful). An older
182
+ // server ignores it and serves fast; the response's quality signal tells.
183
+ query: { format: opts.format, quality: opts.quality },
181
184
  accept,
182
185
  fetchImpl: ctx.fetchImpl
183
186
  });
@@ -203,6 +206,10 @@ function moveDiagram(ctx, id, folderId) {
203
206
  });
204
207
  }
205
208
 
209
+ // src/server.ts
210
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
211
+ import { z as z2 } from "zod";
212
+
206
213
  // src/bootstrap.ts
207
214
  var DSL_BOOTSTRAP_EXAMPLE = {
208
215
  version: "1",
@@ -419,6 +426,9 @@ var EditFragmentZ = z.object({
419
426
  updateNodes: z.array(UpdateNodePatchZ).optional()
420
427
  });
421
428
 
429
+ // src/version.ts
430
+ var VERSION = "0.2.1";
431
+
422
432
  // src/server.ts
423
433
  function textResult(text) {
424
434
  return { content: [{ type: "text", text }] };
@@ -460,7 +470,7 @@ function elementBreakdown(detail) {
460
470
  return { total: elements.length, byType };
461
471
  }
462
472
  function createServer(deps) {
463
- const server2 = new McpServer({ name: "excaliwow", version: "0.1.1" });
473
+ const server2 = new McpServer({ name: "excaliwow", version: VERSION });
464
474
  server2.registerTool(
465
475
  "generate_diagram",
466
476
  {
@@ -516,9 +526,11 @@ function createServer(deps) {
516
526
  try {
517
527
  const png = await renderDiagram(ctx, id, { format: "png" });
518
528
  if (png && png.bytes.length > 0) {
529
+ const text = png.quality && png.quality !== "faithful" ? `${summary}
530
+ preview fidelity: ${png.quality} \u2014 faithful renderer unavailable; layout/fonts approximate` : summary;
519
531
  return {
520
532
  content: [
521
- { type: "text", text: summary },
533
+ { type: "text", text },
522
534
  {
523
535
  type: "image",
524
536
  data: Buffer.from(png.bytes).toString("base64"),
@@ -631,5 +643,83 @@ function createServer(deps) {
631
643
  }
632
644
 
633
645
  // src/bin.ts
646
+ var argv = process.argv.slice(2);
647
+ var has = (...flags) => argv.some((a) => flags.includes(a));
648
+ if (has("--version", "-v")) {
649
+ process.stdout.write(`${VERSION}
650
+ `);
651
+ process.exit(0);
652
+ }
653
+ if (has("--help", "-h")) {
654
+ process.stdout.write(
655
+ [
656
+ `excaliwow-mcp ${VERSION} \u2014 Excaliwow Model Context Protocol server (stdio).`,
657
+ "",
658
+ "An MCP client (Claude Desktop, Claude Code, \u2026) launches this on demand; you",
659
+ "do not normally run it by hand. The flags below are for setup/debugging.",
660
+ "",
661
+ "Usage:",
662
+ " excaliwow-mcp Start the MCP server on stdio (what a client runs).",
663
+ " excaliwow-mcp --health Check the token + API reachability, then exit.",
664
+ " excaliwow-mcp --version Print the version and exit.",
665
+ " excaliwow-mcp --help Print this help and exit.",
666
+ "",
667
+ "Environment:",
668
+ " EXCALIWOW_TOKEN Personal Access Token with read + write. Required.",
669
+ " Mint one at https://excaliwow.com/app/settings.",
670
+ " EXCALIWOW_API_URL API origin. Defaults to https://excaliwow.com.",
671
+ ""
672
+ ].join("\n")
673
+ );
674
+ process.exit(0);
675
+ }
676
+ if (has("--health")) {
677
+ process.exit(await health());
678
+ }
679
+ if (!resolveToken()) {
680
+ process.stderr.write(
681
+ "[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"
682
+ );
683
+ }
634
684
  var server = createServer();
635
685
  await server.connect(new StdioServerTransport());
686
+ async function health() {
687
+ const token = resolveToken();
688
+ const baseUrl = resolveBaseUrl({});
689
+ if (!token) {
690
+ process.stderr.write(
691
+ "[excaliwow-mcp] health: EXCALIWOW_TOKEN is not set \u2014 no token to check.\n"
692
+ );
693
+ return 1;
694
+ }
695
+ try {
696
+ await listDiagrams({ token, baseUrl }, { limit: 1 });
697
+ process.stderr.write(`[excaliwow-mcp] health: ok \u2014 token authenticates against ${baseUrl}.
698
+ `);
699
+ return 0;
700
+ } catch (err) {
701
+ if (err instanceof ApiError && err.status === 401) {
702
+ process.stderr.write(
703
+ `[excaliwow-mcp] health: 401 \u2014 token is invalid or expired (${baseUrl}).
704
+ `
705
+ );
706
+ return 1;
707
+ }
708
+ if (err instanceof ApiError && err.status === 403) {
709
+ process.stderr.write(
710
+ `[excaliwow-mcp] health: token authenticates against ${baseUrl}, but lacks the \`read\` capability. Mint a token with read + write.
711
+ `
712
+ );
713
+ return 0;
714
+ }
715
+ if (err instanceof NotAuthenticatedError) {
716
+ process.stderr.write(`[excaliwow-mcp] health: ${err.message}
717
+ `);
718
+ return 1;
719
+ }
720
+ const msg = err instanceof Error ? err.message : String(err);
721
+ process.stderr.write(`[excaliwow-mcp] health: could not reach ${baseUrl} \u2014 ${msg}
722
+ `);
723
+ return 2;
724
+ }
725
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@excaliwow/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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": {