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
@@ -4,7 +4,7 @@
4
4
  * Intents are EPHEMERAL PRESENCE, deliberately modelled like the attention /
5
5
  * timeline ephemerality layer rather than canvas-bound state:
6
6
  * - an in-memory Map (NOT CanvasStateManager) — never serialized, never
7
- * snapshotted, never returned by canvas_get_layout;
7
+ * snapshotted, never returned by canvas_query (layout action);
8
8
  * - count-capped (oldest evicted) and TTL-swept so a ghost can never linger;
9
9
  * - emitted over the same workbench SSE stream as `ax-intent` /
10
10
  * `ax-intent-clear` frames via an INJECTED emitter (server.ts wires it,
@@ -124,7 +124,7 @@ export class IntentRegistry {
124
124
 
125
125
  const now = Date.now();
126
126
  const ttl = typeof input.ttlMs === 'number' ? input.ttlMs : DEFAULT_INTENT_TTL_MS;
127
- const id = input.id && this.intents.has(input.id) ? input.id : input.id ?? nextIntentId();
127
+ const id = input.id && this.intents.has(input.id) ? input.id : (input.id ?? nextIntentId());
128
128
  this.pruneVetoTombstones();
129
129
  if (this.vetoedIntentIds.has(id)) {
130
130
  throw new OperationError(`Intent "${id}" was vetoed. Use a new id for a revised intent.`, 409);
@@ -234,10 +234,7 @@ export class IntentRegistry {
234
234
  throw new OperationError(`No live intent "${id}" to commit.`, 409);
235
235
  }
236
236
  if (!allowedKinds.includes(intent.kind)) {
237
- throw new OperationError(
238
- `Intent "${id}" has kind "${intent.kind}", which cannot commit this mutation.`,
239
- 409,
240
- );
237
+ throw new OperationError(`Intent "${id}" has kind "${intent.kind}", which cannot commit this mutation.`, 409);
241
238
  }
242
239
  this.committingIntentIds.add(id);
243
240
  return intent;
@@ -134,16 +134,12 @@ function toSafeExternalUrl(url: string): string | null {
134
134
  return parsed.toString();
135
135
  }
136
136
 
137
- export function getMcpAppCandidateFromToolCompletion(
138
- data: McpAppToolCompletionInput,
139
- ): McpAppToolCandidate | null {
137
+ export function getMcpAppCandidateFromToolCompletion(data: McpAppToolCompletionInput): McpAppToolCandidate | null {
140
138
  const inlineUrls = [
141
139
  ...(typeof data.content === 'string' ? [data.content] : []),
142
140
  ...(typeof data.detailedContent === 'string' ? [data.detailedContent] : []),
143
141
  ...collectToolResultTextFragments(data.result),
144
- ].flatMap((value) =>
145
- (value.match(/https?:\/\/[^\s<>"'`]+/gi) ?? []).map((url) => ({ url, keyHint: 'inline' })),
146
- );
142
+ ].flatMap((value) => (value.match(/https?:\/\/[^\s<>"'`]+/gi) ?? []).map((url) => ({ url, keyHint: 'inline' })));
147
143
 
148
144
  const nestedUrls = collectToolResultUrlCandidates(data.result);
149
145
  const candidates = [...inlineUrls, ...nestedUrls];
@@ -156,10 +152,9 @@ export function getMcpAppCandidateFromToolCompletion(
156
152
  const safe = toSafeExternalUrl(entry.url);
157
153
  if (!safe) continue;
158
154
  const key = entry.keyHint.toLowerCase();
159
- const hintedByKey =
160
- /resource|resource_link|resourceurl|resource_url|app|uri|url|link|viewer|preview|canvas/.test(
161
- key,
162
- );
155
+ const hintedByKey = /resource|resource_link|resourceurl|resource_url|app|uri|url|link|viewer|preview|canvas/.test(
156
+ key,
157
+ );
163
158
  const hintedByUrl = isLikelyMcpAppWebUrl(safe);
164
159
  if (!hintedByKey && !hintedByUrl && !sourceHasMcp) continue;
165
160
  return {
@@ -66,11 +66,7 @@ function ensureConfigDir(): void {
66
66
  }
67
67
  }
68
68
 
69
- const DEFAULT_MCP_APP_HOST_STATE_FILE = join(
70
- PMX_CANVAS_CONFIG_DIR,
71
- 'workbench',
72
- 'mcp-app-host-state.json',
73
- );
69
+ const DEFAULT_MCP_APP_HOST_STATE_FILE = join(PMX_CANVAS_CONFIG_DIR, 'workbench', 'mcp-app-host-state.json');
74
70
 
75
71
  function sessionDiagLogPath(): string {
76
72
  return String(process.env.PMX_SESSION_LOG || process.env.PMX_TEST_LOG || '').trim();
@@ -158,9 +154,7 @@ function resetRuntimeMetrics(): void {
158
154
  function clearPersistedRuntimeSessionsOnLoad(): void {
159
155
  const hadSessions = sessions.size;
160
156
  const hadMetrics =
161
- metrics.hostedOpens > 0 ||
162
- metrics.fallbackTotal > 0 ||
163
- Object.keys(metrics.fallbackByReason).length > 0;
157
+ metrics.hostedOpens > 0 || metrics.fallbackTotal > 0 || Object.keys(metrics.fallbackByReason).length > 0;
164
158
  if (hadSessions === 0 && !activeSessionId && !hadMetrics) return;
165
159
 
166
160
  sessions.clear();
@@ -192,9 +186,7 @@ function ensureStateLoaded(): void {
192
186
  const normalized: McpAppHostCapability = {
193
187
  serverName: entry.serverName.trim(),
194
188
  state:
195
- entry.state === 'supported' ||
196
- entry.state === 'unsupported' ||
197
- entry.state === 'degraded'
189
+ entry.state === 'supported' || entry.state === 'unsupported' || entry.state === 'degraded'
198
190
  ? entry.state
199
191
  : 'degraded',
200
192
  reasonCode:
@@ -204,9 +196,7 @@ function ensureStateLoaded(): void {
204
196
  runtimeReady: entry.runtimeReady === true,
205
197
  serverSupportsHost: entry.serverSupportsHost === true,
206
198
  updatedAt:
207
- typeof entry.updatedAt === 'string' && entry.updatedAt.trim().length > 0
208
- ? entry.updatedAt
209
- : nowIso(),
199
+ typeof entry.updatedAt === 'string' && entry.updatedAt.trim().length > 0 ? entry.updatedAt : nowIso(),
210
200
  };
211
201
  capabilities.set(normalized.serverName, normalized);
212
202
  }
@@ -235,20 +225,15 @@ function ensureStateLoaded(): void {
235
225
  ? entry.state
236
226
  : 'background',
237
227
  createdAt:
238
- typeof entry.createdAt === 'string' && entry.createdAt.trim().length > 0
239
- ? entry.createdAt
240
- : nowIso(),
228
+ typeof entry.createdAt === 'string' && entry.createdAt.trim().length > 0 ? entry.createdAt : nowIso(),
241
229
  lastSeenAt:
242
- typeof entry.lastSeenAt === 'string' && entry.lastSeenAt.trim().length > 0
243
- ? entry.lastSeenAt
244
- : nowIso(),
230
+ typeof entry.lastSeenAt === 'string' && entry.lastSeenAt.trim().length > 0 ? entry.lastSeenAt : nowIso(),
245
231
  fallbackReason:
246
232
  typeof entry.fallbackReason === 'string' && entry.fallbackReason.trim().length > 0
247
233
  ? entry.fallbackReason
248
234
  : null,
249
235
  lastExternalOpenAt:
250
- typeof entry.lastExternalOpenAt === 'string' &&
251
- entry.lastExternalOpenAt.trim().length > 0
236
+ typeof entry.lastExternalOpenAt === 'string' && entry.lastExternalOpenAt.trim().length > 0
252
237
  ? entry.lastExternalOpenAt
253
238
  : null,
254
239
  };
@@ -268,12 +253,8 @@ function ensureStateLoaded(): void {
268
253
  if (parsed.metrics && typeof parsed.metrics === 'object') {
269
254
  const loadedHostedOpens = Number(parsed.metrics.hostedOpens ?? 0);
270
255
  const loadedFallbackTotal = Number(parsed.metrics.fallbackTotal ?? 0);
271
- metrics.hostedOpens = Number.isFinite(loadedHostedOpens)
272
- ? Math.max(0, Math.floor(loadedHostedOpens))
273
- : 0;
274
- metrics.fallbackTotal = Number.isFinite(loadedFallbackTotal)
275
- ? Math.max(0, Math.floor(loadedFallbackTotal))
276
- : 0;
256
+ metrics.hostedOpens = Number.isFinite(loadedHostedOpens) ? Math.max(0, Math.floor(loadedHostedOpens)) : 0;
257
+ metrics.fallbackTotal = Number.isFinite(loadedFallbackTotal) ? Math.max(0, Math.floor(loadedFallbackTotal)) : 0;
277
258
  if (parsed.metrics.fallbackByReason && typeof parsed.metrics.fallbackByReason === 'object') {
278
259
  for (const [reason, count] of Object.entries(parsed.metrics.fallbackByReason)) {
279
260
  const normalizedReason = reason.trim();
@@ -370,10 +351,9 @@ function supportsHostSessionByHint(input: McpAppCandidateInput): boolean {
370
351
  const keyHint = String(input.keyHint || '').toLowerCase();
371
352
  const inferredType = String(input.inferredType || '').toLowerCase();
372
353
 
373
- const keySuggestsApps =
374
- /resource|resource_link|resourceurl|resource_url|app|uri|url|link|viewer|preview|canvas/.test(
375
- keyHint,
376
- );
354
+ const keySuggestsApps = /resource|resource_link|resourceurl|resource_url|app|uri|url|link|viewer|preview|canvas/.test(
355
+ keyHint,
356
+ );
377
357
  const sourceSuggestsApps =
378
358
  sourceServer.includes('mcp') ||
379
359
  sourceServer.includes('excalidraw') ||
@@ -533,10 +513,7 @@ export function preRegisterKnownMcpAppHostCapabilities(serverNames: string[]): v
533
513
  }
534
514
  }
535
515
 
536
- function resolveCapabilityForCandidate(
537
- input: McpAppCandidateInput,
538
- trustedDomain: boolean,
539
- ): McpAppHostCapability {
516
+ function resolveCapabilityForCandidate(input: McpAppCandidateInput, trustedDomain: boolean): McpAppHostCapability {
540
517
  const runtimeReady = isHostRuntimeEnabled();
541
518
  const serverName = normalizeServerName(input.sourceServer);
542
519
  const serverSupportsHost = supportsHostSessionByHint(input);
@@ -645,8 +622,7 @@ function findMatchingOpenSession(input: McpAppCandidateInput): McpAppHostSession
645
622
  function registerFallback(reasonCode: string): void {
646
623
  metrics.fallbackTotal += 1;
647
624
  const normalizedReason = reasonCode.trim() || 'fallback';
648
- metrics.fallbackByReason[normalizedReason] =
649
- (metrics.fallbackByReason[normalizedReason] ?? 0) + 1;
625
+ metrics.fallbackByReason[normalizedReason] = (metrics.fallbackByReason[normalizedReason] ?? 0) + 1;
650
626
  }
651
627
 
652
628
  export function routeMcpAppCandidateToHost(input: McpAppCandidateInput): McpAppHostRoutingResult {
@@ -13,15 +13,8 @@ import type {
13
13
  Tool,
14
14
  } from '@modelcontextprotocol/sdk/types.js';
15
15
  import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
16
- import {
17
- EXTENSION_ID,
18
- RESOURCE_MIME_TYPE,
19
- } from '@modelcontextprotocol/ext-apps/server';
20
- import type {
21
- McpUiClientCapabilities,
22
- McpUiResourceCsp,
23
- McpUiResourceMeta,
24
- } from '@modelcontextprotocol/ext-apps';
16
+ import { EXTENSION_ID, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
17
+ import type { McpUiClientCapabilities, McpUiResourceCsp, McpUiResourceMeta } from '@modelcontextprotocol/ext-apps';
25
18
  import { getToolUiResourceUri } from '@modelcontextprotocol/ext-apps/app-bridge';
26
19
  import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
27
20
 
@@ -39,9 +32,7 @@ export interface ExternalMcpStdioTransportConfig {
39
32
  env?: Record<string, string>;
40
33
  }
41
34
 
42
- export type ExternalMcpTransportConfig =
43
- | ExternalMcpHttpTransportConfig
44
- | ExternalMcpStdioTransportConfig;
35
+ export type ExternalMcpTransportConfig = ExternalMcpHttpTransportConfig | ExternalMcpStdioTransportConfig;
45
36
 
46
37
  export interface OpenMcpAppInput {
47
38
  transport: ExternalMcpTransportConfig;
@@ -220,10 +211,7 @@ async function createSession(
220
211
  timeoutMs?: number,
221
212
  ): Promise<McpAppSession> {
222
213
  const transport = buildTransport(transportConfig);
223
- const client = new Client(
224
- { name: 'pmx-canvas-app-host', version: '0.1.0' },
225
- { capabilities: clientCapabilities },
226
- );
214
+ const client = new Client({ name: 'pmx-canvas-app-host', version: '0.1.0' }, { capabilities: clientCapabilities });
227
215
  await client.connect(transport, requestOptions(timeoutMs));
228
216
 
229
217
  const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
@@ -369,10 +357,14 @@ export async function openMcpApp(input: OpenMcpAppInput): Promise<OpenMcpAppResu
369
357
  }
370
358
 
371
359
  const toolInput = isRecord(input.toolArguments) ? input.toolArguments : {};
372
- const rawToolResult = await session.client.callTool({
373
- name: tool.name,
374
- arguments: toolInput,
375
- }, undefined, options);
360
+ const rawToolResult = await session.client.callTool(
361
+ {
362
+ name: tool.name,
363
+ arguments: toolInput,
364
+ },
365
+ undefined,
366
+ options,
367
+ );
376
368
  const toolResult = normalizeExtAppToolResult({ result: rawToolResult });
377
369
  const readResult = await session.client.readResource({ uri: resourceUri }, options);
378
370
  const resourceMeta = resourceMetaFromReadResult(readResult);
@@ -254,10 +254,14 @@ export function diffLayouts(
254
254
  const changes: string[] = [];
255
255
 
256
256
  if (snapNode.position.x !== curNode.position.x || snapNode.position.y !== curNode.position.y) {
257
- changes.push(`moved (${snapNode.position.x},${snapNode.position.y}) → (${curNode.position.x},${curNode.position.y})`);
257
+ changes.push(
258
+ `moved (${snapNode.position.x},${snapNode.position.y}) → (${curNode.position.x},${curNode.position.y})`,
259
+ );
258
260
  }
259
261
  if (snapNode.size.width !== curNode.size.width || snapNode.size.height !== curNode.size.height) {
260
- changes.push(`resized ${snapNode.size.width}x${snapNode.size.height} → ${curNode.size.width}x${curNode.size.height}`);
262
+ changes.push(
263
+ `resized ${snapNode.size.width}x${snapNode.size.height} → ${curNode.size.width}x${curNode.size.height}`,
264
+ );
261
265
  }
262
266
  if (snapNode.collapsed !== curNode.collapsed) {
263
267
  changes.push(curNode.collapsed ? 'collapsed' : 'expanded');
@@ -314,8 +318,12 @@ export function diffLayouts(
314
318
  export function formatDiff(diff: SnapshotDiffResult): string {
315
319
  const lines: string[] = [`Diff: current canvas vs snapshot "${diff.snapshotName}"`, ''];
316
320
 
317
- const total = diff.addedNodes.length + diff.removedNodes.length + diff.modifiedNodes.length
318
- + diff.addedEdges.length + diff.removedEdges.length;
321
+ const total =
322
+ diff.addedNodes.length +
323
+ diff.removedNodes.length +
324
+ diff.modifiedNodes.length +
325
+ diff.addedEdges.length +
326
+ diff.removedEdges.length;
319
327
 
320
328
  if (total === 0) {
321
329
  lines.push('No differences — canvas matches the snapshot exactly.');
@@ -365,7 +373,9 @@ export function formatDiff(diff: SnapshotDiffResult): string {
365
373
  lines.push('');
366
374
  }
367
375
 
368
- lines.push(`Summary: +${diff.addedNodes.length} -${diff.removedNodes.length} ~${diff.modifiedNodes.length} nodes, +${diff.addedEdges.length} -${diff.removedEdges.length} edges`);
376
+ lines.push(
377
+ `Summary: +${diff.addedNodes.length} -${diff.removedNodes.length} ~${diff.modifiedNodes.length} nodes, +${diff.addedEdges.length} -${diff.removedEdges.length} edges`,
378
+ );
369
379
 
370
380
  return lines.join('\n');
371
381
  }
@@ -9,17 +9,22 @@
9
9
  * to the standalone `canvas_add_edge` — same op, same arg mapping, same result
10
10
  * shape — by construction. No handler logic lives here.
11
11
  *
12
- * Migration (docs/api-stability.md + plan-006): composites land ADDITIVELY in
13
- * v0.2 alongside the legacy single-purpose tools (the tool surface grows, then
14
- * shrinks when the legacy tools are removed in v0.3). Every action here maps to a
15
- * registry-backed operation (plan-005 slices 1–7 + plan-008 Wave 1).
12
+ * Migration (docs/api-stability.md + plan-006): composites landed ADDITIVELY in
13
+ * v0.2 alongside the legacy single-purpose tools. As of v0.3.0 the legacy tools
14
+ * those composites fold are REMOVED (registration-suppressed see
15
+ * `compositeFoldedOpNames` below); every action here maps to a registry-backed
16
+ * operation (plan-005 slices 1–7 + plan-008 Wave 1) that is now reachable ONLY
17
+ * through its composite (or `canvas_batch`).
16
18
  *
17
- * Still deferred (its legacy standalone tool keeps working; see plan-008): the
18
- * `canvas_snapshot` composite (the v0.3 name collision). The action enums are
19
- * forward-compatible: adding an action later is additive. (`canvas_webview`
20
- * shipped in plan-008 Wave 3 via runner injection; `canvas_app` shipped in Wave 4
21
- * open-mcp-app / diagram / build-artifact. Wave 5 folded the last three legacy
22
- * tools deprecate-only NO per-action input-injection mechanism was needed:
19
+ * Deferred to v0.4: the `canvas_snapshot` composite (still a name collision
20
+ * see below). Its 6 legacy snapshot standalones (`canvas_snapshot`,
21
+ * `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`,
22
+ * `canvas_gc_snapshots`, `canvas_diff`) stay registered in v0.3.0 but are marked
23
+ * deprecated (description-prefixed) per docs/api-stability.md's
24
+ * deprecate-one-minor-before-removal rule. (`canvas_webview` shipped in plan-008
25
+ * Wave 3 via runner injection; `canvas_app` shipped in Wave 4 — open-mcp-app /
26
+ * diagram / build-artifact. Wave 5 folded the last three legacy tools
27
+ * deprecate-only — NO per-action input-injection mechanism was needed:
23
28
  * `canvas_add_html_node` / `canvas_add_html_primitive` → `canvas_node` action
24
29
  * "add" (type:"html" [+ primitive]); `canvas_refresh_webpage_node` → `canvas_node`
25
30
  * action "update" (refresh:true). `canvas_screenshot` stays standalone — it
@@ -30,8 +35,8 @@
30
35
  * name is ALREADY a legacy standalone tool (the save-snapshot tool, op
31
36
  * `snapshot.save`), so it cannot be added additively without a name clash, and
32
37
  * repurposing `canvas_snapshot` to be action-discriminated now would break
33
- * existing callers. It lands in v0.3, in the same change that removes the legacy
34
- * single-purpose snapshot tools and frees the name.
38
+ * existing callers. It lands in v0.4, in the same change that removes the 6
39
+ * kept-but-deprecated legacy snapshot tools and frees the name.
35
40
  *
36
41
  * This module must never import server.ts or index.ts.
37
42
  */
@@ -47,7 +52,7 @@ import { z, type ZodRawShape, type ZodTypeAny } from 'zod';
47
52
  * - Two-discriminator (`canvas_ax_gate`, plan-007 Slice C): a `kind` × `action`
48
53
  * matrix folds 9 ops into one tool. Set `extraDiscriminatorShape` (the `kind`
49
54
  * enum), `memberOps` (the op names — used to derive the schema union + the
50
- * deprecation notes), `actionEnum` (the action discriminator values), and
55
+ * folded-op set), `actionEnum` (the action discriminator values), and
51
56
  * `resolveOp` (maps `{ kind, action }` → op name, or undefined for an invalid
52
57
  * combo → a loud error at dispatch). The flat `actions` map is left empty for
53
58
  * these; the matrix path uses `resolveOp` instead.
@@ -78,8 +83,9 @@ export interface CompositeToolDefinition {
78
83
  actionEnum?: readonly string[];
79
84
  /**
80
85
  * Two-discriminator extension: every member op name. Used to build the schema
81
- * union (all member-op fields, optional) and to derive a deprecation note per
82
- * member op (each mapped back to its (kind, action) by `describeOp`).
86
+ * union (all member-op fields, optional) and to populate the folded-op set
87
+ * (`compositeFoldedOpNames`) that suppresses each member op's standalone
88
+ * registration.
83
89
  */
84
90
  memberOps?: string[];
85
91
  /**
@@ -331,36 +337,28 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
331
337
  ];
332
338
 
333
339
  /**
334
- * Deprecation notes for the legacy single-purpose tools, DERIVED from the
335
- * composites: every operation a composite folds gets a `Deprecated: use
336
- * canvas_x with action "y".` prefix on its standalone tool description, steering
337
- * agents to the composite during the v0.2 overlap window (the legacy tools and
338
- * these notes are removed together in v0.3). Keyed by registry operation name.
339
- * Deriving it keeps the deprecation list in lockstep with the composites — a new
340
- * folded action automatically deprecates the tool it replaces.
340
+ * Operation names FOLDED by a composite, DERIVED from the composites: every op
341
+ * a composite folds had its standalone single-purpose tool REMOVED in v0.3.0
342
+ * (see docs/api-stability.md). This set is the do-not-register list
343
+ * `registerOperationTools` skips any op whose name is in it, since the op is
344
+ * only reachable through its composite (and through `canvas_batch`) now.
345
+ * Deriving it from the composites keeps suppression in lockstep with them — a
346
+ * newly folded action is automatically suppressed from standalone registration.
341
347
  */
342
- export function buildCompositeDeprecationNotes(
343
- definitions: CompositeToolDefinition[] = compositeToolDefinitions,
344
- ): Map<string, string> {
345
- const notes = new Map<string, string>();
348
+ export function compositeFoldedOpNames(definitions: CompositeToolDefinition[] = compositeToolDefinitions): Set<string> {
349
+ const names = new Set<string>();
346
350
  for (const def of definitions) {
347
- // Single-discriminator composites: one note per (action → op).
348
- for (const [action, opName] of Object.entries(def.actions)) {
349
- notes.set(opName, `Deprecated: use ${def.toolName} with action "${action}". `);
351
+ // Single-discriminator composites: the flat `actions` map values.
352
+ for (const opName of Object.values(def.actions)) {
353
+ names.add(opName);
350
354
  }
351
- // Two-discriminator composites (canvas_ax_gate): one note per member op,
352
- // each mapped back to its (kind, action) so the deprecation points exactly
353
- // at the composite invocation that replaces the legacy tool.
354
- if (def.memberOps && def.describeOp) {
355
+ // Two-discriminator composites (canvas_ax_gate): the explicit `memberOps`
356
+ // list (the flat `actions` map is empty for these).
357
+ if (def.memberOps) {
355
358
  for (const opName of def.memberOps) {
356
- const combo = def.describeOp(opName);
357
- if (!combo) continue;
358
- notes.set(
359
- opName,
360
- `Deprecated: use ${def.toolName} with kind "${combo.kind}" action "${combo.action}". `,
361
- );
359
+ names.add(opName);
362
360
  }
363
361
  }
364
362
  }
365
- return notes;
363
+ return names;
366
364
  }
@@ -38,7 +38,8 @@ function matchPath(template: string, pathname: string): Record<string, string> |
38
38
  /**
39
39
  * Shared body reader: preserves the parsed JSON value as-is (object, array,
40
40
  * or primitive) — per-op `readInput` decides what to do with non-object
41
- * bodies; the shared reader never coerces.
41
+ * bodies; the shared reader never coerces. A non-empty body that fails to
42
+ * parse is a 400 (OperationError), never a silent empty input.
42
43
  */
43
44
  export async function readJsonValue(req: Request): Promise<unknown> {
44
45
  let text = '';
@@ -51,7 +52,7 @@ export async function readJsonValue(req: Request): Promise<unknown> {
51
52
  try {
52
53
  return JSON.parse(text) as unknown;
53
54
  } catch {
54
- return undefined;
55
+ throw new OperationError('Malformed JSON body.');
55
56
  }
56
57
  }
57
58
 
@@ -68,9 +69,8 @@ async function defaultReadInput(
68
69
  return { ...query, ...params };
69
70
  }
70
71
  const body = await readJsonValue(req);
71
- const record = body !== null && typeof body === 'object' && !Array.isArray(body)
72
- ? body as Record<string, unknown>
73
- : {};
72
+ const record =
73
+ body !== null && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {};
74
74
  return { ...query, ...record, ...params };
75
75
  }
76
76
 
@@ -20,6 +20,9 @@ import { batchOperations } from './ops/batch.js';
20
20
  import { webviewOperations } from './ops/webview.js';
21
21
  import { appOperations } from './ops/app.js';
22
22
  import { intentOperations } from './ops/intent.js';
23
+ import { extAppOperations } from './ops/ext-app.js';
24
+ import { axReadOperations } from './ops/ax-read.js';
25
+ import { canvasWireOperations } from './ops/canvas-wire.js';
23
26
 
24
27
  for (const op of [
25
28
  ...nodeOperations,
@@ -39,6 +42,9 @@ for (const op of [
39
42
  ...webviewOperations,
40
43
  ...appOperations,
41
44
  ...intentOperations,
45
+ ...extAppOperations,
46
+ ...axReadOperations,
47
+ ...canvasWireOperations,
42
48
  ]) {
43
49
  registerOperation(op);
44
50
  }
@@ -53,7 +59,7 @@ export {
53
59
  export { dispatchOperationRoute } from './http.js';
54
60
  export { runCanvasBatchOperation, type BatchEnvelope } from './ops/batch.js';
55
61
  // OpenMcpAppCoreResult is the SDK's cast target for executeOperation('mcpapp.open').
56
- export { type OpenMcpAppCoreResult } from './ops/app.js';
62
+ export type { OpenMcpAppCoreResult } from './ops/app.js';
57
63
  export { LocalOperationInvoker, HttpOperationInvoker, type OperationInvoker } from './invoker.js';
58
64
  export { registerOperationTools, registerCompositeTools, type OperationToolHost } from './mcp.js';
59
65
  export { compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
@@ -77,9 +77,10 @@ export class HttpOperationInvoker implements OperationInvoker {
77
77
  }
78
78
  if (!response.ok) {
79
79
  if (route.errorBodyAsResult) return parsed;
80
- const message = parsed !== null && typeof parsed === 'object' && 'error' in parsed
81
- ? String((parsed as { error?: unknown }).error)
82
- : `HTTP ${response.status}`;
80
+ const message =
81
+ parsed !== null && typeof parsed === 'object' && 'error' in parsed
82
+ ? String((parsed as { error?: unknown }).error)
83
+ : `HTTP ${response.status}`;
83
84
  throw new OperationError(message, toOperationErrorStatus(response.status));
84
85
  }
85
86
  return parsed;
@@ -12,26 +12,24 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  import { getOperation, listOperations } from './registry.js';
13
13
  import type { OperationInvoker } from './invoker.js';
14
14
  import { OperationError, type OperationMcpToolHost } from './types.js';
15
- import { buildCompositeDeprecationNotes, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
15
+ import { compositeFoldedOpNames, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
16
16
 
17
17
  export interface OperationToolHost extends OperationMcpToolHost {
18
18
  invoker(): OperationInvoker;
19
19
  }
20
20
 
21
- export function registerOperationTools(
22
- server: McpServer,
23
- getHost: () => Promise<OperationToolHost>,
24
- ): void {
25
- // Legacy tools folded by a composite get a "Deprecated: use canvas_x …" prefix
26
- // (plan-006 step 2) so agents migrate during the v0.2 overlap window.
27
- const deprecations = buildCompositeDeprecationNotes();
21
+ export function registerOperationTools(server: McpServer, getHost: () => Promise<OperationToolHost>): void {
22
+ // Ops folded by a composite are NOT registered standalone: their legacy
23
+ // single-purpose tools were removed in v0.3.0 (docs/api-stability.md). The op
24
+ // itself is untouched — it stays reachable via its composite and canvas_batch.
25
+ const foldedOpNames = compositeFoldedOpNames();
28
26
  for (const op of listOperations()) {
27
+ if (foldedOpNames.has(op.name)) continue;
29
28
  const tool = op.mcp;
30
29
  if (!tool) continue;
31
- const note = deprecations.get(op.name);
32
30
  server.tool(
33
31
  tool.toolName,
34
- note ? note + tool.description : tool.description,
32
+ tool.description,
35
33
  { ...op.inputShape, ...(tool.extraShape ?? {}) },
36
34
  async (input: Record<string, unknown>) => {
37
35
  try {
@@ -190,32 +188,27 @@ export function registerCompositeTools(
190
188
  definitions: CompositeToolDefinition[] = compositeToolDefinitions,
191
189
  ): void {
192
190
  for (const def of definitions) {
193
- server.tool(
194
- def.toolName,
195
- def.description,
196
- buildCompositeShape(def),
197
- async (input: Record<string, unknown>) => {
198
- try {
199
- const host = await getHost();
200
- const opName = resolveCompositeOp(def, input);
201
- const op = getOperation(opName);
202
- // Strip the composite discriminators (action + any extra, e.g. `kind`)
203
- // and undo any field remap; the rest is the op's raw MCP args — the same
204
- // value the standalone tool would receive.
205
- const rest = stripCompositeDiscriminators(def, input);
206
- const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
207
- const result = await host.invoker().invoke(opName, opInput);
208
- if (op.mcp?.formatResult) {
209
- return await op.mcp.formatResult(result, rest, host);
210
- }
211
- return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
212
- } catch (error) {
213
- return {
214
- content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
215
- isError: true,
216
- };
191
+ server.tool(def.toolName, def.description, buildCompositeShape(def), async (input: Record<string, unknown>) => {
192
+ try {
193
+ const host = await getHost();
194
+ const opName = resolveCompositeOp(def, input);
195
+ const op = getOperation(opName);
196
+ // Strip the composite discriminators (action + any extra, e.g. `kind`)
197
+ // and undo any field remap; the rest is the op's raw MCP args — the same
198
+ // value the standalone tool would receive.
199
+ const rest = stripCompositeDiscriminators(def, input);
200
+ const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
201
+ const result = await host.invoker().invoke(opName, opInput);
202
+ if (op.mcp?.formatResult) {
203
+ return await op.mcp.formatResult(result, rest, host);
217
204
  }
218
- },
219
- );
205
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
206
+ } catch (error) {
207
+ return {
208
+ content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
209
+ isError: true,
210
+ };
211
+ }
212
+ });
220
213
  }
221
214
  }