pmx-canvas 0.2.6 → 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 (59) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +18 -18
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/mcp/canvas-access.d.ts +23 -7
  6. package/dist/types/server/ax-state-manager.d.ts +3 -2
  7. package/dist/types/server/bundled-skills.d.ts +3 -3
  8. package/dist/types/server/canvas-state.d.ts +2 -0
  9. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  10. package/dist/types/server/operations/composites.d.ts +29 -23
  11. package/dist/types/server/operations/http.d.ts +2 -1
  12. package/dist/types/shared/ax-intent.d.ts +1 -1
  13. package/docs/RELEASE.md +23 -8
  14. package/docs/ax-host-adapter-contract.md +7 -7
  15. package/docs/ax-state-contract.md +7 -6
  16. package/docs/bun-webview-integration.md +5 -5
  17. package/docs/cli.md +1 -1
  18. package/docs/http-api.md +7 -3
  19. package/docs/mcp.md +121 -100
  20. package/docs/node-types.md +45 -35
  21. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  22. package/docs/plans/plan-008-registry-finish.md +2 -0
  23. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  24. package/docs/screenshot.png +0 -0
  25. package/docs/tech-debt-assessment-2026-06.md +1 -1
  26. package/docs/tech-debt-assessment-2026-07.md +135 -0
  27. package/package.json +1 -1
  28. package/skills/data-analysis/SKILL.md +3 -3
  29. package/skills/frontend-design/SKILL.md +3 -2
  30. package/skills/json-render-mcp/SKILL.md +4 -3
  31. package/skills/playwright-cli/SKILL.md +1 -1
  32. package/skills/pmx-canvas/SKILL.md +20 -15
  33. package/skills/pmx-canvas/evals/evals.json +2 -2
  34. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  35. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  36. package/skills/pmx-canvas/references/full-reference.md +65 -62
  37. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  38. package/skills/tufte-viz/SKILL.md +5 -4
  39. package/skills/web-artifacts-builder/SKILL.md +4 -4
  40. package/src/client/App.tsx +2 -1
  41. package/src/mcp/canvas-access.ts +115 -107
  42. package/src/mcp/server.ts +74 -201
  43. package/src/server/ax-state-manager.ts +3 -2
  44. package/src/server/bundled-skills.ts +3 -3
  45. package/src/server/canvas-schema.ts +46 -29
  46. package/src/server/canvas-state.ts +5 -0
  47. package/src/server/code-graph.ts +1 -1
  48. package/src/server/ext-app-lookup.ts +1 -8
  49. package/src/server/index.ts +1 -1
  50. package/src/server/intent-registry.ts +1 -1
  51. package/src/server/operations/composites.ts +39 -39
  52. package/src/server/operations/http.ts +3 -2
  53. package/src/server/operations/mcp.ts +7 -6
  54. package/src/server/operations/ops/app.ts +4 -4
  55. package/src/server/operations/ops/nodes.ts +5 -5
  56. package/src/server/operations/ops/query.ts +19 -8
  57. package/src/server/operations/ops/snapshots.ts +6 -6
  58. package/src/server/server.ts +40 -4
  59. package/src/shared/ax-intent.ts +1 -1
