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
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Ext-app operations (plan-009 C1 slice 1): extapp.call-tool /
3
+ * extapp.read-resource / extapp.list-tools / extapp.list-resources /
4
+ * extapp.list-resource-templates / extapp.list-prompts / extapp.model-context
5
+ * — the /api/ext-app/* wire surface the canvas iframe bridge calls. Wire
6
+ * envelopes are byte-identical to the legacy server.ts handlers they replace
7
+ * ({ ok: true, result } / { ok: false, error } with the same status codes).
8
+ * HTTP-only: none of these register MCP tools (the sandboxed iframe is the
9
+ * caller, not an agent), so the frozen 27-tool MCP surface is untouched.
10
+ *
11
+ * This module must never import server.ts or index.ts.
12
+ */
13
+ import { z } from 'zod';
14
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
15
+ import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
16
+ import {
17
+ buildExcalidrawRestoreCheckpointToolInput,
18
+ buildExcalidrawCheckpointId,
19
+ ensureExcalidrawCheckpointId,
20
+ EXCALIDRAW_READ_CHECKPOINT_TOOL,
21
+ EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
22
+ getExcalidrawCheckpointIdFromToolResult,
23
+ isExcalidrawCreateView,
24
+ } from '../../diagram-presets.js';
25
+ import {
26
+ callMcpAppTool,
27
+ listMcpAppPrompts,
28
+ listMcpAppResources,
29
+ listMcpAppResourceTemplates,
30
+ listMcpAppTools,
31
+ readMcpAppResource,
32
+ } from '../../mcp-app-runtime.js';
33
+ import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
34
+ import { isRecord } from './nodes.js';
35
+
36
+ // ── Excalidraw checkpoint helpers (moved from server.ts) ──────
37
+
38
+ function isCheckpointToolName(toolName: string): boolean {
39
+ return toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL || toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL;
40
+ }
41
+
42
+ /**
43
+ * Decide whether a fresh `callServerTool` result should *replace* the
44
+ * canvas node's bootstrap-replay `toolResult`.
45
+ *
46
+ * The bootstrap-replay toolResult is what the server re-sends to the
47
+ * widget on reload to restore visible state. We only want to overwrite
48
+ * it when the new result genuinely carries widget state — `isError` or
49
+ * `structuredContent`. A plain-text result (e.g. `read_checkpoint`
50
+ * returning a string status, or any informational message) updates
51
+ * `appModelContext` for the agent's record but should *not* clobber the
52
+ * bootstrap entry, because doing so would replace the widget's restored
53
+ * state with conversational noise on the next reload.
54
+ *
55
+ * This separation is exercised by:
56
+ * - tests/unit/server-api.test.ts "keeps ext-app model context
57
+ * separate from the replayed tool result" (text-only result preserves
58
+ * bootstrap replay)
59
+ * - tests/unit/server-api.test.ts "app-only text tool results update
60
+ * model context without replacing bootstrap replay"
61
+ * - tests/unit/server-api.test.ts "rehydrates Excalidraw checkpoint
62
+ * replay after server restart" (structured-content result becomes
63
+ * the new bootstrap replay)
64
+ */
65
+ function shouldReplayAppToolResult(toolName: string, result: CallToolResult): boolean {
66
+ void toolName;
67
+ return Boolean(result.isError || result.structuredContent);
68
+ }
69
+
70
+ function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
71
+ const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
72
+ const storedCheckpointId = appCheckpoint?.id;
73
+ if (typeof storedCheckpointId === 'string' && storedCheckpointId.trim().length > 0) {
74
+ return storedCheckpointId.trim();
75
+ }
76
+ return getExcalidrawCheckpointIdFromToolResult(node.data.toolResult) ?? buildExcalidrawCheckpointId(node.id);
77
+ }
78
+
79
+ function getLocalExcalidrawCheckpointData(
80
+ node: CanvasNodeState,
81
+ args: Record<string, unknown> | undefined,
82
+ ): string | null {
83
+ if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return null;
84
+ if (!isRecord(args) || typeof args.id !== 'string') return null;
85
+ if (args.id.trim() !== getExtAppNodeCheckpointId(node)) return null;
86
+ const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
87
+ const data = appCheckpoint?.data;
88
+ return typeof data === 'string' ? data : '';
89
+ }
90
+
91
+ function persistExcalidrawCheckpointToNode(
92
+ nodeId: string,
93
+ node: CanvasNodeState,
94
+ args: Record<string, unknown> | undefined,
95
+ ): boolean {
96
+ if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return false;
97
+ if (!isRecord(args) || typeof args.id !== 'string') return false;
98
+ const checkpointId = getExtAppNodeCheckpointId(node);
99
+ if (args.id.trim() !== checkpointId) return false;
100
+
101
+ const currentToolInput = isRecord(node.data.toolInput) ? node.data.toolInput : {};
102
+ const nextToolInput = {
103
+ ...currentToolInput,
104
+ elements: buildExcalidrawRestoreCheckpointToolInput(checkpointId, args.data),
105
+ };
106
+ const currentToolResult = isRecord(node.data.toolResult)
107
+ ? ensureExcalidrawCheckpointId(node.data.toolResult as CallToolResult, node.id, checkpointId)
108
+ : undefined;
109
+
110
+ canvasState.updateNode(nodeId, {
111
+ data: {
112
+ ...node.data,
113
+ toolInput: nextToolInput,
114
+ ...(currentToolResult ? { toolResult: currentToolResult } : {}),
115
+ appCheckpoint: {
116
+ toolName: EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
117
+ id: checkpointId,
118
+ ...(typeof args.data === 'string' ? { data: args.data } : {}),
119
+ updatedAt: new Date().toISOString(),
120
+ },
121
+ },
122
+ });
123
+ return true;
124
+ }
125
+
126
+ // ── Shared input plumbing ─────────────────────────────────────
127
+
128
+ function requireSessionId(body: Record<string, unknown>): string {
129
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
130
+ if (!sessionId) throw new OperationError('Missing sessionId.');
131
+ return sessionId;
132
+ }
133
+
134
+ /** Runtime failures keep the legacy wire shape: 400 { ok:false, error }. */
135
+ function toWireError(error: unknown): OperationError {
136
+ return new OperationError(error instanceof Error ? error.message : String(error));
137
+ }
138
+
139
+ const sessionShape = {
140
+ sessionId: z.unknown().optional().describe('Ext-app session id'),
141
+ };
142
+
143
+ /** The four list-* endpoints differ only in the runtime call they proxy. */
144
+ function defineExtAppListOperation(segment: string, list: (sessionId: string) => Promise<unknown>): Operation {
145
+ const schema = z.looseObject(sessionShape);
146
+ return defineOperation<z.infer<typeof schema>, Record<string, unknown>>({
147
+ name: `extapp.${segment}`,
148
+ mutates: false,
149
+ input: schema,
150
+ inputShape: sessionShape,
151
+ http: {
152
+ method: 'POST',
153
+ path: `/api/ext-app/${segment}`,
154
+ },
155
+ handler: async (input) => {
156
+ const sessionId = requireSessionId(input);
157
+ try {
158
+ return { ok: true, result: await list(sessionId) };
159
+ } catch (error) {
160
+ throw toWireError(error);
161
+ }
162
+ },
163
+ });
164
+ }
165
+
166
+ // ── extapp.call-tool ──────────────────────────────────────────
167
+
168
+ const callToolShape = {
169
+ sessionId: z.unknown().optional().describe('Ext-app session id'),
170
+ toolName: z.unknown().optional().describe('Tool to invoke on the ext-app MCP session'),
171
+ arguments: z.unknown().optional().describe('Tool arguments object'),
172
+ nodeId: z.unknown().optional().describe('Canvas node id to replay/persist the tool result on'),
173
+ };
174
+
175
+ const callToolSchema = z.looseObject(callToolShape);
176
+
177
+ const callToolOperation = defineOperation<z.infer<typeof callToolSchema>, Record<string, unknown>>({
178
+ name: 'extapp.call-tool',
179
+ mutates: false,
180
+ input: callToolSchema,
181
+ inputShape: callToolShape,
182
+ http: {
183
+ method: 'POST',
184
+ path: '/api/ext-app/call-tool',
185
+ },
186
+ handler: async (input, ctx: OperationContext) => {
187
+ const body: Record<string, unknown> = input;
188
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
189
+ const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
190
+ if (!sessionId || !toolName) {
191
+ throw new OperationError('Missing sessionId or toolName.');
192
+ }
193
+
194
+ const args = isRecord(body.arguments) ? body.arguments : undefined;
195
+ const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
196
+
197
+ try {
198
+ const requestedNode = nodeId ? canvasState.getNode(nodeId) : undefined;
199
+ const canReadLocalCheckpoint =
200
+ requestedNode?.type === 'mcp-app' &&
201
+ requestedNode.data.mode === 'ext-app' &&
202
+ requestedNode.data.appSessionId === sessionId;
203
+ const localCheckpointData =
204
+ canReadLocalCheckpoint && toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL
205
+ ? getLocalExcalidrawCheckpointData(requestedNode, args)
206
+ : null;
207
+ const result =
208
+ localCheckpointData === null
209
+ ? await callMcpAppTool(sessionId, toolName, args)
210
+ : ({ content: [{ type: 'text', text: localCheckpointData }] } satisfies CallToolResult);
211
+ if (nodeId) {
212
+ const node = canvasState.getNode(nodeId);
213
+ if (node?.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.appSessionId === sessionId) {
214
+ let changed = false;
215
+ if (toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL && persistExcalidrawCheckpointToNode(nodeId, node, args)) {
216
+ // Checkpoint saves are replayed through toolInput.elements instead of
217
+ // replacing the original create_view result with a generic "ok".
218
+ changed = true;
219
+ } else if (
220
+ !(isExcalidrawCreateView(node.data.serverName, node.data.toolName) && isCheckpointToolName(toolName))
221
+ ) {
222
+ const nextData: Record<string, unknown> = { ...node.data };
223
+ if (shouldReplayAppToolResult(toolName, result)) nextData.toolResult = result;
224
+ const nextModelContext: Record<string, unknown> = {};
225
+ if (Array.isArray(result.content)) {
226
+ nextModelContext.content = result.content;
227
+ }
228
+ if (
229
+ result.structuredContent &&
230
+ typeof result.structuredContent === 'object' &&
231
+ !Array.isArray(result.structuredContent)
232
+ ) {
233
+ nextModelContext.structuredContent = result.structuredContent;
234
+ }
235
+ if (Object.keys(nextModelContext).length > 0) {
236
+ nextData.appModelContext = {
237
+ ...nextModelContext,
238
+ updatedAt: new Date().toISOString(),
239
+ };
240
+ }
241
+ canvasState.updateNode(nodeId, {
242
+ data: nextData,
243
+ });
244
+ changed = true;
245
+ }
246
+ if (changed) {
247
+ ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
248
+ }
249
+ }
250
+ }
251
+ return { ok: true, result };
252
+ } catch (error) {
253
+ throw toWireError(error);
254
+ }
255
+ },
256
+ });
257
+
258
+ // ── extapp.read-resource ──────────────────────────────────────
259
+
260
+ const readResourceShape = {
261
+ sessionId: z.unknown().optional().describe('Ext-app session id'),
262
+ uri: z.unknown().optional().describe('Resource URI to read from the ext-app MCP session'),
263
+ };
264
+
265
+ const readResourceSchema = z.looseObject(readResourceShape);
266
+
267
+ const readResourceOperation = defineOperation<z.infer<typeof readResourceSchema>, Record<string, unknown>>({
268
+ name: 'extapp.read-resource',
269
+ mutates: false,
270
+ input: readResourceSchema,
271
+ inputShape: readResourceShape,
272
+ http: {
273
+ method: 'POST',
274
+ path: '/api/ext-app/read-resource',
275
+ },
276
+ handler: async (input) => {
277
+ const body: Record<string, unknown> = input;
278
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
279
+ const uri = typeof body.uri === 'string' ? body.uri.trim() : '';
280
+ if (!sessionId || !uri) {
281
+ throw new OperationError('Missing sessionId or uri.');
282
+ }
283
+ try {
284
+ return { ok: true, result: await readMcpAppResource(sessionId, uri) };
285
+ } catch (error) {
286
+ throw toWireError(error);
287
+ }
288
+ },
289
+ });
290
+
291
+ // ── extapp.model-context ──────────────────────────────────────
292
+
293
+ const modelContextShape = {
294
+ nodeId: z.unknown().optional().describe('Canvas node id to attach the model context to'),
295
+ content: z.unknown().optional().describe('MCP content array'),
296
+ structuredContent: z.unknown().optional().describe('MCP structured content object'),
297
+ };
298
+
299
+ const modelContextSchema = z.looseObject(modelContextShape);
300
+
301
+ const modelContextOperation = defineOperation<z.infer<typeof modelContextSchema>, Record<string, unknown>>({
302
+ name: 'extapp.model-context',
303
+ mutates: false,
304
+ input: modelContextSchema,
305
+ inputShape: modelContextShape,
306
+ http: {
307
+ method: 'POST',
308
+ path: '/api/ext-app/model-context',
309
+ },
310
+ handler: (input, ctx: OperationContext) => {
311
+ const body: Record<string, unknown> = input;
312
+ const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
313
+ if (!nodeId) throw new OperationError('Missing nodeId.');
314
+
315
+ const node = canvasState.getNode(nodeId);
316
+ if (!node) throw new OperationError(`Node "${nodeId}" not found.`, 404);
317
+
318
+ canvasState.updateNode(nodeId, {
319
+ data: {
320
+ ...node.data,
321
+ appModelContext: {
322
+ ...(Array.isArray(body.content) ? { content: body.content } : {}),
323
+ ...(body.structuredContent &&
324
+ typeof body.structuredContent === 'object' &&
325
+ !Array.isArray(body.structuredContent)
326
+ ? { structuredContent: body.structuredContent }
327
+ : {}),
328
+ updatedAt: new Date().toISOString(),
329
+ },
330
+ },
331
+ });
332
+
333
+ ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
334
+ return { ok: true };
335
+ },
336
+ });
337
+
338
+ export const extAppOperations: Operation[] = [
339
+ callToolOperation,
340
+ readResourceOperation,
341
+ defineExtAppListOperation('list-tools', listMcpAppTools),
342
+ defineExtAppListOperation('list-resources', listMcpAppResources),
343
+ defineExtAppListOperation('list-resource-templates', listMcpAppResourceTemplates),
344
+ defineExtAppListOperation('list-prompts', listMcpAppPrompts),
345
+ modelContextOperation,
346
+ ];
@@ -10,11 +10,7 @@
10
10
  */
