pmx-canvas 0.2.7 → 0.3.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 (189) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/cli/daemon.d.ts +74 -0
  6. package/dist/types/cli/watch.d.ts +2 -2
  7. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  8. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  9. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/mcp/canvas-access.d.ts +0 -7
  17. package/dist/types/server/ax-state-manager.d.ts +3 -2
  18. package/dist/types/server/bundled-skills.d.ts +3 -3
  19. package/dist/types/server/canvas-schema.d.ts +1 -1
  20. package/dist/types/server/canvas-state.d.ts +2 -0
  21. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  22. package/dist/types/server/html-primitives.d.ts +1 -1
  23. package/dist/types/server/index.d.ts +4 -4
  24. package/dist/types/server/operations/composites.d.ts +29 -23
  25. package/dist/types/server/operations/http.d.ts +2 -1
  26. package/dist/types/server/operations/index.d.ts +1 -1
  27. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  28. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  29. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  30. package/dist/types/shared/ax-intent.d.ts +1 -1
  31. package/docs/RELEASE.md +23 -8
  32. package/docs/ax-host-adapter-contract.md +7 -7
  33. package/docs/ax-state-contract.md +7 -6
  34. package/docs/bun-webview-integration.md +5 -5
  35. package/docs/cli.md +1 -1
  36. package/docs/http-api.md +35 -3
  37. package/docs/mcp.md +121 -100
  38. package/docs/node-types.md +45 -35
  39. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  40. package/docs/plans/plan-008-registry-finish.md +2 -0
  41. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  42. package/docs/screenshot.png +0 -0
  43. package/docs/tech-debt-assessment-2026-06.md +1 -1
  44. package/docs/tech-debt-assessment-2026-07.md +135 -0
  45. package/package.json +5 -2
  46. package/skills/data-analysis/SKILL.md +3 -3
  47. package/skills/frontend-design/SKILL.md +3 -2
  48. package/skills/json-render-mcp/SKILL.md +4 -3
  49. package/skills/playwright-cli/SKILL.md +1 -1
  50. package/skills/pmx-canvas/SKILL.md +12 -7
  51. package/skills/pmx-canvas/evals/evals.json +2 -2
  52. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  53. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  54. package/skills/pmx-canvas/references/full-reference.md +58 -56
  55. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  56. package/skills/tufte-viz/SKILL.md +5 -4
  57. package/skills/web-artifacts-builder/SKILL.md +4 -4
  58. package/src/cli/agent.ts +1861 -1548
  59. package/src/cli/daemon.ts +460 -0
  60. package/src/cli/index.ts +63 -326
  61. package/src/cli/watch.ts +2 -10
  62. package/src/client/App.tsx +48 -46
  63. package/src/client/canvas/AttentionHistory.tsx +11 -1
  64. package/src/client/canvas/CanvasNode.tsx +41 -29
  65. package/src/client/canvas/CanvasViewport.tsx +101 -66
  66. package/src/client/canvas/CommandPalette.tsx +61 -27
  67. package/src/client/canvas/ContextMenu.tsx +13 -20
  68. package/src/client/canvas/ContextPinBar.tsx +1 -5
  69. package/src/client/canvas/ContextPinHud.tsx +1 -6
  70. package/src/client/canvas/DockedNode.tsx +4 -4
  71. package/src/client/canvas/EdgeLayer.tsx +37 -36
  72. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  73. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  74. package/src/client/canvas/IntentLayer.tsx +31 -14
  75. package/src/client/canvas/Minimap.tsx +11 -16
  76. package/src/client/canvas/SelectionBar.tsx +4 -11
  77. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  78. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  79. package/src/client/canvas/auto-fit.ts +15 -14
  80. package/src/client/canvas/snap-guides.ts +12 -12
  81. package/src/client/canvas/use-node-resize.ts +1 -5
  82. package/src/client/canvas/use-pan-zoom.ts +25 -26
  83. package/src/client/ext-app/bridge.ts +3 -12
  84. package/src/client/icons.tsx +63 -20
  85. package/src/client/nodes/ContextNode.tsx +14 -25
  86. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  87. package/src/client/nodes/FileNode.tsx +74 -62
  88. package/src/client/nodes/GroupNode.tsx +4 -6
  89. package/src/client/nodes/HtmlNode.tsx +76 -46
  90. package/src/client/nodes/ImageNode.tsx +18 -27
  91. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  92. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  93. package/src/client/nodes/LedgerNode.tsx +10 -4
  94. package/src/client/nodes/MarkdownNode.tsx +3 -10
  95. package/src/client/nodes/McpAppNode.tsx +26 -22
  96. package/src/client/nodes/MdFormatBar.tsx +10 -7
  97. package/src/client/nodes/PromptNode.tsx +23 -51
  98. package/src/client/nodes/ResponseNode.tsx +3 -13
  99. package/src/client/nodes/StatusNode.tsx +5 -9
  100. package/src/client/nodes/StatusSummary.tsx +2 -8
  101. package/src/client/nodes/WebpageNode.tsx +20 -14
  102. package/src/client/nodes/iframe-document-url.ts +25 -16
  103. package/src/client/nodes/image-warnings.ts +1 -7
  104. package/src/client/nodes/md-format.ts +20 -5
  105. package/src/client/state/attention-bridge.ts +4 -9
  106. package/src/client/state/attention-store.ts +1 -7
  107. package/src/client/state/canvas-store.ts +52 -36
  108. package/src/client/state/intent-bridge.ts +176 -112
  109. package/src/client/state/intent-store.ts +4 -1
  110. package/src/client/state/sse-bridge.ts +53 -70
  111. package/src/json-render/catalog.ts +12 -16
  112. package/src/json-render/charts/components.tsx +16 -20
  113. package/src/json-render/charts/extra-components.tsx +8 -16
  114. package/src/json-render/charts/extra-definitions.ts +1 -2
  115. package/src/json-render/charts/tufte-components.tsx +37 -20
  116. package/src/json-render/charts/tufte-definitions.ts +8 -2
  117. package/src/json-render/renderer/index.tsx +42 -22
  118. package/src/json-render/schema.ts +6 -3
  119. package/src/json-render/server.ts +33 -39
  120. package/src/mcp/canvas-access.ts +40 -121
  121. package/src/mcp/server.ts +206 -271
  122. package/src/server/agent-context.ts +63 -36
  123. package/src/server/ax-context.ts +7 -5
  124. package/src/server/ax-interaction.ts +176 -43
  125. package/src/server/ax-state-manager.ts +185 -41
  126. package/src/server/ax-state.ts +142 -47
  127. package/src/server/bundled-skills.ts +3 -3
  128. package/src/server/canvas-db.ts +213 -95
  129. package/src/server/canvas-operations.ts +177 -120
  130. package/src/server/canvas-provenance.ts +1 -4
  131. package/src/server/canvas-schema.ts +500 -102
  132. package/src/server/canvas-serialization.ts +27 -35
  133. package/src/server/canvas-state.ts +155 -58
  134. package/src/server/chart-template.ts +4 -5
  135. package/src/server/code-graph.ts +20 -7
  136. package/src/server/diagram-presets.ts +28 -29
  137. package/src/server/ext-app-lookup.ts +4 -20
  138. package/src/server/html-node-summary.ts +19 -10
  139. package/src/server/html-primitives.ts +326 -97
  140. package/src/server/html-surface.ts +6 -9
  141. package/src/server/image-source.ts +6 -4
  142. package/src/server/index.ts +321 -218
  143. package/src/server/intent-registry.ts +3 -6
  144. package/src/server/mcp-app-candidate.ts +5 -10
  145. package/src/server/mcp-app-host.ts +14 -38
  146. package/src/server/mcp-app-runtime.ts +12 -20
  147. package/src/server/mutation-history.ts +15 -5
  148. package/src/server/operations/composites.ts +38 -40
  149. package/src/server/operations/http.ts +5 -5
  150. package/src/server/operations/index.ts +7 -1
  151. package/src/server/operations/invoker.ts +4 -3
  152. package/src/server/operations/mcp.ts +29 -36
  153. package/src/server/operations/ops/annotation.ts +122 -10
  154. package/src/server/operations/ops/app.ts +102 -77
  155. package/src/server/operations/ops/ax-await.ts +17 -10
  156. package/src/server/operations/ops/ax-read.ts +347 -0
  157. package/src/server/operations/ops/ax-shared.ts +2 -7
  158. package/src/server/operations/ops/ax-state.ts +32 -14
  159. package/src/server/operations/ops/ax-timeline.ts +32 -19
  160. package/src/server/operations/ops/ax-work.ts +54 -37
  161. package/src/server/operations/ops/batch.ts +39 -14
  162. package/src/server/operations/ops/canvas-wire.ts +91 -0
  163. package/src/server/operations/ops/edges.ts +37 -25
  164. package/src/server/operations/ops/ext-app.ts +346 -0
  165. package/src/server/operations/ops/groups.ts +49 -20
  166. package/src/server/operations/ops/intent.ts +18 -12
  167. package/src/server/operations/ops/json-render.ts +239 -98
  168. package/src/server/operations/ops/nodes.ts +300 -111
  169. package/src/server/operations/ops/query.ts +62 -33
  170. package/src/server/operations/ops/snapshots.ts +35 -26
  171. package/src/server/operations/ops/validate.ts +2 -1
  172. package/src/server/operations/ops/viewport.ts +60 -16
  173. package/src/server/operations/ops/webview.ts +44 -18
  174. package/src/server/operations/registry.ts +2 -3
  175. package/src/server/operations/types.ts +7 -5
  176. package/src/server/operations/webview-runner.ts +1 -3
  177. package/src/server/placement.ts +8 -18
  178. package/src/server/server.ts +121 -1012
  179. package/src/server/spatial-analysis.ts +39 -25
  180. package/src/server/trace-manager.ts +3 -8
  181. package/src/server/web-artifacts.ts +23 -27
  182. package/src/server/webpage-node.ts +5 -13
  183. package/src/shared/auto-arrange.ts +12 -5
  184. package/src/shared/ax-intent.ts +1 -1
  185. package/src/shared/content-height-reporter.ts +8 -6
  186. package/src/shared/ext-app-tool-result.ts +2 -6
  187. package/src/shared/placement.ts +1 -4
  188. package/src/shared/semantic-attention.ts +39 -37
  189. package/src/shared/surface.ts +8 -4
