pmx-canvas 0.1.5 → 0.1.6

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.
@@ -22,14 +22,41 @@ relatedness, clusters imply grouping, reading order (top-left to bottom-right) i
22
22
 
23
23
  ## When to Use
24
24
 
25
- - **Investigation boards** lay out files, logs, stack traces, and findings spatially while debugging
26
- - **Architecture diagrams** show system components and their relationships
27
- - **Plans & task tracking** create task nodes with dependencies and color-coded status
28
- - **Status dashboards** display build results, test output, deployment state
29
- - **Context maps** show how code, configs, and data flow connect
30
- - **Code dependency graphs** visualize file imports and module relationships
31
- - **Comparison views** place options side by side for the human to evaluate
32
- - **Any time spatial layout helps** — when a flat list or text wall is not enough
25
+ The canvas is **agnostic about what you do with it**. Reach for it whenever a
26
+ flat conversation, a list, or a single document hides the relationships
27
+ between pieces of information. The total reach of any session is the union
28
+ of pmx-canvas's own node types and whatever your harness already has access
29
+ toMCP servers, MCP apps, shell commands and CLIs, files in the working
30
+ directory, web fetch, anything else in your toolbelt. The canvas does not
31
+ care where the data came from; it cares about getting it on the surface as
32
+ the right node type.
33
+
34
+ The README has a non-exhaustive list of example use cases (idea generation,
35
+ validation, research, analysis, mind mapping, investigation boards,
36
+ architecture diagrams, status dashboards, comparison views, plus whatever
37
+ your toolbelt unlocks). This skill stays focused on the operational
38
+ mechanics. If a flat list or text wall is not enough to hold the
39
+ relationships you're working with, the canvas is the right tool — the rest
40
+ of this document is how to drive it.
41
+
42
+ ### When the connected tool is unfamiliar
43
+
44
+ When the human asks you to use a data source or tool you have not used
45
+ before — an MCP server, an MCP app, a CLI, a script, an arbitrary file
46
+ tree:
47
+
48
+ 1. List the tools / commands available; sample one or two outputs to see
49
+ the actual shape.
50
+ 2. Decide which canvas node type best matches each output:
51
+ - Long-form / narrative results → `markdown`
52
+ - Structured records, tables, dashboards → `json-render` or `graph`
53
+ - Interactive tool surfaces with their own UI → `mcp-app` (open with
54
+ `canvas_open_mcp_app`)
55
+ - Local source files → `file` (live-watched)
56
+ - URLs that need cached fetches → `webpage`
57
+ - Stream of state events → `status` or `ledger`
58
+ 3. Propose the mapping to the human before bulk-creating nodes; let them
59
+ confirm or adjust before you commit a layout.
33
60
 
34
61
  ## Starting the Canvas
35
62
 
@@ -46,11 +73,31 @@ pmx-canvas --no-open # Start without opening browser (for agents)
46
73
  pmx-canvas --port=8080 # Custom port
47
74
  pmx-canvas --demo # Start with sample content
48
75
  pmx-canvas --theme=light # Light theme