11
11
  import { z } from 'zod';
12
12
  import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
13
- import {
14
- createCanvasGroup,
15
- groupCanvasNodes,
16
- ungroupCanvasNodes,
17
- } from '../../canvas-operations.js';
13
+ import { createCanvasGroup, groupCanvasNodes, ungroupCanvasNodes } from '../../canvas-operations.js';
18
14
  import { defineOperation, OperationError, type Operation } from '../types.js';
19
15
  import { buildNodeResponse, createdNodePayloadFromNode, isRecord } from './nodes.js';
20
16
 
@@ -25,7 +21,11 @@ function pickChildLayout(value: unknown): 'grid' | 'column' | 'flow' | undefined
25
21
  // ── group.create ──────────────────────────────────────────────
26
22
 
27
23
  const groupCreateShape = {
28
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
24
+ intentId: z
25
+ .string()
26
+ .optional()
27
+ .catch(undefined)
28
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
29
29
  title: z.string().optional().catch(undefined).describe('Group title (default: "Group")'),
30
30
  childIds: z.unknown().optional().describe('Node IDs to include in the group. Group auto-sizes to fit them.'),
31
31
  color: z.string().optional().catch(undefined).describe('Group accent color (CSS color string, e.g. "#4a9eff")'),
@@ -33,7 +33,11 @@ const groupCreateShape = {
33
33
  y: z.number().optional().catch(undefined).describe('Y position (auto-computed from children if omitted)'),
34
34
  width: z.number().optional().catch(undefined).describe('Width (auto-computed from children if omitted)'),
35
35
  height: z.number().optional().catch(undefined).describe('Height (auto-computed from children if omitted)'),
36
- childLayout: z.enum(['grid', 'column', 'flow']).optional().catch(undefined).describe('Optional child auto-layout. Omit to preserve current child positions.'),
36
+ childLayout: z
37
+ .enum(['grid', 'column', 'flow'])
38
+ .optional()
39
+ .catch(undefined)
40
+ .describe('Optional child auto-layout. Omit to preserve current child positions.'),
37
41
  };
38
42
 
39
43
  const groupCreateSchema = z.looseObject(groupCreateShape);
@@ -49,10 +53,17 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
49
53
  },
