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.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- 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
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
318
|
-
|
|
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(
|
|
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
|
|
13
|
-
* v0.2 alongside the legacy single-purpose tools
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
34
|
-
*
|
|
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
|
-
*
|
|
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
|
|
82
|
-
*
|
|
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
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* Deriving it
|
|
340
|
-
* folded action automatically
|
|
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
|
|
343
|
-
|
|
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:
|
|
348
|
-
for (const
|
|
349
|
-
|
|
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):
|
|
352
|
-
//
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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 =
|
|
81
|
-
|
|
82
|
-
|
|
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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
}
|