76
+ pmx-canvas --version # Print installed version and exit
49
77
  ```
50
78
 
79
+ `--theme` accepts `dark` (default), `light`, or `high-contrast`. Same value can be set via
80
+ the `PMX_CANVAS_THEME` environment variable, or toggled live in the browser toolbar.
81
+
51
82
  Start the canvas once per session, then reuse it. Use `--no-open` when running as an agent — the
52
83
  human can open the browser URL themselves.
53
84
 
85
+ ### Daemon mode (long-running background server)
86
+
87
+ When you need the canvas to outlive the current shell or agent session — e.g. so a follow-up
88
+ agent run can attach to the same state — start it as a daemon:
89
+
90
+ ```bash
91
+ pmx-canvas serve --daemon --no-open --wait-ms=20000 # Detach, wait for /health
92
+ pmx-canvas serve status # Print daemon health + pid state
93
+ pmx-canvas serve stop # Stop the daemon for this port
94
+ ```
95
+
96
+ `serve --daemon` writes a pid file (`.pmx-canvas/daemon-<port>.pid`) and a log file
97
+ (`.pmx-canvas/daemon-<port>.log`); the wait flag blocks until `/health` returns OK so a script
98
+ can rely on the server being responsive when the command returns. `serve stop` reads the pid
99
+ file, sends SIGTERM, and cleans up on exit.
100
+
54
101
  ## Browser Workflows
55
102
 
56
103
  The browser is not just a passive view. Human interactions on the canvas persist back to the
@@ -86,6 +133,7 @@ pmx-canvas node add --type markdown --title "Plan"
86
133
  pmx-canvas node add --type webpage --url https://example.com/docs
87
134
  pmx-canvas node add --type graph --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
88
135
  pmx-canvas node add --type graph --graphType bar --data '[{"x":"a","y":1}]' --xKey x --yKey y
136
+ pmx-canvas graph add --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
89
137
  pmx-canvas external-app add --kind excalidraw --title "Diagram"
90
138
  pmx-canvas node add --help --type webpage --json
91
139
  pmx-canvas node schema --type json-render --component Table --summary
@@ -109,7 +157,11 @@ pmx-canvas spatial
109
157
 
110
158
  - `node add|list|get|update|remove` — manage nodes
111
159
  - `node schema` — inspect running-server create schemas and canonical examples, with `--summary`, `--field`, and `--component` filters
160
+ - `graph add` — convenience alias for graph nodes; `node add --type graph` remains the canonical form
112
161
  - Graph CLI fields accept both kebab-case flags and camelCase schema names, e.g. `--graph-type`/`--graphType`, `--x-key`/`--xKey`, and `--bar-color`/`--barColor`.
162
+ - Graph CLI height flags are split: use `--node-height`/`--nodeHeight` for the
163
+ canvas frame and `--chart-height` for the chart content. CLI `--height`
164
+ remains a frame-height compatibility alias.
113
165
  - `edge add|list|remove` — manage edges
114
166
  - Search-based edge selectors must be specific enough to resolve exactly one node. Queries like
115
167
  `"DVT O3"` can be ambiguous; prefer the full visible title such as `"DVT O3 — GitOps"`.
@@ -139,6 +191,8 @@ Current caveat:
139
191
  - Generic `pmx-canvas node add --type mcp-app` is intentionally not supported because app nodes
140
192
  need app/session metadata. Use `pmx-canvas web-artifact build` for bundled React artifacts or
141
193
  `pmx-canvas external-app add --kind excalidraw` for the Excalidraw preset.
194
+ - For local `image` nodes on macOS, iCloud/OneDrive cloud-only placeholder files are rejected with
195
+ a download-first hint. Download the image locally before adding it to the canvas.
142
196
 
143
197
  The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_URL` or
144
198
  `PMX_CANVAS_PORT` when the canvas is running elsewhere.
@@ -160,8 +214,8 @@ The CLI targets `http://localhost:4313` by default. Override with `PMX_CANVAS_UR
160
214
  | `json-render` | Native structured UI panel | Dashboards, forms, tables, interactive layouts from json-render specs |
161
215
  | `graph` | Native chart panel | Line, bar, pie, area, scatter, radar, stacked-bar, and composed charts rendered inside the canvas |
162
216
  | `group` | Spatial container/frame | Visually group related nodes together |
163
- | `prompt` | Prompt thread root | Canvas-native prompt entry points for agent conversations |
164
- | `response` | Prompt reply / streamed answer | Agent responses linked to prompt threads |
217
+ | `prompt` | Prompt thread root | Canvas-native prompt entry points for agent conversations. **Internal type — surfaces in `canvas://layout` for thread rendering but is not created via the public `canvas_add_node` API. Don't try to add one directly.** |
218
+ | `response` | Prompt reply / streamed answer | Agent responses linked to prompt threads. **Same internal-only restriction as `prompt`.** |
165
219
 
166
220
  ### Edge Types
167
221
 
@@ -196,8 +250,22 @@ Use color consistently to convey meaning:
196
250
 
197
251
  ### Node Operations
198
252
 