50
54
  mcp: {
51
55
  toolName: 'canvas_create_group',
52
- description: 'Create a group (frame) on the canvas that visually contains other nodes. Groups are spatial containers — they communicate "these nodes belong together." If childIds are provided, grouping preserves child positions by default; pass childLayout to auto-pack them. You can also provide an explicit frame (x/y/width/height) and auto-arrange children inside it.',
56
+ description:
57
+ 'Create a group (frame) on the canvas that visually contains other nodes. Groups are spatial containers — they communicate "these nodes belong together." If childIds are provided, grouping preserves child positions by default; pass childLayout to auto-pack them. You can also provide an explicit frame (x/y/width/height) and auto-arrange children inside it.',
53
58
  extraShape: {
54
- childIds: z.array(z.string()).optional().describe('Node IDs to include in the group. Group auto-sizes to fit them.'),
55
- full: z.boolean().optional().describe('Return the full created group payload. Default false returns compact metadata.'),
59
+ childIds: z
60
+ .array(z.string())
61
+ .optional()
62
+ .describe('Node IDs to include in the group. Group auto-sizes to fit them.'),
63
+ full: z
64
+ .boolean()
65
+ .optional()
66
+ .describe('Return the full created group payload. Default false returns compact metadata.'),
56
67
  verbose: z.boolean().optional().describe('Alias for full:true.'),
57
68
  },
58
69
  formatResult: (result, input) => {
@@ -82,7 +93,16 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
82
93
  );
83
94
  }