@@ -1,18 +1,21 @@
1
1
  /**
2
- * Annotation mutation op (plan-008 Wave 1): annotation.remove.
2
+ * Annotation ops: annotation.remove (plan-008 Wave 1) and annotation.add
3
+ * (plan-009 C1 slice 3 — the legacy POST /api/canvas/annotation handler,
4
+ * wire-identical).
3
5
  *
4
- * DELETE-by-id of a human-drawn canvas annotation. mutates: true → the registry
5
- * auto-emits one canvas-layout-update after success, matching the legacy
6
- * handleCanvasRemoveAnnotation handler (which emitted the layout update only on a
7
- * successful removal). A non-existent id is a 404 OperationError carrying the
8
- * exact legacy message; an omitted id is a 400 so composite callers get a loud
9
- * input error instead of a misleading "not found". HTTP renders errors as
10
- * { ok:false, error } and MCP renders them as isError tool results.
6
+ * annotation.remove: DELETE-by-id of a human-drawn canvas annotation.
7
+ * mutates: true → the registry auto-emits one canvas-layout-update after
8
+ * success, matching the legacy handler. A non-existent id is a 404
9
+ * OperationError carrying the exact legacy message; an omitted id is a 400 so
10
+ * composite callers get a loud input error instead of a misleading "not
11
+ * found". HTTP renders errors as { ok:false, error } and MCP renders them as
12
+ * isError tool results.
11
13
  *
12
14
  * This module must never import server.ts or index.ts.
13
15
  */