253
+ MCP node-type routing:
254
+
255
+ | Node category | MCP creation tool |
256
+ |---------------|-------------------|
257
+ | Basic nodes (`markdown`, `status`, `context`, `ledger`, `trace`, `file`, `image`, `webpage`) | `canvas_add_node` |
258
+ | `json-render` | `canvas_add_json_render_node` |
259
+ | `graph` | `canvas_add_graph_node` |
260
+ | `web-artifact` | `canvas_build_web_artifact` |
261
+ | `external-app` / tool-backed `mcp-app` | `canvas_open_mcp_app` |
262
+ | `group` | `canvas_create_group` |
263
+
264
+ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` and read
265
+ `mcp.nodeTypeRouting`; do not keep retrying the generic tool.
266
+
199
267
  **`canvas_add_node`** — Add a node to the canvas
200
- - `type` (required): node type (see table above)
268
+ - `type` (required): basic node type only; structured/app/group nodes use the routing table above
201
269
  - `title`: short, scannable title
202
270
  - `content`: for most types, this is markdown text. For `file` type, pass the **file path**
203
271
  (e.g., `"src/auth/login.ts"`) — the server auto-loads the file content and watches for changes.
@@ -220,10 +288,30 @@ Use color consistently to convey meaning:
220
288
  **`canvas_get_node`** — Get a single node's full data
221
289
  - `id` (required): node to retrieve
222
290
 
291
+ **`canvas_refresh_webpage_node`** — Re-fetch the URL stored on a `webpage` node
292
+ - `id` (required): webpage node to refresh
293
+ - Optional `url`: replace the stored URL before refreshing (use when the human moved the page)
294
+ - Returns the refreshed node with updated `pageTitle` and cached extracted text
295
+ - Use this when a saved canvas is reopened and the agent needs fresh page content without
296
+ losing the node's identity, position, or pins. Example flow:
297
+
298
+ ```typescript
299
+ // Add the page once
300
+ canvas_add_node({ type: 'webpage', url: 'https://example.com/docs' })
301
+ // → returns { id: 'node-abc' }
302
+
303
+ // …later, after the human reopens the canvas…
304
+ canvas_refresh_webpage_node({ id: 'node-abc' })
305
+ // → re-fetches the URL, updates pageTitle + extracted text, keeps the node ID and position
306
+ ```
307
+
223
308
  **`canvas_add_json_render_node`** — Add a native json-render node
224
309
  - Required: `title`, `spec`
225
310
  - The `spec` must be a complete json-render object with `root`, `elements`, and optional `state`
226
311
  - Use this when you want a structured UI panel rendered directly inside PMX Canvas
312
+ - For shadcn `Badge`, prefer `props.text` with variants `default`, `secondary`, `destructive`, or
313
+ `outline`. Legacy `props.label` and status variants (`success`, `info`, `warning`, `error`,
314
+ `danger`) are normalized for saved-spec compatibility.
227
315
 
228
316
  **`canvas_add_graph_node`** — Add a native graph/chart node
229
317
  - Required: `graphType`, `data`
@@ -235,10 +323,29 @@ Use color consistently to convey meaning:
235
323
  - Use `axisKey` plus `metrics` for radar graphs
236
324
  - Use `series` for stacked-bar graphs
237
325
  - Use `barKey`/`lineKey` plus optional `barColor`/`lineColor` for composed graphs
326
+ - Use `nodeHeight` for the canvas frame height and `height` for chart content height
238
327
  - Uses the native json-render chart catalog under the hood
239
328
 
329
+ **`canvas_build_web_artifact`** — Build and optionally open a bundled web artifact
330
+ - Required: `title`, `appTsx` (source string contents, not a file path)
331
+ - CLI `--app-file` reads a file before calling the same build path; MCP callers must pass the source contents
332
+ - Cold builds commonly take 45-60 seconds; use a long client timeout such as 300000 ms or more
333
+ - Returns both `id` and `nodeId` for the created artifact node when `openInCanvas` is true
334
+
335
+ **`canvas_open_mcp_app`** — Open a tool-backed external MCP app node
336
+ - Required: `toolName`, `transport`
337
+ - `transport` is either `{ type: "stdio", command, args?, cwd?, env? }` or `{ type: "http", url, headers? }`
338
+ - This is lower-level than `pmx-canvas external-app add --kind excalidraw`; use `canvas_add_diagram` for the built-in Excalidraw preset
339
+
340
+ **`canvas_pin_nodes`** — Set, add, or remove pinned context nodes
341
+ - Use `{ nodeIds: [...] }`; the field is `nodeIds`, not `ids`
342
+
343
+ **`canvas_diff`** — Compare current canvas state with a saved snapshot
344
+ - Requires `{ snapshot: "<snapshot-id-or-name>" }`; there is no implicit previous-snapshot default
345
+
240
346
  **`canvas_describe_schema`** — Inspect the running server's create schemas and canonical examples
241
347
  - Use this before generating structured payloads when you need the authoritative current shape
348
+ - Read `mcp.nodeTypeRouting` to choose the right MCP creation tool for each node category
242
349
 
243
350
  **`canvas_validate_spec`** — Validate a json-render spec or graph payload without creating a node
244
351
  - Returns the normalized json-render spec the server would accept
@@ -251,6 +358,7 @@ Use color consistently to convey meaning:
251
358
  `xKey`, `yKey`, `zKey`, `nameKey`, `valueKey`, `axisKey`, `metrics`, `series`, `barKey`,
252
359
  `lineKey`, `aggregate`, `color`, `barColor`, `lineColor`, `height`, `x`, `y`, `width`, and
253
360
  `nodeHeight`.
361
+ - In batch/MCP/HTTP payloads, `height` is chart content height and `nodeHeight` is the canvas frame height.
254
362
 
255
363
  ### Edge Operations
256
364
 
@@ -369,6 +477,58 @@ Current product caveats for grouped comparison boards:
369
477
  - **Always call `canvas_snapshot` first** to save a backup before clearing
370
478
  - This is irreversible without a prior snapshot
371
479
 
480
+ ### Browser Automation (WebView)
481
+
482
+ The canvas exposes a headless browser session over MCP for self-inspection and
483
+ automated screenshotting. Use this when you want to (a) verify what the live
484
+ canvas actually looks like after a sequence of mutations, (b) capture an image
485
+ of a freshly-built artifact for the human to review, or (c) drive arbitrary
486
+ JavaScript inside the workbench page.
487
+
488
+ The WebView automation runs on Bun's WebKit-based WebView (macOS) or a headless
489
+ Chromium fallback (Linux). It does **not** open a visible window; it's an
490
+ additional headless renderer attached to the same canvas server, so all five
491
+ tools below operate on the live canvas state.
492
+
493
+ **`canvas_webview_status`** — Inspect the current automation session
494
+ - Returns `{ supported, active, backend, viewportWidth, viewportHeight, url, lastError }`
495
+ - Call before `start` to check whether a session is already alive
496
+
497
+ **`canvas_webview_start`** — Start (or replace) the automation session
498
+ - Optional: `backend` (`webkit` macOS-only, or `chrome`), `width`, `height`
499
+ - The session opens `/workbench` at the canvas URL, waits for the SPA to
500
+ hydrate, and reports back via `canvas_webview_status`
501
+
502
+ **`canvas_webview_stop`** — Tear down the automation session
503
+
504
+ **`canvas_evaluate`** — Run JavaScript inside the workbench page and return the result
505
+ - Required: `expression` (a JS expression or a function body)
506
+ - Useful for asserting DOM state after a sequence of canvas mutations
507
+ - Example: read the count of rendered `.canvas-node` elements:
508
+
509
+ ```typescript
510
+ canvas_evaluate({ expression: 'document.querySelectorAll(".canvas-node").length' })
511
+ ```
512
+
513
+ **`canvas_resize`** — Change the WebView viewport
514
+ - Required: `width`, `height`
515
+ - Use before `canvas_screenshot` when the human needs a specific aspect ratio
516
+
517
+ **`canvas_screenshot`** — Capture a PNG of the current workbench
518
+ - Optional: `format` (`png` default), `fullPage` (boolean)
519
+ - Returns both an MCP image payload (renderable inline by capable agents) and
520
+ a path under `.pmx-canvas/screenshots/` so the human can view the file
521
+ - Pair with `canvas_resize` to control the framing
522
+
523
+ Typical flow when you want to show a result:
524
+
525
+ ```typescript
526
+ canvas_webview_start({ width: 1440, height: 900 });
527
+ // …mutations…
528
+ canvas_screenshot({ fullPage: true });
529
+ canvas_webview_stop();
530
+ ```
531
+
372
532
  ### Diagrams (Excalidraw MCP app preset)
373
533
 
374
534
  **`canvas_add_diagram`** — Draw a hand-drawn diagram on the canvas via the hosted
@@ -432,7 +592,8 @@ what the human has set up and what they're focusing on.
432
592
  | `canvas://summary` | Compact overview: node counts by type, pinned titles |