84
95
  }
85
- const { node } = createCanvasGroup({ title, childIds, color, x, y, width, height, ...(childLayout ? { childLayout } : {}) });
96
+ const { node } = createCanvasGroup({
97
+ title,
98
+ childIds,
99
+ color,
100
+ x,
101
+ y,
102
+ width,
103
+ height,
104
+ ...(childLayout ? { childLayout } : {}),
105
+ });
86
106
  return node;
87
107
  },
88
108
  serialize: (node) => buildNodeResponse(node),
@@ -91,10 +111,18 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
91
111
  // ── group.add ─────────────────────────────────────────────────
92
112
 
93
113
  const groupAddShape = {
94
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
114
+ intentId: z
115
+ .string()
116
+ .optional()
117
+ .catch(undefined)
118
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
95
119
  groupId: z.string().optional().catch(undefined).describe('The group node ID'),
96
120
  childIds: z.unknown().optional().describe('Node IDs to add to the group'),
97
- childLayout: z.enum(['grid', 'column', 'flow']).optional().catch(undefined).describe('Optional child layout to apply while grouping'),
121
+ childLayout: z
122
+ .enum(['grid', 'column', 'flow'])
123
+ .optional()
124
+ .catch(undefined)
125
+ .describe('Optional child layout to apply while grouping'),
98
126
  };