package/src/mcp/server.ts CHANGED
@@ -20,14 +20,15 @@
20
20
  * ```
21
21
  */
22
22
 
23
+ import { readFileSync } from 'node:fs';
24
+ import { dirname, join } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
23
26
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
24
27
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
28
  import { z } from 'zod';
26
29
  import { canvasState, describeCanvasSchema } from '../server/index.js';
27
30
  import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
28
31
  import { buildPendingAxActivity } from '../server/ax-state.js';
29
- import { isHtmlPrimitiveKind } from '../server/html-primitives.js';
30
- import type { HtmlPrimitiveKind } from '../server/html-primitives.js';
31
32
  import { registerOperationTools, registerCompositeTools } from '../server/operations/index.js';
32
33
  import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
33
34
  import { serializeNodeForAgentContext } from '../server/agent-context.js';
@@ -36,7 +37,6 @@ import {
36
37
  getCanvasNodeTitle,
37
38
  serializeCanvasLayoutForAgent,
38
39
  serializeCanvasNode,
39
- serializeCanvasNodeForAgent,
40
40
  summarizeCanvasAnnotationForContext,
41
41
  } from '../server/canvas-serialization.js';
42
42
  import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js';
@@ -46,7 +46,18 @@ let resourceNotificationServer: McpServer | null = null;
46
46
  let localResourceNotificationsStarted = false;
47
47
  let remoteResourceNotificationsBaseUrl: string | null = null;
48
48
 
49
- const htmlPrimitiveKindSchema = z.string().refine(isHtmlPrimitiveKind, 'Unknown HTML primitive kind');
49
+ // Resolved from the sibling package.json so it stays accurate through bunx,
50
+ // global npm installs, and repo-local runs (no hard-coded string, no build
51
+ // step required) — same mechanism as --version in src/cli/index.ts.
52
+ function readPackageVersion(): string {
53
+ try {
54
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
55
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
56
+ return pkg.version ?? 'unknown';
57
+ } catch {
58
+ return 'unknown';
59
+ }
60
+ }
50
61
 
51
62
  function structuredSchemaDescription(): string {
52
63
  const routing = describeCanvasSchema().mcp.nodeTypeRouting;
@@ -55,10 +66,6 @@ function structuredSchemaDescription(): string {
55
66
  .join(', ');
56
67
  }
57
68
 
58
- // workspaceRoot / isPathInside / safeWorkspacePath removed with the
59
- // canvas_build_web_artifact MCP tool (plan-008 Wave 4). The webartifact.build op
60
- // sandboxes projectPath/outputPath via web-artifacts.ts resolveWorkspacePath.
61
-
62
69
  async function ensureCanvas(): Promise<CanvasAccess> {
63
70
  if (!canvas) {
64
71
  canvas = await createCanvasAccess();
@@ -215,19 +222,6 @@ function agentSafeFullLayoutPayload(layout: Awaited<ReturnType<CanvasAccess['get
215
222
  };
216
223
  }
217
224
 
218
- async function createdNodePayload(c: CanvasAccess, id: string, options: { full?: boolean; verbose?: boolean; includeData?: boolean } = {}): Promise<Record<string, unknown>> {
219
- // Expose both `id` and a `nodeId` alias on every node-create response so
220
- // agents using either key (or a cached schema) work — matching the
221
- // external-app / web-artifact responses that already return both.
222
- const node = await c.getNode(id);
223
- if (!node) return { ok: true, id, nodeId: id };
224
- if (!wantsFullPayload(options)) {
225
- return { ok: true, node: compactNodePayload(node), id, nodeId: id };
226
- }
227
- const serialized = serializeCanvasNodeForAgent(node);
228
- return { ok: true, node: serialized, ...serialized, nodeId: node.id };
229
- }
230
-
231
225
  function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>, pinnedIds: string[]): Record<string, unknown> {
232
226
  const pinned = new Set(pinnedIds);
233
227
  const nodesByType: Record<string, number> = {};
@@ -252,15 +246,19 @@ function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayo
252
246
  export async function startMcpServer(): Promise<void> {
253
247
  const server = new McpServer({
254
248
  name: 'pmx-canvas',
255
- version: '0.1.0',
249
+ version: readPackageVersion(),
256
250
  });
257
251
  resourceNotificationServer = server;
258
252
 
259
253
  // ── Operation-registry tools (plan-005) ────────────────────────
260
- // canvas_get_layout / canvas_get_node / canvas_add_node /
261
- // canvas_update_node / canvas_remove_node are registered from the shared
262
- // operation registry. Tool names and compact/full payload behavior are
263
- // frozen (tests/unit/mcp-tool-freeze.test.ts, operation-parity.test.ts).
254
+ // Standalone tools are registered from the shared operation registry for
255
+ // every op not folded into a composite (registerOperationTools skips
256
+ // composite-folded ops see compositeFoldedOpNames in composites.ts).
257
+ // canvas_get_layout / canvas_get_node / canvas_add_node / canvas_update_node /
258
+ // canvas_remove_node were folded into canvas_query / canvas_node and removed
259
+ // as standalone tools in v0.3.0; they remain reachable via their composite
260
+ // and canvas_batch. Remaining standalone tool names and compact/full payload
261
+ // behavior are frozen (tests/unit/mcp-tool-freeze.test.ts, operation-parity.test.ts).
264
262
  registerOperationTools(server, ensureCanvas);
265
263
 
266
264
  // ── Composite (action-discriminated) tools (plan-006) ───────────
@@ -268,151 +266,12 @@ export async function startMcpServer(): Promise<void> {
268
266
  // (canvas_node, canvas_render, canvas_edge, canvas_group, canvas_history,
269
267
  // canvas_view, canvas_query, plus the AX composites). Each action dispatches
270
268
  // to the same registered operation as its standalone tool, so behavior is
271
- // identical. Additive in v0.2 (legacy tools still registered below); legacy
272
- // removed in v0.3 per docs/api-stability.md. (canvas_snapshot composite is
273
- // deferred to v0.3 — its name is still held by the legacy save-snapshot tool.)
269
+ // identical. The legacy single-purpose tools these composites replace are
270
+ // removed as of v0.3.0 per docs/api-stability.md. (canvas_snapshot composite
271
+ // is deferred to v0.4 — its name is still held by the legacy save-snapshot
272
+ // tool; the 6 snapshot standalones are deprecated in v0.3.0 and fold into it.)
274
273
  registerCompositeTools(server, ensureCanvas);
275
274
 
276
- // ── canvas_add_html_node ────────────────────────────────────────
277
- server.tool(
278
- 'canvas_add_html_node',
279
- 'Deprecated: use canvas_node with action "add" and type:"html". Add a normal html node: a self-contained HTML document (with optional inline <script> and CDN <script src="...">) rendered inside a sandboxed iframe (sandbox="allow-scripts"). This is the default HTML surface for reports, widgets, and bespoke visualizations. Presentation mode is opt-in: only pass presentation:true when the user explicitly asks for a deck/fullscreen presentation, or use canvas_add_html_primitive with kind="presentation". The iframe inherits live canvas theme tokens via injected CSS custom properties (both --c-* and common --color-* aliases) so authored HTML using var(--color-text-secondary), var(--color-bg), etc. renders cohesively. No same-origin access; no top-navigation; no forms. For declarative-only views with zero JS, prefer canvas_add_json_render_node. For React + shadcn + routing or multi-component apps, use canvas_build_web_artifact.',
280
- {
281
- html: z.string().describe('HTML document or fragment. Full <html>...</html> documents are passed through with theme styles injected into <head>; bare fragments are wrapped in a minimal document. Inline <script> and remote CDN <script src="..."> are allowed. If this is a bare path to an existing local .html/.htm file, the file contents are read and used as the HTML.'),
282
- title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
283
- summary: z.string().optional().describe('Agent-readable semantic summary for this HTML node. If omitted, PMX derives one from visible HTML text.'),
284
- agentSummary: z.string().optional().describe('Explicit agent-readable summary. Alias for summary with higher priority when both are provided.'),
285
- description: z.string().optional().describe('Short description included in search and pinned/spatial context.'),
286
- presentation: z.boolean().optional().describe('Marks this HTML surface as a fullscreen presentation/deck. Omit unless the user explicitly requested presentation mode.'),
287
- slideTitles: z.array(z.string()).optional().describe('Agent-readable slide titles for presentation HTML.'),
288
- embeddedNodeIds: z.array(z.string()).optional().describe('Canvas node IDs embedded or represented by this HTML surface.'),
289
- embeddedUrls: z.array(z.string()).optional().describe('URLs embedded or represented by this HTML surface.'),
290
- x: z.number().optional().describe('X position (auto-placed if omitted).'),
291
- y: z.number().optional().describe('Y position (auto-placed if omitted).'),
292
- width: z.number().optional().describe('Width in pixels (default: 720).'),
293
- height: z.number().optional().describe('Height in pixels (default: 640).'),
294
- strictSize: z.boolean().optional().describe('Keep explicit width/height fixed; iframe scrolls overflow internally.'),
295
- axCapabilities: z.object({
296
- enabled: z.boolean().optional(),
297
- allowed: z.array(z.string()).optional().describe('AX interaction types this node may emit (e.g. ax.work.create, ax.work.update, ax.steer, ax.focus.set, ax.evidence.add, ax.event.record). Clamped to the html capability ceiling server-side; cannot escalate.'),
298
- }).optional().describe('Opt this html node into AX interactions so its sandboxed UI can emit ax.* via window.PMX_AX.emit(type, payload) (and reflect live AX state). html nodes are AX-disabled by default; set { enabled: true, allowed: [...] } to turn the bridge on. Build interactive boards (work queues, review boards, inboxes) this way.'),
299
- full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
300
- verbose: z.boolean().optional().describe('Alias for full:true.'),
301
- },
302
- async (input) => {
303
- const c = await ensureCanvas();
304
- const id = await c.addHtmlNode({
305
- html: input.html,
306
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
307
- ...(input.axCapabilities ? { axCapabilities: input.axCapabilities } : {}),
308
- ...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
309
- ...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
310
- ...(typeof input.description === 'string' ? { description: input.description } : {}),
311
- ...(input.presentation === true ? { presentation: true } : {}),
312
- ...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
313
- ...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
314
- ...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
315
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
316
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
317
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
318
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
319
- ...(input.strictSize === true ? { strictSize: true } : {}),
320
- });
321
- return {
322
- content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id, input), null, 2) }],
323
- };
324
- },
325
- );
326
-
327
- server.tool(
328
- 'canvas_add_html_primitive',
329
- 'Deprecated: use canvas_node with action "add", type:"html", primitive:"<kind>" (and data). Create a reusable HTML communication primitive as a normal sandboxed html node. Use this instead of long markdown for side-by-side choices, implementation plans, PR review sheets, module maps, design sheets, component galleries, flowcharts, explainers, status reports, and throwaway editors with export/copy paths. Use kind="presentation" only when the user explicitly asks for a PowerPoint-like deck, pitch, briefing, workshop walkthrough, or fullscreen story.',
330
- {
331
- kind: htmlPrimitiveKindSchema.describe('Primitive kind. Call canvas_describe_schema and read htmlPrimitives for data shapes and examples.'),
332
- title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
333
- data: z.record(z.string(), z.unknown()).optional().describe('Primitive-specific data payload. For kind="presentation", data may include theme:"canvas"|"midnight"|"paper"|"aurora" or a custom color object. See canvas_describe_schema.htmlPrimitives for each shape.'),
334
- x: z.number().optional().describe('X position (auto-placed if omitted).'),
335
- y: z.number().optional().describe('Y position (auto-placed if omitted).'),
336
- width: z.number().optional().describe('Width in pixels (defaults per primitive).'),
337
- height: z.number().optional().describe('Height in pixels (defaults per primitive).'),
338
- strictSize: z.boolean().optional().describe('Keep explicit width/height fixed; iframe scrolls overflow internally.'),
339
- full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
340
- verbose: z.boolean().optional().describe('Alias for full:true.'),
341
- },
342
- async (input) => {
343
- const c = await ensureCanvas();
344
- const kind = input.kind as HtmlPrimitiveKind;
345
- const result = await c.addHtmlPrimitive({
346
- kind,
347
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
348
- ...(input.data ? { data: input.data } : {}),
349
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
350
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
351
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
352
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
353
- ...(input.strictSize === true ? { strictSize: true } : {}),
354
- });
355
- return {
356
- content: [{
357
- type: 'text',
358
- text: JSON.stringify({
359
- ...(await createdNodePayload(c, result.id, input)),
360
- primitive: { kind: result.kind, title: result.title, htmlBytes: result.htmlBytes },
361
- }, null, 2),
362
- }],
363
- };
364
- },
365
- );
366
-
367
- // canvas_open_mcp_app + canvas_add_diagram migrated to the operation registry
368
- // (plan-008 Wave 4): src/server/operations/ops/app.ts (mcpapp.open /
369
- // diagram.open). Folded into the canvas_app composite.
370
-
371
- server.tool(
372
- 'canvas_refresh_webpage_node',
373
- 'Deprecated: use canvas_node with action "update" and refresh:true. Refresh a webpage node from its persisted URL so the server re-fetches and caches the latest page text and metadata.',
374
- {
375
- id: z.string().describe('Webpage node ID to refresh'),
376
- url: z.string().optional().describe('Optional replacement URL before refresh'),
377
- },
378
- async ({ id, url }) => {
379
- const c = await ensureCanvas();
380
- const result = await c.refreshWebpageNode(id, url);
381
- return {
382
- content: [{ type: 'text', text: JSON.stringify(result) }],
383
- ...(result.ok ? {} : { isError: true }),
384
- };
385
- },
386
- );
387
-
388
- // canvas_build_web_artifact migrated to the operation registry (plan-008
389
- // Wave 4): src/server/operations/ops/app.ts (webartifact.build). Folded into
390
- // the canvas_app composite.
391
-
392
- // canvas_remove_annotation migrated to the operation registry (plan-008
393
- // Wave 1): src/server/operations/ops/annotation.ts.
394
-
395
- // ── AX context and focus ───────────────────────────────────────
396
- // canvas_get_ax + canvas_set_ax_focus migrated to the operation registry
397
- // (plan-007 Slice B.1): src/server/operations/ops/ax-state.ts.
398
-
399
- // canvas_record_ax_event / canvas_send_steering / canvas_get_ax_timeline
400
- // migrated to the operation registry (plan-007 Slice B wave 3):
401
- // src/server/operations/ops/ax-timeline.ts.
402
-
403
- // canvas_add_work_item / canvas_update_work_item / canvas_request_approval /
404
- // canvas_resolve_approval migrated to the operation registry (plan-007 Slice B
405
- // wave 2): src/server/operations/ops/ax-work.ts.
406
-
407
- // canvas_add_evidence migrated to the operation registry (plan-007 Slice B
408
- // wave 3): src/server/operations/ops/ax-timeline.ts.
409
-
410
- // canvas_add_review_annotation migrated to the operation registry (plan-007
411
- // Slice B wave 2): src/server/operations/ops/ax-work.ts.
412
-
413
- // canvas_report_host_capability migrated to the operation registry
414
- // (plan-007 Slice B.1): src/server/operations/ops/ax-state.ts.
415
-
416
275
  server.tool(
417
276
  'canvas_ax_interaction',
418
277
  'Submit a node-originated AX interaction: a capability-gated, validated event from an eligible node that maps onto an AX operation (work item, evidence, approval, review, focus, steering, event). Returns { ok: false, code } if the node type/metadata does not allow the interaction type or the payload is invalid.',
@@ -427,18 +286,25 @@ export async function startMcpServer(): Promise<void> {
427
286
  .describe('Optional host/source label. Defaults to mcp.'),
428
287
  },
429
288
  async ({ type, sourceNodeId, payload, sourceSurface, correlationId, source }) => {
430
- const c = await ensureCanvas();
431
- const result = await c.submitAxInteraction(
432
- {
433
- type,
434
- sourceNodeId,
435
- ...(payload ? { payload } : {}),
436
- ...(sourceSurface ? { sourceSurface } : {}),
437
- ...(correlationId ? { correlationId } : {}),
438
- },
439
- { source: source ?? 'mcp' },
440
- );
441
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
289
+ try {
290
+ const c = await ensureCanvas();
291
+ const result = await c.submitAxInteraction(
292
+ {
293
+ type,
294
+ sourceNodeId,
295
+ ...(payload ? { payload } : {}),
296
+ ...(sourceSurface ? { sourceSurface } : {}),
297
+ ...(correlationId ? { correlationId } : {}),
298
+ },
299
+ { source: source ?? 'mcp' },
300
+ );
301
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
302
+ } catch (error) {
303
+ return {
304
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
305
+ isError: true,
306
+ };
307
+ }
442
308
  },
443
309
  );
444
310
 
@@ -479,21 +345,28 @@ export async function startMcpServer(): Promise<void> {
479
345
  source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
480
346
  },
481
347
  async ({ kind, title, summary, outcome, ref, nodeIds, data, reactions, source }) => {
482
- const c = await ensureCanvas();
483
- const result = await c.ingestActivity(
484
- {
485
- kind,
486
- title,
487
- ...(summary !== undefined ? { summary } : {}),
488
- ...(outcome !== undefined ? { outcome } : {}),
489
- ...(ref !== undefined ? { ref } : {}),
490
- ...(nodeIds !== undefined ? { nodeIds } : {}),
491
- ...(data !== undefined ? { data } : {}),
492
- ...(reactions !== undefined ? { reactions } : {}),
493
- },
494
- { source: source ?? 'mcp' },
495
- );
496
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, ...result }) }] };
348
+ try {
349
+ const c = await ensureCanvas();
350
+ const result = await c.ingestActivity(
351
+ {
352
+ kind,
353
+ title,
354
+ ...(summary !== undefined ? { summary } : {}),
355
+ ...(outcome !== undefined ? { outcome } : {}),
356
+ ...(ref !== undefined ? { ref } : {}),
357
+ ...(nodeIds !== undefined ? { nodeIds } : {}),
358
+ ...(data !== undefined ? { data } : {}),
359
+ ...(reactions !== undefined ? { reactions } : {}),
360
+ },
361
+ { source: source ?? 'mcp' },
362
+ );
363
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, ...result }) }] };
364
+ } catch (error) {
365
+ return {
366
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
367
+ isError: true,
368
+ };
369
+ }
497
370
  },
498
371
  );
499
372
 
@@ -516,7 +389,7 @@ export async function startMcpServer(): Promise<void> {
516
389
  // ── canvas_screenshot ─────────────────────────────────────────
517
390
  server.tool(
518
391
  'canvas_screenshot',
519
- 'Capture a screenshot from the active Bun.WebView automation session. Returns both an MCP image payload and JSON metadata. Requires an active automation session started via canvas_webview_start.',
392
+ 'Capture a screenshot from the active Bun.WebView automation session. Returns both an MCP image payload and JSON metadata. Requires an active automation session started via canvas_webview with action "start".',
520
393
  {
521
394
  format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Screenshot format (default depends on Bun; png recommended)'),
522
395
  quality: z.number().optional().describe('Optional quality for lossy formats'),
@@ -752,7 +625,7 @@ export async function startMcpServer(): Promise<void> {
752
625
  'canvas://ax-pending-steering',
753
626
  {
754
627
  description:
755
- 'Adapterless AX delivery surface. `pending`: undelivered steering messages to claim and act on, then mark via canvas_mark_ax_delivery. `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — usually created by the human in the browser; these fire ax-state-changed (resource-subscribers are also pushed canvas://ax-work). Resolve pendingActivity via its own tool, not canvas_mark_ax_delivery. Use canvas_claim_ax_delivery for the loop-safe, consumer-scoped view.',
628
+ 'Adapterless AX delivery surface. `pending`: undelivered steering messages to claim and act on, then mark via canvas_ax_delivery { action: "mark" }. `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — usually created by the human in the browser; these fire ax-state-changed (resource-subscribers are also pushed canvas://ax-work). Resolve pendingActivity via its own tool, not the delivery mark action. Use canvas_ax_delivery { action: "claim" } for the loop-safe, consumer-scoped view.',
756
629
  mimeType: 'application/json',
757
630
  },