433
593
  | `canvas://spatial-context` | Proximity clusters, reading order, pinned neighborhoods |
434
594
  | `canvas://history` | Human-readable mutation timeline |
435
- | `canvas://code-graph` | Auto-detected file import dependencies |
595
+ | `canvas://code-graph` | Auto-detected file import dependencies (JS/TS, Python, Go, Rust) |
596
+ | `canvas://skills` | Index of bundled agent skills shipped with the install. Each skill is also addressable as `canvas://skills/<name>` (e.g. `canvas://skills/web-artifacts-builder`) and returns the full SKILL.md. Read this resource first to discover companion workflows the canvas is built to support. |
436
597
 
437
598
  ### Reading Spatial Intent
438
599
 
@@ -493,6 +654,12 @@ All POST/PATCH endpoints accept `Content-Type: application/json`. Default base U
493
654
 
494
655
  ## Workflow Patterns
495
656
 
657
+ These are **operational recipes** — how to sequence canvas calls for a few
658
+ recurring shapes of work. They are not the project's use cases (those live
659
+ in the README and are intentionally non-exhaustive). The patterns here exist
660
+ to make the agent's tool-call sequencing concrete: which MCP tool fires
661
+ when, what to pin, when to read `canvas://pinned-context`, when to snapshot.
662
+
496
663
  ### Responding to Pinned Context