99
127
 
100
128
  const groupAddSchema = z.looseObject(groupAddShape);
@@ -138,7 +166,11 @@ const groupAddOperation = defineOperation<z.infer<typeof groupAddSchema>, Record
138
166
  // ── group.remove (ungroup) ────────────────────────────────────
139
167
 
140
168
  const groupRemoveShape = {
141
- intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
169
+ intentId: z
170
+ .string()
171
+ .optional()
172
+ .catch(undefined)
173
+ .describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
142
174
  groupId: z.string().optional().catch(undefined).describe('The group node ID to ungroup'),
143
175
  };
144
176
 
@@ -155,7 +187,8 @@ const groupRemoveOperation = defineOperation<z.infer<typeof groupRemoveSchema>,
155
187
  },
156
188
  mcp: {
157
189
  toolName: 'canvas_ungroup',
158
- description: 'Remove all children from a group, releasing them as independent nodes. The group node itself remains (delete it separately with canvas_remove_node if desired).',
190
+ description:
191
+ 'Remove all children from a group, releasing them as independent nodes. The group node itself remains (delete it separately with canvas_remove_node if desired).',
159
192
  extraShape: {
160
193
  groupId: z.string().describe('The group node ID to ungroup'),
161
194
  },
@@ -172,8 +205,4 @@ const groupRemoveOperation = defineOperation<z.infer<typeof groupRemoveSchema>,
172
205
  },