14
16
  import { z } from 'zod';
15
- import { canvasState } from '../../canvas-state.js';
17
+ import { canvasState, type CanvasAnnotation } from '../../canvas-state.js';
18
+ import { summarizeCanvasAnnotation } from '../../canvas-serialization.js';
16
19
  import { defineOperation, OperationError, type Operation } from '../types.js';
17
20
 
18
21
  const annotationRemoveShape = {
@@ -57,4 +60,113 @@ const annotationRemoveOperation = defineOperation<z.infer<typeof annotationRemov
57
60
  },
58
61
  });
59
62
 
60
- export const annotationOperations: Operation[] = [annotationRemoveOperation];
63
+ // ── annotation.add (helpers moved from server.ts) ─────────────
64
+
65
+ function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation['bounds'] {
66
+ const xs = points.map((point) => point.x);
67
+ const ys = points.map((point) => point.y);
68
+ const minX = Math.min(...xs);
69
+ const minY = Math.min(...ys);
70
+ const maxX = Math.max(...xs);
71
+ const maxY = Math.max(...ys);
72
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
73
+ }
74
+
75
+ function textAnnotationBounds(
76
+ point: CanvasAnnotation['points'][number],
77
+ text: string,
78
+ width: number,
79
+ ): CanvasAnnotation['bounds'] {
80
+ return {
81
+ x: point.x,
82
+ y: point.y - width,
83
+ width: Math.max(width, text.length * width * 0.62),
84
+ height: width * 1.2,
85
+ };
86
+ }
87
+
88
+ function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
89
+ if (!Array.isArray(value)) return [];
90
+ return value
91
+ .map((point) => {
92
+ if (!point || typeof point !== 'object' || Array.isArray(point)) return null;
93
+ const record = point as Record<string, unknown>;
94
+ if (typeof record.x !== 'number' || typeof record.y !== 'number') return null;
95
+ if (!Number.isFinite(record.x) || !Number.isFinite(record.y)) return null;
96
+ return { x: record.x, y: record.y };
97
+ })
98
+ .filter((point): point is CanvasAnnotation['points'][number] => point !== null);
99
+ }
100
+
101
+ const annotationAddShape = {
102
+ type: z.unknown().optional().describe("Annotation type: 'freehand' (default) or 'text'"),
103
+ points: z.unknown().optional().describe('Annotation points: [{ x, y }, …]'),
104
+ width: z.unknown().optional().describe('Stroke width (freehand) or font size (text)'),
105
+ color: z.unknown().optional().describe("'currentColor' or a #rrggbb hex color"),
106
+ label: z.unknown().optional().describe('Short annotation label'),
107
+ text: z.unknown().optional().describe('Text annotation content'),
108
+ id: z.unknown().optional().describe('Explicit annotation id'),
109
+ };
110
+
111
+ const annotationAddSchema = z.looseObject(annotationAddShape);
112
+
113
+ const annotationAddOperation = defineOperation<z.infer<typeof annotationAddSchema>, Record<string, unknown>>({
114
+ name: 'annotation.add',
115
+ mutates: true,
116
+ input: annotationAddSchema,
117
+ inputShape: annotationAddShape,
118
+ http: {
119
+ method: 'POST',
120
+ path: '/api/canvas/annotation',
121
+ },
122
+ handler: (input) => {
123
+ const body: Record<string, unknown> = input;
124
+ const type = body.type === 'text' ? 'text' : 'freehand';
125
+ const points = parseAnnotationPoints(body.points);
126
+ if (points.length < (type === 'text' ? 1 : 2)) {
127
+ throw new OperationError(
128
+ type === 'text' ? 'Text annotation requires a valid point.' : 'Annotation requires at least two valid points.',
129
+ );
130
+ }
131
+
132
+ const defaultWidth = type === 'text' ? 24 : 4;
133
+ const maxWidth = type === 'text' ? 96 : 24;
134
+ const width =
135
+ typeof body.width === 'number' && Number.isFinite(body.width)
136
+ ? Math.min(maxWidth, Math.max(1, body.width))
137
+ : defaultWidth;
138
+ const color =
139
+ typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
140
+ ? body.color
141
+ : 'currentColor';
142
+ const label =
143
+ typeof body.label === 'string' && body.label.trim().length > 0 ? body.label.trim().slice(0, 160) : undefined;
144
+ const text =
145
+ type === 'text' && typeof body.text === 'string' && body.text.trim().length > 0
146
+ ? body.text.trim().slice(0, 240)
147
+ : undefined;
148
+ if (type === 'text' && !text) {
149
+ throw new OperationError('Text annotation requires text.');
150
+ }
151
+ const id =
152
+ typeof body.id === 'string' && body.id.trim().length > 0
153
+ ? body.id.trim().slice(0, 120)
154
+ : `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
155
+ const annotation: CanvasAnnotation = {
156
+ id,
157
+ type,
158
+ points,
159
+ bounds: type === 'text' ? textAnnotationBounds(points[0]!, text!, width) : annotationBounds(points),
160
+ color,
161
+ width,
162
+ ...(text ? { text } : {}),
163
+ ...((label ?? text) ? { label: label ?? text } : {}),
164
+ createdAt: new Date().toISOString(),
165
+ };
166
+
167
+ canvasState.addAnnotation(annotation);
168
+ return { ok: true, annotation: summarizeCanvasAnnotation(annotation) };
169
+ },
170
+ });
171
+
172
+ export const annotationOperations: Operation[] = [annotationRemoveOperation, annotationAddOperation];
@@ -85,19 +85,16 @@ export interface OpenMcpAppCoreResult {
85
85
  * call it. The diagram.open op delegates here after building the Excalidraw input
86
86
  * (the SSE pair fires ONCE — diagram.open does not re-emit).
87
87
  */
88
- export async function openMcpAppCore(
89
- input: OpenMcpAppCoreInput,
90
- ctx: OperationContext,
91
- ): Promise<OpenMcpAppCoreResult> {
88
+ export async function openMcpAppCore(input: OpenMcpAppCoreInput, ctx: OperationContext): Promise<OpenMcpAppCoreResult> {
92
89
  // The canvas node is created as a side-effect of the `ext-app-open` SSE event
93
90
  // (syncEventToCanvasState). Inside a suppressed-emit run (canvas.batch) that
94
91
  // event is dropped, so the node would never be created and the just-opened
95
92
  // external session would leak with a null nodeId. Reject loudly BEFORE opening
96
93
  // any session rather than silently corrupting. mcpapp.open / diagram.open are
97
- // not batchable — call canvas_open_mcp_app / canvas_add_diagram (or canvas_app).
94
+ // not batchable — call canvas_app (action "open-mcp-app" / "diagram") instead.
98
95
  if (isEmitSuppressed()) {
99
96
  throw new OperationError(
100
- 'mcpapp.open / diagram.open cannot run inside canvas_batch: the canvas node is created from the ext-app-open SSE event, which batch suppresses. Call the op directly (canvas_open_mcp_app / canvas_add_diagram, or canvas_app).',
97
+ 'mcpapp.open / diagram.open cannot run inside canvas_batch: the canvas node is created from the ext-app-open SSE event, which batch suppresses. Call canvas_app with action "open-mcp-app" or "diagram" outside the batch.',
101
98
  );
102
99
  }
103
100
  const targetNode = input.nodeId ? canvasState.getNode(input.nodeId) : undefined;
@@ -155,10 +152,12 @@ export async function openMcpAppCore(
155
152
  success: toolResult.isError !== true,
156
153
  result: toolResult,
157
154
  });
158
- const nodeId = input.nodeId ?? findCanvasExtAppNodeId(toolCallId, {
159
- getNode: (id) => canvasState.getNode(id),
160
- listNodes: () => canvasState.getLayout().nodes,
161
- });
155
+ const nodeId =
156
+ input.nodeId ??
157
+ findCanvasExtAppNodeId(toolCallId, {
158
+ getNode: (id) => canvasState.getNode(id),
159
+ listNodes: () => canvasState.getLayout().nodes,
160
+ });
162
161
  return {
163
162
  ok: true,
164
163
  ...(nodeId ? { id: nodeId } : {}),
@@ -180,15 +179,10 @@ function buildOpenMcpAppInput(body: Record<string, unknown>): OpenMcpAppCoreInpu
180
179
  throw new OperationError('Missing valid transport or toolName.');
181
180
  }
182
181
  const toolArguments = isRecord(body.toolArguments) ? body.toolArguments : undefined;
183
- const serverName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
184
- ? body.serverName.trim()
185
- : undefined;
186
- const title = typeof body.title === 'string' && body.title.trim().length > 0
187
- ? body.title.trim()
188
- : undefined;
189
- const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
190
- ? body.nodeId.trim()
191
- : undefined;
182
+ const serverName =
183
+ typeof body.serverName === 'string' && body.serverName.trim().length > 0 ? body.serverName.trim() : undefined;
184
+ const title = typeof body.title === 'string' && body.title.trim().length > 0 ? body.title.trim() : undefined;
185
+ const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0 ? body.nodeId.trim() : undefined;
192
186
  return {
193
187
  transport,
194
188
  toolName,
@@ -216,21 +210,26 @@ const openMcpAppShape = {
216
210
  y: z.number().optional().describe('Y position (auto-placed if omitted)'),
217
211
  width: z.number().optional().describe('Width in pixels (default: 720)'),
218
212
  height: z.number().optional().describe('Height in pixels (default: 500)'),
219
- timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for cold external app servers'),
220
- transport: z.union([
221
- z.object({
222
- type: z.literal('stdio'),
223
- command: z.string().describe('Executable used to start the external MCP server'),
224
- args: z.array(z.string()).optional().describe('Arguments for the executable'),
225
- cwd: z.string().optional().describe('Optional working directory'),
226
- env: z.record(z.string(), z.string()).optional().describe('Optional environment overrides'),
227
- }),
228
- z.object({
229
- type: z.literal('http'),
230
- url: z.string().describe('Streamable HTTP MCP endpoint URL'),
231
- headers: z.record(z.string(), z.string()).optional().describe('Optional HTTP headers'),
232
- }),
233
- ]).describe('How PMX Canvas should connect to the external MCP server'),
213
+ timeoutMs: z
214
+ .number()
215
+ .optional()
216
+ .describe('Optional MCP request timeout in milliseconds for cold external app servers'),
217
+ transport: z
218
+ .union([
219
+ z.object({
220
+ type: z.literal('stdio'),
221
+ command: z.string().describe('Executable used to start the external MCP server'),
222
+ args: z.array(z.string()).optional().describe('Arguments for the executable'),
223
+ cwd: z.string().optional().describe('Optional working directory'),
224
+ env: z.record(z.string(), z.string()).optional().describe('Optional environment overrides'),
225
+ }),
226
+ z.object({
227
+ type: z.literal('http'),
228
+ url: z.string().describe('Streamable HTTP MCP endpoint URL'),
229
+ headers: z.record(z.string(), z.string()).optional().describe('Optional HTTP headers'),
230
+ }),
231
+ ])
232
+ .describe('How PMX Canvas should connect to the external MCP server'),
234
233
  };
235
234
  const openMcpAppSchema = z.looseObject(openMcpAppShape);
236
235
 
@@ -246,7 +245,7 @@ const openMcpAppOperation = defineOperation<z.infer<typeof openMcpAppSchema>, Op
246
245
  mcp: {
247
246
  toolName: 'canvas_open_mcp_app',
248
247
  description:
249
- '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.',
248
+ '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_app action "diagram" for the built-in Excalidraw preset.',
250
249
  formatResult: (result) => ({
251
250
  content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
252
251
  }),
@@ -257,17 +256,29 @@ const openMcpAppOperation = defineOperation<z.infer<typeof openMcpAppSchema>, Op
257
256
  // ── diagram.open (Excalidraw preset → mcpapp.open core) ───────
258
257
 
259
258
  const diagramShape = {
260
- elements: z.union([
261
- z.string().describe('JSON array string of Excalidraw elements'),
262
- z.array(z.record(z.string(), z.unknown())).describe('Array of Excalidraw elements'),
263
- ]).describe('Excalidraw elements to render. See https://github.com/excalidraw/excalidraw-mcp for the element format.'),
264
- nodeId: z.string().optional().describe('Existing Excalidraw mcp-app node ID to update in place instead of creating a new node.'),
259
+ elements: z
260
+ .union([
261
+ z.string().describe('JSON array string of Excalidraw elements'),
262
+ z.array(z.record(z.string(), z.unknown())).describe('Array of Excalidraw elements'),
263
+ ])
264
+ .describe(
265
+ 'Excalidraw elements to render. See https://github.com/excalidraw/excalidraw-mcp for the element format.',
266
+ ),
267
+ nodeId: z
268
+ .string()
269
+ .optional()
270
+ .describe('Existing Excalidraw mcp-app node ID to update in place instead of creating a new node.'),
265
271
  title: z.string().optional().describe('Optional canvas node title override'),
266
272
  x: z.number().optional().describe('X position (auto-placed if omitted)'),
267
273
  y: z.number().optional().describe('Y position (auto-placed if omitted)'),
268
274
  width: z.number().optional().describe('Width in pixels (default: 720)'),
269
275
  height: z.number().optional().describe('Height in pixels (default: 500)'),
270
- timeoutMs: z.number().optional().describe('Optional MCP request timeout in milliseconds for Excalidraw cold starts. Client-side MCP hosts may still enforce their own total request timeout.'),
276
+ timeoutMs: z
277
+ .number()
278
+ .optional()
279
+ .describe(
280
+ 'Optional MCP request timeout in milliseconds for Excalidraw cold starts. Client-side MCP hosts may still enforce their own total request timeout.',
281
+ ),
271
282
  };
272
283
  const diagramSchema = z.looseObject(diagramShape);
273
284
 
@@ -283,13 +294,13 @@ const diagramOperation = defineOperation<z.infer<typeof diagramSchema>, OpenMcpA
283
294
  mcp: {
284
295
  toolName: 'canvas_add_diagram',
285
296
  description:
286
- 'Draw a hand-drawn diagram on the canvas via the hosted Excalidraw MCP app. Pass an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows, text). The diagram opens inside an mcp-app node that supports fullscreen editing. For other MCP apps, use canvas_open_mcp_app.',
297
+ 'Draw a hand-drawn diagram on the canvas via the hosted Excalidraw MCP app. Pass an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows, text). The diagram opens inside an mcp-app node that supports fullscreen editing. For other MCP apps, use canvas_app action "open-mcp-app".',
287
298
  formatResult: (result) => ({
288
299
  content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
289
300
  }),
290
301
  },
291
302
  handler: (input, ctx) => {
292
- let built;
303
+ let built: ReturnType<typeof buildExcalidrawOpenMcpAppInput>;
293
304
  try {
294
305
  built = buildExcalidrawOpenMcpAppInput(input);
295
306
  } catch (error) {
@@ -297,19 +308,22 @@ const diagramOperation = defineOperation<z.infer<typeof diagramSchema>, OpenMcpA
297
308
  }
298
309
  // Delegate to the shared open core (the SSE pair fires once — diagram.open
299
310
  // does not re-emit).
300
- return openMcpAppCore({
301
- transport: built.transport,
302
- toolName: built.toolName,
303
- toolArguments: built.toolArguments,
304
- serverName: built.serverName,
305
- ...(built.nodeId ? { nodeId: built.nodeId } : {}),
306
- ...(built.title ? { title: built.title } : {}),
307
- ...(typeof built.x === 'number' ? { x: built.x } : {}),
308
- ...(typeof built.y === 'number' ? { y: built.y } : {}),
309
- ...(typeof built.width === 'number' ? { width: built.width } : {}),
310
- ...(typeof built.height === 'number' ? { height: built.height } : {}),
311
- ...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
312
- }, ctx);
311
+ return openMcpAppCore(
312
+ {
313
+ transport: built.transport,
314
+ toolName: built.toolName,
315
+ toolArguments: built.toolArguments,
316
+ serverName: built.serverName,
317
+ ...(built.nodeId ? { nodeId: built.nodeId } : {}),
318
+ ...(built.title ? { title: built.title } : {}),
319
+ ...(typeof built.x === 'number' ? { x: built.x } : {}),
320
+ ...(typeof built.y === 'number' ? { y: built.y } : {}),
321
+ ...(typeof built.width === 'number' ? { width: built.width } : {}),
322
+ ...(typeof built.height === 'number' ? { height: built.height } : {}),
323
+ ...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
324
+ },
325
+ ctx,
326
+ );
313
327
  },
314
328
  });
315
329
 
@@ -321,23 +335,42 @@ const webArtifactShape = {
321
335
  indexCss: z.string().optional().describe('Optional contents for src/index.css'),
322
336
  mainTsx: z.string().optional().describe('Optional contents for src/main.tsx'),
323
337
  indexHtml: z.string().optional().describe('Optional contents for index.html'),
324
- files: z.record(z.string(), z.string()).optional().describe('Optional map of additional project-relative file paths to file contents'),
325
- deps: z.array(z.string()).optional().describe('Optional npm dependencies to install before bundling (e.g. ["recharts", "framer-motion@^11"]). Validated against npm-name format; flags and shell metacharacters are rejected.'),
326
- projectPath: z.string().optional().describe('Optional workspace-relative reusable project path. Defaults to .pmx-canvas/artifacts/.web-artifacts/<slug>'),
327
- outputPath: z.string().optional().describe('Optional workspace-relative HTML output path. Defaults to .pmx-canvas/artifacts/<slug>.html'),
338
+ files: z
339
+ .record(z.string(), z.string())
340
+ .optional()
341
+ .describe('Optional map of additional project-relative file paths to file contents'),
342
+ deps: z
343
+ .array(z.string())
344
+ .optional()
345
+ .describe(
346
+ 'Optional npm dependencies to install before bundling (e.g. ["recharts", "framer-motion@^11"]). Validated against npm-name format; flags and shell metacharacters are rejected.',
347
+ ),
348
+ projectPath: z
349
+ .string()
350
+ .optional()
351
+ .describe(
352
+ 'Optional workspace-relative reusable project path. Defaults to .pmx-canvas/artifacts/.web-artifacts/<slug>',
353
+ ),
354
+ outputPath: z
355
+ .string()
356
+ .optional()
357
+ .describe('Optional workspace-relative HTML output path. Defaults to .pmx-canvas/artifacts/<slug>.html'),
328
358
  openInCanvas: z.boolean().optional().describe('Open the generated artifact in canvas after build (default true)'),
329
359
  includeLogs: z.boolean().optional().describe('Include raw build stdout/stderr in the response (default false)'),
330
- initScriptPath: z.string().optional().describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
331
- bundleScriptPath: z.string().optional().describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
360
+ initScriptPath: z
361
+ .string()
362
+ .optional()
363
+ .describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
364
+ bundleScriptPath: z
365
+ .string()
366
+ .optional()
367
+ .describe('Optional script path override for tests/debugging. Must resolve inside the workspace.'),
332
368
  timeoutMs: z.number().optional().describe('Optional timeout in milliseconds for init and bundle commands'),
333
369
  };
334
370
  const webArtifactSchema = z.looseObject(webArtifactShape);
335
371
 
336
372
  /** Shape the byte-identical web-artifact response envelope from the build result. */
337
- function webArtifactEnvelope(
338
- result: WebArtifactCanvasBuildResult,
339
- includeLogs: boolean,
340
- ): Record<string, unknown> {
373
+ function webArtifactEnvelope(result: WebArtifactCanvasBuildResult, includeLogs: boolean): Record<string, unknown> {
341
374
  return {
342
375
  ok: true,
343
376
  path: result.filePath,
@@ -415,12 +448,8 @@ const webArtifactOperation = defineOperation<z.infer<typeof webArtifactSchema>,
415
448
  // legacy HTTP handler used resolveWorkspacePath; the MCP tool used its
416
449
  // own safeWorkspacePath — both enforce containment). resolveWorkspacePath
417
450
  // resolves against the active canvas workspace root.
418
- ...(typeof input.projectPath === 'string'
419
- ? { projectPath: resolveWorkspacePath(input.projectPath) }
420
- : {}),
421
- ...(typeof input.outputPath === 'string'
422
- ? { outputPath: resolveWorkspacePath(input.outputPath) }
423
- : {}),
451
+ ...(typeof input.projectPath === 'string' ? { projectPath: resolveWorkspacePath(input.projectPath) } : {}),
452
+ ...(typeof input.outputPath === 'string' ? { outputPath: resolveWorkspacePath(input.outputPath) } : {}),
424
453
  // Script-path overrides are honored only when contained inside the
425
454
  // workspace (enforced by resolveTrustedScriptPath in
426
455
  // executeWebArtifactBuild), so they cannot point at an arbitrary host
@@ -440,8 +469,4 @@ const webArtifactOperation = defineOperation<z.infer<typeof webArtifactSchema>,
440
469
  },
441
470
  });
442
471
 
443
- export const appOperations: Operation[] = [
444
- openMcpAppOperation,
445
- diagramOperation,
446
- webArtifactOperation,
447
- ];
472
+ export const appOperations: Operation[] = [openMcpAppOperation, diagramOperation, webArtifactOperation];
@@ -92,7 +92,13 @@ const awaitInputSchema = z.looseObject(awaitInputShape);
92
92
 
93
93
  const awaitMcpExtraShape = {
94
94
  id: z.string(),
95
- timeoutMs: z.number().int().min(0).max(120000).optional().describe('Max ms to block (default 30000; 0 = immediate read; capped at 120000).'),
95
+ timeoutMs: z
96
+ .number()
97
+ .int()
98
+ .min(0)
99
+ .max(120000)
100
+ .optional()
101
+ .describe('Max ms to block (default 30000; 0 = immediate read; capped at 120000).'),
96
102
  };
97
103
 
98
104
  // ── ax.approval.get (canvas_await_approval) ───────────────────
@@ -110,7 +116,8 @@ const axApprovalGetOperation = defineOperation<z.infer<typeof awaitInputSchema>,
110
116
  },
111
117
  mcp: {
112
118
  toolName: 'canvas_await_approval',
113
- description: 'Block until an approval gate resolves (the human approves/rejects it in the browser) or the timeout elapses — primitive D, gates that actually gate. timeoutMs 0 = read immediately without waiting. Returns { approvalGate, pending } (pending=true → still unresolved after the wait).',
119
+ description:
120
+ 'Block until an approval gate resolves (the human approves/rejects it in the browser) or the timeout elapses — primitive D, gates that actually gate. timeoutMs 0 = read immediately without waiting. Returns { approvalGate, pending } (pending=true → still unresolved after the wait).',
114
121
  extraShape: awaitMcpExtraShape,
115
122
  buildInput: buildAwaitInput,
116
123
  formatResult: (result) => {
@@ -118,7 +125,9 @@ const axApprovalGetOperation = defineOperation<z.infer<typeof awaitInputSchema>,
118
125
  const approvalGate = (body.approvalGate ?? null) as PmxAxApprovalGate | null;
119
126
  const pending = body.pending === true;
120
127
  return {
121
- content: [{ type: 'text' as const, text: JSON.stringify({ ok: approvalGate !== null, approvalGate, pending }) }],
128
+ content: [
129
+ { type: 'text' as const, text: JSON.stringify({ ok: approvalGate !== null, approvalGate, pending }) },
130
+ ],
122
131
  };
123
132
  },
124
133
  },
@@ -148,7 +157,8 @@ const axElicitationGetOperation = defineOperation<z.infer<typeof awaitInputSchem
148
157
  },
149
158
  mcp: {
150
159
  toolName: 'canvas_await_elicitation',
151
- description: 'Block until an elicitation is answered (the human responds in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { elicitation, pending }.',
160
+ description:
161
+ 'Block until an elicitation is answered (the human responds in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { elicitation, pending }.',
152
162
  extraShape: awaitMcpExtraShape,
153
163
  buildInput: buildAwaitInput,
154
164
  formatResult: (result) => {
@@ -186,7 +196,8 @@ const axModeGetOperation = defineOperation<z.infer<typeof awaitInputSchema>, Rec
186
196
  },
187
197
  mcp: {
188
198
  toolName: 'canvas_await_mode',
189
- description: 'Block until a mode request resolves (approved/rejected in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { modeRequest, pending }.',
199
+ description:
200
+ 'Block until a mode request resolves (approved/rejected in the browser) or the timeout elapses — primitive D. timeoutMs 0 = read immediately. Returns { modeRequest, pending }.',
190
201
  extraShape: awaitMcpExtraShape,
191
202
  buildInput: buildAwaitInput,
192
203
  formatResult: (result) => {
@@ -209,8 +220,4 @@ const axModeGetOperation = defineOperation<z.infer<typeof awaitInputSchema>, Rec
209
220
  },
210
221
  });
211
222
 
212
- export const axAwaitOperations: Operation[] = [
213
- axApprovalGetOperation,
214
- axElicitationGetOperation,
215
- axModeGetOperation,
216
- ];
223
+ export const axAwaitOperations: Operation[] = [axApprovalGetOperation, axElicitationGetOperation, axModeGetOperation];