497
664
 
498
665
  When the human pins nodes, they're telling you what matters. This is the most important
package/src/cli/agent.ts CHANGED
@@ -229,6 +229,18 @@ function optionalPositiveFiniteFlag(flags: Record<string, string | true>, name:
229
229
  return parsed;
230
230
  }
231
231
 
232
+ function optionalPositiveFiniteFlagWithAliases(
233
+ flags: Record<string, string | true>,
234
+ hint: string,
235
+ ...names: string[]
236
+ ): number | undefined {
237
+ for (const name of names) {
238
+ const parsed = optionalPositiveFiniteFlag(flags, name, hint);
239
+ if (parsed !== undefined) return parsed;
240
+ }
241
+ return undefined;
242
+ }
243
+
232
244
  function isRecord(value: unknown): value is Record<string, unknown> {
233
245
  return !!value && typeof value === 'object' && !Array.isArray(value);
234
246
  }
@@ -590,7 +602,13 @@ async function buildGraphRequestBody(
590
602
  const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
591
603
  const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
592
604
  const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
593
- const nodeHeight = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 520');
605
+ const nodeHeight = optionalPositiveFiniteFlagWithAliases(
606
+ flags,
607
+ 'Use a positive number, e.g. --node-height 520',
608
+ 'node-height',
609
+ 'nodeHeight',
610
+ 'height',
611
+ );
594
612
  if (chartHeight !== undefined) body.height = chartHeight;
595
613
  if (x !== undefined) body.x = x;
596
614
  if (y !== undefined) body.y = y;
@@ -992,6 +1010,18 @@ cmd('node add', 'Add a node to the canvas', [
992
1010
  output(result);
993
1011
  });
994
1012
 
1013
+ cmd('graph add', 'Add a graph node to the canvas', [
1014
+ 'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1015
+ 'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
1016
+ 'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1017
+ ], async (args) => {
1018
+ const { flags } = parseFlags(args);
1019
+ if (flags.help || flags.h) return showCommandHelp('graph add');
1020
+
1021
+ const result = await api('POST', '/api/canvas/graph', await buildGraphRequestBody(flags));
1022
+ output(result);
1023
+ });
1024
+
995
1025
  cmd('node schema', 'Describe server-supported node create schemas and canonical examples', [
996
1026
  'pmx-canvas node schema',
997
1027
  'pmx-canvas node schema --type webpage',
@@ -2056,9 +2086,10 @@ function showCommandHelp(name: string): void {
2056
2086
  console.log(' pmx-canvas node add --help --type graph');
2057
2087
  console.log(' pmx-canvas node add --help --type webpage --json');
2058
2088
  }
2059
- if (name === 'node add' || name === 'validate spec') {
2089
+ if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
2060
2090
  console.log('\nGraph flags:');
2061
2091
  console.log(' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey');
2092
+ console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
2062
2093
  }
2063
2094
  if (name === 'node schema') {
2064
2095
  console.log('\nFilters:');
@@ -2112,6 +2143,7 @@ Node commands:
2112
2143
  pmx-canvas node get <id> Get a node by ID
2113
2144
  pmx-canvas node update <id> [opts] Update a node
2114
2145
  pmx-canvas node remove <id> Remove a node
2146
+ pmx-canvas graph add [options] Add a graph node
2115
2147
 
2116
2148
  Edge commands:
2117
2149
  pmx-canvas edge add [options] Add an edge between nodes
@@ -2179,6 +2211,7 @@ Examples:
2179
2211
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
2180
2212
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
2181
2213
  pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2214
+ pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2182
2215
  pmx-canvas node add --help --type webpage
2183
2216
  pmx-canvas node schema --type json-render
2184
2217
  pmx-canvas node schema --type json-render --component Table --summary
package/src/cli/index.ts CHANGED
@@ -31,7 +31,7 @@ if (args.includes('--version') || args.includes('-v')) {
31
31
  const AGENT_COMMANDS = new Set([
32
32
  'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
33
33
  'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
34
- 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'batch', 'validate', 'serve',
34
+ 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'batch', 'validate', 'serve',
35
35
  ]);
36
36
 
37
37
  const firstArg = args[0] ?? '';
@@ -486,6 +486,7 @@ Server options:
486
486
 
487
487
  Agent CLI (works against running server):
488
488
  node add|list|get|update|remove Manage nodes
489
+ graph add Add graph nodes (alias for node add --type graph)
489
490
  edge add|list|remove Manage edges
490
491
  webview status|start|evaluate|resize|screenshot|stop
491
492
  Manage Bun.WebView automation session
@@ -540,6 +541,7 @@ Examples:
540
541
  pmx-canvas node add --type webpage --url "https://example.com" Add a webpage node
541
542
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
542
543
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
544
+ pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
543
545
  pmx-canvas node list List all nodes
544
546
  pmx-canvas node schema --type json-render Show running-server schema info
545
547
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
@@ -262,6 +262,14 @@ function normalizeButtonVariant(value: unknown): unknown {
262
262
  return value;
263
263
  }
264
264
 
265
+ function normalizeBadgeVariant(value: unknown): unknown {
266
+ if (value === 'success') return 'default';
267
+ if (value === 'info') return 'secondary';
268
+ if (value === 'warning') return 'outline';
269
+ if (value === 'error' || value === 'danger') return 'destructive';
270
+ return value;
271
+ }
272
+
265
273
  function deriveElementName(elementKey: string): string {
266
274
  const normalized = elementKey.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
267
275
  return normalized || 'field';
@@ -325,6 +333,22 @@ function normalizeElementProps(
325
333
  props.text = props.content;
326
334
  }
327
335
 
336
+ if (type === 'Badge') {
337
+ if (!hasString(props.text) && hasString(props.label)) {
338
+ props.text = props.label;
339
+ }
340
+ // Drop the legacy alias once it's been migrated so the persisted spec
341
+ // contains exactly one of `text` or `label` rather than both. Keeps
342
+ // saved canvases tidy and prevents future stricter validators from
343
+ // flagging the old key as unknown.
344
+ if ('label' in props) {
345
+ delete props.label;
346
+ }
347
+ if ('variant' in props) {
348
+ props.variant = normalizeBadgeVariant(props.variant);
349
+ }
350
+ }
351
+
328
352
  if (type === 'Select' || type === 'Radio') {
329
353
  if (!Array.isArray(props.options) && Array.isArray(props.items)) {
330
354
  props.options = normalizeLabelArray(props.items);
package/src/mcp/server.ts CHANGED
@@ -47,6 +47,13 @@ const jsonRenderSpecSchema = z.object({
47
47
  state: z.record(z.string(), z.unknown()).optional(),
48
48
  }).passthrough();
49
49
 
50
+ function structuredSchemaDescription(): string {
51
+ const routing = describeCanvasSchema().mcp.nodeTypeRouting;
52
+ return Object.entries(routing)
53
+ .map(([type, tool]) => `${type}: ${tool}`)
54
+ .join(', ');
55
+ }
56
+
50
57
  function workspaceRoot(): string {
51
58
  return resolve(process.cwd());
52
59
  }
@@ -127,7 +134,7 @@ export async function startMcpServer(): Promise<void> {
127
134
  // ── canvas_add_node ────────────────────────────────────────────
128
135
  server.tool(
129
136
  'canvas_add_node',
130
- 'Add a node to the canvas. Returns the created node with normalized title/content and rendered geometry. Node types: markdown (rich content), status (compact indicator), context, ledger, trace, file (live file viewer set content to a file path), image (set content to an image file path, data URI, or URL), webpage (prefer url for the page URL; content is still accepted), mcp-app. Use canvas_add_json_render_node, canvas_add_graph_node, and canvas_build_web_artifact for structured UI, graph, and artifact nodes.',
137
+ 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, group. Dedicated node tools: json-render -> canvas_add_json_render_node, graph -> canvas_add_graph_node, web-artifact -> canvas_build_web_artifact, external apps -> canvas_open_mcp_app, groups -> canvas_create_group. Call canvas_describe_schema for the full nodeTypeRouting table.',
131
138
  {
132
139
  type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'group'])
133
140
  .describe('Node type (prefer canvas_create_group for groups)'),
@@ -171,7 +178,7 @@ export async function startMcpServer(): Promise<void> {
171
178
 
172
179
  server.tool(
173
180
  'canvas_open_mcp_app',
174
- 'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node.',
181
+ 'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use canvas_add_diagram for the built-in Excalidraw preset.',
175
182
  {
176
183
  toolName: z.string().describe('Tool name on the external MCP server'),
177
184
  serverName: z.string().optional().describe('Optional display name for the external MCP server'),
@@ -261,7 +268,7 @@ export async function startMcpServer(): Promise<void> {
261
268
 
262
269
  server.tool(
263
270
  'canvas_describe_schema',
264
- 'Describe the current server-supported canvas create schemas, json-render component catalog, canonical examples, and related MCP entry points.',
271
+ 'Describe the current server-supported canvas create schemas, json-render component catalog, canonical examples, and related MCP entry points. Includes mcp.nodeTypeRouting, the authoritative map from node type to MCP creation tool.',
265
272
  {},
266
273
  async () => ({
267
274
  content: [{ type: 'text', text: JSON.stringify(describeCanvasSchema(), null, 2) }],
@@ -356,7 +363,7 @@ export async function startMcpServer(): Promise<void> {
356
363
  // ── canvas_build_web_artifact ───────────────────────────────
357
364
  server.tool(
358
365
  'canvas_build_web_artifact',
359
- 'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
366
+ 'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds commonly take 45-60s on cold workspaces; use a long client timeout. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
360
367
  {
361
368
  title: z.string().describe('Artifact title used for default project and output paths'),
362
369
  appTsx: z.string().describe('Contents for src/App.tsx'),
@@ -407,6 +414,9 @@ export async function startMcpServer(): Promise<void> {
407
414
  bytes: result.fileSize,
408
415
  projectPath: result.projectPath,
409
416
  openedInCanvas: result.openedInCanvas,
417
+ // `id` only present when a canvas node was actually created.
418
+ // See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
419
+ ...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
410
420
  nodeId: result.nodeId,
411
421
  url: result.url,
412
422
  metadata: result.metadata,
@@ -1003,7 +1013,7 @@ export async function startMcpServer(): Promise<void> {
1003
1013
  'canvas://schema',
1004
1014
  {
1005
1015
  description:
1006
- 'Machine-readable create schemas, canonical examples, and json-render catalog details from the running PMX Canvas server version.',
1016
+ `Machine-readable create schemas, canonical examples, json-render catalog details, and MCP node-type routing from the running PMX Canvas server version. Routing: ${structuredSchemaDescription()}.`,
1007
1017
  mimeType: 'application/json',
1008
1018
  },
1009
1019
  async () => ({