758
631
  async () => {
@@ -931,8 +804,8 @@ export async function startMcpServer(): Promise<void> {
931
804
  // ── canvas://skills ────────────────────────────────────────
932
805
  // Discoverability for the skill prompts bundled with the npm package
933
806
  // (skills/<name>/SKILL.md). Before 0.1.2 these files shipped but were
934
- // invisible to agents — calling canvas_build_web_artifact without the
935
- // companion `web-artifacts-builder` skill led to predictable misuse.
807
+ // invisible to agents — building web artifacts without the companion
808
+ // `web-artifacts-builder` skill led to predictable misuse.
936
809
  // The index lists every bundled skill with its frontmatter description;
937
810
  // individual skills are served verbatim at canvas://skills/<name>.
938
811
  server.resource(
@@ -943,7 +816,7 @@ export async function startMcpServer(): Promise<void> {
943
816
  'Index of agent skills bundled with this PMX Canvas install. Lists name, ' +
944
817
  'description, and per-skill URI (canvas://skills/<name>). Read a specific ' +
945
818
  'skill for workflow guidance — notably web-artifacts-builder for ' +
946
- 'canvas_build_web_artifact, and pmx-canvas for the broader workbench.',
819
+ 'canvas_app { action: "build-artifact" }, and pmx-canvas for the broader workbench.',
947
820
  mimeType: 'application/json',
948
821
  },
949
822
  async () => {
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * • Canvas-bound (`_axState`): focus, work items, approval gates, review
9
9
  * annotations, elicitations, mode requests, policy. Snapshotted, cleared by
10
- * `canvas_clear`, replaced by `restore`. Mutators record undo/redo history
11
- * (via the injected `recordMutation` / `suppressed` callbacks).
10
+ * `canvas_view` (`clear` action), replaced by `restore`. Mutators record
11
+ * undo/redo history (via the injected `recordMutation` / `suppressed`
12
+ * callbacks).
12
13
  * • Timeline (audit-only): agent events, evidence, steering. DB-direct, NOT in
13
14
  * `_axState`, NOT history-recorded, NOT snapshotted, retention-bounded.
14
15
  * • Host/session: a single host-capability row in its own table.
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Skill files ship inside the npm package under `skills/<name>/SKILL.md`
5
5
  * but until 0.1.2 they were not discoverable to the agent — an agent
6
- * calling `canvas_build_web_artifact` had no way to find the companion
7
- * `skills/web-artifacts-builder/SKILL.md` prompt that documents the
8
- * workflow, stack choices, and gotchas.
6
+ * calling `canvas_app` (`build-artifact` action) had no way to find the
7
+ * companion `skills/web-artifacts-builder/SKILL.md` prompt that documents
8
+ * the workflow, stack choices, and gotchas.
9
9
  *
10
10
  * This module locates the bundled `skills/` directory relative to the
11
11
  * package root (works for both repo-local development and global npm
@@ -80,7 +80,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
80
80
  kind: 'node',
81
81
  description: 'Freeform markdown note.',
82
82
  endpoint: '/api/canvas/node',
83
- mcpTool: 'canvas_add_node',
83
+ mcpTool: 'canvas_node (action:"add")',
84
84
  fields: [
85
85
  { name: 'title', type: 'string', required: false, description: 'Optional node title.' },
86
86
  { name: 'content', type: 'string', required: false, description: 'Markdown body.' },
@@ -104,7 +104,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
104
104
  kind: 'node',
105
105
  description: 'Compact status indicator.',
106
106
  endpoint: '/api/canvas/node',
107
- mcpTool: 'canvas_add_node',
107
+ mcpTool: 'canvas_node (action:"add")',
108
108
  fields: [
109
109
  { name: 'title', type: 'string', required: false, description: 'Status label.' },
110
110
  { name: 'content', type: 'string', required: false, description: 'Rendered status text.' },
@@ -120,7 +120,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
120
120
  kind: 'node',
121
121
  description: 'Agent context card container.',
122
122
  endpoint: '/api/canvas/node',
123
- mcpTool: 'canvas_add_node',
123
+ mcpTool: 'canvas_node (action:"add")',
124
124
  fields: [
125
125
  { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
126
126
  { name: 'content', type: 'string', required: false, description: 'Optional context body.' },
@@ -136,7 +136,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
136
136
  kind: 'node',
137
137
  description: 'Structured ledger/log node.',
138
138
  endpoint: '/api/canvas/node',
139
- mcpTool: 'canvas_add_node',
139
+ mcpTool: 'canvas_node (action:"add")',
140
140
  fields: [
141
141
  { name: 'title', type: 'string', required: false, description: 'Optional title.' },
142
142
  { name: 'content', type: 'string', required: false, description: 'Ledger body text.' },
@@ -152,7 +152,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
152
152
  kind: 'node',
153
153
  description: 'Execution trace viewer.',
154
154
  endpoint: '/api/canvas/node',
155
- mcpTool: 'canvas_add_node',
155
+ mcpTool: 'canvas_node (action:"add")',
156
156
  fields: [
157
157
  { name: 'title', type: 'string', required: false, description: 'Optional title.' },
158
158
  { name: 'content', type: 'string', required: false, description: 'Trace summary.' },
@@ -175,7 +175,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
175
175
  kind: 'node',
176
176
  description: 'Workspace file viewer.',
177
177
  endpoint: '/api/canvas/node',
178
- mcpTool: 'canvas_add_node',
178
+ mcpTool: 'canvas_node (action:"add")',
179
179
  fields: [
180
180
  { name: 'content', type: 'string', required: true, description: 'Workspace-relative or absolute file path.' },
181
181
  { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
@@ -193,7 +193,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
193
193
  kind: 'node',
194
194
  description: 'Image node backed by a path, URL, or data URI.',
195
195
  endpoint: '/api/canvas/node',
196
- mcpTool: 'canvas_add_node',
196
+ mcpTool: 'canvas_node (action:"add")',
197
197
  fields: [
198
198
  { name: 'content', type: 'string', required: true, description: 'Image path, URL, or data URI.', aliases: ['path'] },
199
199
  { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
@@ -219,7 +219,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
219
219
  kind: 'node',
220
220
  description: 'Persisted webpage snapshot with server-side fetch and refresh.',
221
221
  endpoint: '/api/canvas/node',
222
- mcpTool: 'canvas_add_node',
222
+ mcpTool: 'canvas_node (action:"add")',
223
223
  fields: [
224
224
  { name: 'url', type: 'string', required: true, description: 'HTTP(S) URL to fetch and cache.', aliases: ['content'] },
225
225
  { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
@@ -314,7 +314,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
314
314
  kind: 'node',
315
315
  description: 'Hosted iframe/app node.',
316
316
  endpoint: '/api/canvas/node',
317
- mcpTool: 'canvas_open_mcp_app',
317
+ mcpTool: 'canvas_app (action:"open-mcp-app")',
318
318
  fields: [
319
319
  { name: 'title', type: 'string', required: false, description: 'App title.' },
320
320
  { name: 'content', type: 'string', required: false, description: 'Optional inline content.' },
@@ -333,7 +333,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
333
333
  kind: 'virtual-node',
334
334
  description: 'Tool-backed hosted app opened from an external MCP server, such as Excalidraw.',
335
335
  endpoint: '/api/canvas/mcp-app/open',
336
- mcpTool: 'canvas_open_mcp_app',
336
+ mcpTool: 'canvas_app (action:"open-mcp-app")',
337
337
  fields: [
338
338
  { name: 'toolName', type: 'string', required: true, description: 'Tool name on the external MCP server.' },
339
339
  { name: 'transport', type: '{ type: "stdio", command, args? } | { type: "http", url, headers? }', required: true, description: 'External MCP transport definition.' },
@@ -347,8 +347,8 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
347
347
  title: 'Excalidraw Diagram',
348
348
  },
349
349
  notes: [
350
- 'For Excalidraw specifically, prefer canvas_add_diagram because it fills in the built-in transport, toolName, and checkpoint wiring.',
351
- 'The CLI convenience command `external-app add --kind excalidraw` maps to this built-in Excalidraw preset; MCP canvas_open_mcp_app is the lower-level transport form.',
350
+ 'For Excalidraw specifically, prefer canvas_app (action:"diagram") because it fills in the built-in transport, toolName, and checkpoint wiring.',
351
+ 'The CLI convenience command `external-app add --kind excalidraw` maps to this built-in Excalidraw preset; MCP canvas_app (action:"open-mcp-app") is the lower-level transport form.',
352
352
  ],
353
353
  },
354
354
  {
@@ -356,7 +356,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
356
356
  kind: 'node',
357
357
  description: 'Canvas group frame.',
358
358
  endpoint: '/api/canvas/group',
359
- mcpTool: 'canvas_create_group',
359
+ mcpTool: 'canvas_group (action:"create")',
360
360
  fields: [
361
361
  { name: 'title', type: 'string', required: false, description: 'Group title.' },
362
362
  { name: 'childIds', type: 'string[]', required: false, description: 'Initial child node IDs.' },
@@ -373,7 +373,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
373
373
  kind: 'virtual-node',
374
374
  description: 'Native structured UI panel rendered from a validated json-render spec.',
375
375
  endpoint: '/api/canvas/json-render',
376
- mcpTool: 'canvas_add_json_render_node',
376
+ mcpTool: 'canvas_render (action:"add-json-render")',
377
377
  fields: [
378
378
  { name: 'title', type: 'string', required: false, description: 'Optional rendered node title; inferred from the root element when omitted.' },
379
379
  { name: 'spec', type: 'JsonRenderSpec | JsonRenderElement', required: true, description: 'Complete {root, elements} json-render spec, or a legacy single bare component object with a type field.' },
@@ -406,7 +406,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
406
406
  kind: 'virtual-node',
407
407
  description: 'Native chart node backed by the json-render chart catalog.',
408
408
  endpoint: '/api/canvas/graph',
409
- mcpTool: 'canvas_add_graph_node',
409
+ mcpTool: 'canvas_render (action:"add-graph")',
410
410
  fields: [
411
411
  {
412
412
  name: 'graphType',
@@ -467,7 +467,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
467
467
  kind: 'virtual-node',
468
468
  description: 'Bundled single-file HTML artifact that can open as an embedded canvas node.',
469
469
  endpoint: '/api/canvas/web-artifact',
470
- mcpTool: 'canvas_build_web_artifact',
470
+ mcpTool: 'canvas_app (action:"build-artifact")',
471
471
  fields: [
472
472
  { name: 'title', type: 'string', required: true, description: 'Artifact title used for default paths.' },
473
473
  { name: 'appTsx', type: 'string', required: true, description: 'Contents for src/App.tsx. The CLI also accepts piped contents via --stdin.', aliases: ['app-file', 'app-tsx'] },
@@ -556,19 +556,36 @@ export function describeCanvasSchema(): {
556
556
  htmlPrimitives: listHtmlPrimitiveDescriptors(),
557
557
  mcp: {
558
558
  tools: [
559
- 'canvas_add_node',
560
- 'canvas_add_html_node',
561
- 'canvas_add_html_primitive',
562
- 'canvas_add_json_render_node',
563
- 'canvas_stream_json_render_node',
564
- 'canvas_add_graph_node',
565
- 'canvas_build_web_artifact',
566
- 'canvas_open_mcp_app',
567
- 'canvas_create_group',
568
- 'canvas_update_node',
569
- 'canvas_fit_view',
570
- 'canvas_describe_schema',
571
- 'canvas_validate_spec',
559
+ // 15 composites
560
+ 'canvas_node',
561
+ 'canvas_render',
562
+ 'canvas_edge',
563
+ 'canvas_group',
564
+ 'canvas_history',
565
+ 'canvas_view',
566
+ 'canvas_query',
567
+ 'canvas_webview',
568
+ 'canvas_app',
569
+ 'canvas_ax_state',
570
+ 'canvas_ax_work',
571
+ 'canvas_ax_gate',
572
+ 'canvas_ax_timeline',
573
+ 'canvas_ax_delivery',
574
+ 'canvas_intent',
575
+ // 6 standalones
576
+ 'canvas_batch',
577
+ 'canvas_pin_nodes',
578
+ 'canvas_invoke_command',
579
+ 'canvas_ax_interaction',
580
+ 'canvas_ingest_activity',
581
+ 'canvas_screenshot',
582
+ // 6 snapshot tools (deprecated; fold into canvas_snapshot composite in v0.4)
583
+ 'canvas_snapshot',
584
+ 'canvas_list_snapshots',
585
+ 'canvas_restore',
586
+ 'canvas_delete_snapshot',
587
+ 'canvas_gc_snapshots',
588
+ 'canvas_diff',
572
589
  ],
573
590
  resources: ['canvas://schema'],
574
591
  nodeTypeRouting: buildMcpNodeTypeRouting(nodeTypes),
@@ -571,6 +571,11 @@ class CanvasStateManager {
571
571
  private _db: import('bun:sqlite').Database | null = null;
572
572
  private _saveTimer: ReturnType<typeof setTimeout> | null = null;
573
573
 
574
+ /** Workspace root backing persistence and workspace-relative path resolution. */
575
+ get workspaceRoot(): string {
576
+ return this._workspaceRoot;
577
+ }
578
+
574
579
  /** Set the workspace root to enable auto-persistence. */
575
580
  setWorkspaceRoot(workspaceRoot: string): void {
576
581
  this.close();
@@ -113,7 +113,7 @@ function resolveImportPath(specifier: string, fromFilePath: string): string | nu
113
113
  // Check if it looks like a relative project path (contains / and no @)
114
114
  if (!specifier.includes('/') || specifier.startsWith('@')) return null;
115
115
  // For non-relative paths, try workspace-relative resolution
116
- const wsRoot = process.cwd();
116
+ const wsRoot = canvasState.workspaceRoot;
117
117
  return tryResolveWithExtensions(resolve(wsRoot, specifier));
118
118
  }
119
119
 
@@ -9,9 +9,7 @@
9
9
  * Resolution order:
10
10
  * 1. The direct prefixed form (`ext-app-<toolCallId>` if not already
11
11
  * prefixed, otherwise `toolCallId` as-is).
12
- * 2. The legacy `ext-app-ext-app-…` form, for canvases persisted before
13
- * v0.1.4 and still on disk. Remove this fallback in v0.2.x.
14
- * 3. A scan of the layout for any `mcp-app` ext-app node carrying that
12
+ * 2. A scan of the layout for any `mcp-app` ext-app node carrying that
15
13
  * `toolCallId` in its data.
16
14
  */
17
15
  import type { CanvasNodeState } from './canvas-state.js';
@@ -30,11 +28,6 @@ export function findCanvasExtAppNodeId(
30
28
  : `ext-app-${toolCallId}`;
31
29
  if (source.getNode(directId)) return directId;
32
30
 
33
- const legacyDirectId = `ext-app-${toolCallId}`;
34
- if (legacyDirectId !== directId && source.getNode(legacyDirectId)) {
35
- return legacyDirectId;
36
- }
37
-
38
31
  for (const node of source.listNodes()) {
39
32
  if (
40
33
  node.type === 'mcp-app' &&