173
206
  });
174
207
 
175
- export const groupOperations: Operation[] = [
176
- groupCreateOperation,
177
- groupAddOperation,
178
- groupRemoveOperation,
179
- ];
208
+ export const groupOperations: Operation[] = [groupCreateOperation, groupAddOperation, groupRemoveOperation];
@@ -24,7 +24,10 @@ const positionShape = z.object({ x: z.number(), y: z.number() });
24
24
  // ── intent.signal (canvas_intent action "signal") ─────────────
25
25
 
26
26
  const intentSignalShape = {
27
- kind: z.enum(INTENT_KINDS).optional().describe('create | move | connect | remove | edit — the move about to be made.'),
27
+ kind: z
28
+ .enum(INTENT_KINDS)
29
+ .optional()
30
+ .describe('create | move | connect | remove | edit — the move about to be made.'),
28
31
  position: positionShape.optional().describe('World coords: where a create forms, or the destination of a move.'),
29
32
  nodeId: z.string().optional().describe('The existing node a move/edit/remove targets.'),
30
33
  edge: z
@@ -66,7 +69,12 @@ const intentUpdateShape = {
66
69
  confidence: z.number().optional().describe('0..1 → ghost opacity/solidity.'),
67
70
  seq: z.number().optional().describe('New ordering hint.'),
68
71
  ttlMs: z.number().optional().describe('Reset the TTL to this many ms from now.'),
69
- vetoed: z.boolean().optional().describe('Veto the intent: dissolves the ghost AND poisons the id so a later linked settle is rejected (same as clear { vetoed:true }).'),
72
+ vetoed: z
73
+ .boolean()
74
+ .optional()
75
+ .describe(
76
+ 'Veto the intent: dissolves the ghost AND poisons the id so a later linked settle is rejected (same as clear { vetoed:true }).',
77
+ ),
70
78
  };
71
79
 
72
80
  const intentUpdateSchema = z.looseObject(intentUpdateShape);
@@ -104,14 +112,16 @@ const intentClearOperation = defineOperation<z.infer<typeof intentClearSchema>,
104
112
  method: 'DELETE',
105
113
  path: '/api/canvas/ax/intent/:id',
106
114
  readInput: async (req, params, url) => {
107
- const query: Record<string, string> = {};
115
+ const query: Record<string, unknown> = {};
108
116
  url.searchParams.forEach((value, key) => {
109
- query[key] = value;
117
+ // `vetoed` is a boolean in the op schema; query params arrive as strings.
118
+ // Coerce only the literal "true"/"false" forms — anything else passes
119
+ // through and fails validation loudly instead of silently becoming false.
120
+ query[key] = key === 'vetoed' && (value === 'true' || value === 'false') ? value === 'true' : value;
110
121
  });
111
122
  const body = await readJsonValue(req);
112
- const record = body !== null && typeof body === 'object' && !Array.isArray(body)
113
- ? body as Record<string, unknown>
114
- : {};
123
+ const record =
124
+ body !== null && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {};
115
125
  return { ...query, ...record, ...params };
116
126
  },
117
127
  },
@@ -126,8 +136,4 @@ const intentClearOperation = defineOperation<z.infer<typeof intentClearSchema>,
126
136
  },
127
137
  });
128
138
 
129
- export const intentOperations: Operation[] = [
130
- intentSignalOperation,
131
- intentUpdateOperation,
132
- intentClearOperation,
133
- ];
139
+ export const intentOperations: Operation[] = [intentSignalOperation, intentUpdateOperation, intentClearOperation];