pmx-canvas 0.3.0 → 0.3.2
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 +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -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/ExtAppFrame.d.ts +9 -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/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- 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/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- 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 +194 -80
- 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 +35 -21
- package/src/mcp/server.ts +132 -70
- 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 +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- 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 +320 -217
- package/src/server/intent-registry.ts +2 -5
- 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 +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- 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 +41 -15
- 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 +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- 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 +122 -1028
- 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/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
|
@@ -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
|
}
|
|
@@ -345,9 +345,7 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
345
345
|
* Deriving it from the composites keeps suppression in lockstep with them — a
|
|
346
346
|
* newly folded action is automatically suppressed from standalone registration.
|
|
347
347
|
*/
|
|
348
|
-
export function compositeFoldedOpNames(
|
|
349
|
-
definitions: CompositeToolDefinition[] = compositeToolDefinitions,
|
|
350
|
-
): Set<string> {
|
|
348
|
+
export function compositeFoldedOpNames(definitions: CompositeToolDefinition[] = compositeToolDefinitions): Set<string> {
|
|
351
349
|
const names = new Set<string>();
|
|
352
350
|
for (const def of definitions) {
|
|
353
351
|
// Single-discriminator composites: the flat `actions` map values.
|
|
@@ -69,9 +69,8 @@ async function defaultReadInput(
|
|
|
69
69
|
return { ...query, ...params };
|
|
70
70
|
}
|
|
71
71
|
const body = await readJsonValue(req);
|
|
72
|
-
const record =
|
|
73
|
-
? body as Record<string, unknown>
|
|
74
|
-
: {};
|
|
72
|
+
const record =
|
|
73
|
+
body !== null && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {};
|
|
75
74
|
return { ...query, ...record, ...params };
|
|
76
75
|
}
|
|
77
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;
|
|
@@ -18,10 +18,7 @@ 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 {
|
|
21
|
+
export function registerOperationTools(server: McpServer, getHost: () => Promise<OperationToolHost>): void {
|
|
25
22
|
// Ops folded by a composite are NOT registered standalone: their legacy
|
|
26
23
|
// single-purpose tools were removed in v0.3.0 (docs/api-stability.md). The op
|
|
27
24
|
// itself is untouched — it stays reachable via its composite and canvas_batch.
|
|
@@ -191,32 +188,27 @@ export function registerCompositeTools(
|
|
|
191
188
|
definitions: CompositeToolDefinition[] = compositeToolDefinitions,
|
|
192
189
|
): void {
|
|
193
190
|
for (const def of definitions) {
|
|
194
|
-
server.tool(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
|
|
208
|
-
const result = await host.invoker().invoke(opName, opInput);
|
|
209
|
-
if (op.mcp?.formatResult) {
|
|
210
|
-
return await op.mcp.formatResult(result, rest, host);
|
|
211
|
-
}
|
|
212
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
213
|
-
} catch (error) {
|
|
214
|
-
return {
|
|
215
|
-
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
|
|
216
|
-
isError: true,
|
|
217
|
-
};
|
|
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);
|
|
218
204
|
}
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
});
|
|
221
213
|
}
|
|
222
214
|
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Annotation
|
|
2
|
+
* Annotation ops: annotation.remove (plan-008 Wave 1) and annotation.add
|
|
3
|
+
* (plan-009 C1 slice 3 — the legacy POST /api/canvas/annotation handler,
|
|
4
|
+
* wire-identical).
|
|
3
5
|
*
|
|
4
|
-
* DELETE-by-id of a human-drawn canvas annotation.
|
|
5
|
-
* auto-emits one canvas-layout-update after
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* annotation.remove: DELETE-by-id of a human-drawn canvas annotation.
|
|
7
|
+
* mutates: true → the registry auto-emits one canvas-layout-update after
|
|
8
|
+
* success, matching the legacy handler. A non-existent id is a 404
|
|
9
|
+
* OperationError carrying the exact legacy message; an omitted id is a 400 so
|
|
10
|
+
* composite callers get a loud input error instead of a misleading "not
|
|
11
|
+
* found". HTTP renders errors as { ok:false, error } and MCP renders them as
|
|
12
|
+
* isError tool results.
|
|
11
13
|
*
|
|
12
14
|
* This module must never import server.ts or index.ts.
|
|
13
15
|
*/
|
|
14
16
|
import { z } from 'zod';
|
|
15
|
-
import { canvasState } from '../../canvas-state.js';
|
|
17
|
+
import { canvasState, type CanvasAnnotation } from '../../canvas-state.js';
|
|
18
|
+
import { summarizeCanvasAnnotation } from '../../canvas-serialization.js';
|
|
16
19
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
17
20
|
|
|
18
21
|
const annotationRemoveShape = {
|
|
@@ -57,4 +60,113 @@ const annotationRemoveOperation = defineOperation<z.infer<typeof annotationRemov
|
|
|
57
60
|
},
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
// ── annotation.add (helpers moved from server.ts) ─────────────
|
|
64
|
+
|
|
65
|
+
function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation['bounds'] {
|
|
66
|
+
const xs = points.map((point) => point.x);
|
|
67
|
+
const ys = points.map((point) => point.y);
|
|
68
|
+
const minX = Math.min(...xs);
|
|
69
|
+
const minY = Math.min(...ys);
|
|
70
|
+
const maxX = Math.max(...xs);
|
|
71
|
+
const maxY = Math.max(...ys);
|
|
72
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function textAnnotationBounds(
|
|
76
|
+
point: CanvasAnnotation['points'][number],
|
|
77
|
+
text: string,
|
|
78
|
+
width: number,
|
|
79
|
+
): CanvasAnnotation['bounds'] {
|
|
80
|
+
return {
|
|
81
|
+
x: point.x,
|
|
82
|
+
y: point.y - width,
|
|
83
|
+
width: Math.max(width, text.length * width * 0.62),
|
|
84
|
+
height: width * 1.2,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
89
|
+
if (!Array.isArray(value)) return [];
|
|
90
|
+
return value
|
|
91
|
+
.map((point) => {
|
|
92
|
+
if (!point || typeof point !== 'object' || Array.isArray(point)) return null;
|
|
93
|
+
const record = point as Record<string, unknown>;
|
|
94
|
+
if (typeof record.x !== 'number' || typeof record.y !== 'number') return null;
|
|
95
|
+
if (!Number.isFinite(record.x) || !Number.isFinite(record.y)) return null;
|
|
96
|
+
return { x: record.x, y: record.y };
|
|
97
|
+
})
|
|
98
|
+
.filter((point): point is CanvasAnnotation['points'][number] => point !== null);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const annotationAddShape = {
|
|
102
|
+
type: z.unknown().optional().describe("Annotation type: 'freehand' (default) or 'text'"),
|
|
103
|
+
points: z.unknown().optional().describe('Annotation points: [{ x, y }, …]'),
|
|
104
|
+
width: z.unknown().optional().describe('Stroke width (freehand) or font size (text)'),
|
|
105
|
+
color: z.unknown().optional().describe("'currentColor' or a #rrggbb hex color"),
|
|
106
|
+
label: z.unknown().optional().describe('Short annotation label'),
|
|
107
|
+
text: z.unknown().optional().describe('Text annotation content'),
|
|
108
|
+
id: z.unknown().optional().describe('Explicit annotation id'),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const annotationAddSchema = z.looseObject(annotationAddShape);
|
|
112
|
+
|
|
113
|
+
const annotationAddOperation = defineOperation<z.infer<typeof annotationAddSchema>, Record<string, unknown>>({
|
|
114
|
+
name: 'annotation.add',
|
|
115
|
+
mutates: true,
|
|
116
|
+
input: annotationAddSchema,
|
|
117
|
+
inputShape: annotationAddShape,
|
|
118
|
+
http: {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
path: '/api/canvas/annotation',
|
|
121
|
+
},
|
|
122
|
+
handler: (input) => {
|
|
123
|
+
const body: Record<string, unknown> = input;
|
|
124
|
+
const type = body.type === 'text' ? 'text' : 'freehand';
|
|
125
|
+
const points = parseAnnotationPoints(body.points);
|
|
126
|
+
if (points.length < (type === 'text' ? 1 : 2)) {
|
|
127
|
+
throw new OperationError(
|
|
128
|
+
type === 'text' ? 'Text annotation requires a valid point.' : 'Annotation requires at least two valid points.',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultWidth = type === 'text' ? 24 : 4;
|
|
133
|
+
const maxWidth = type === 'text' ? 96 : 24;
|
|
134
|
+
const width =
|
|
135
|
+
typeof body.width === 'number' && Number.isFinite(body.width)
|
|
136
|
+
? Math.min(maxWidth, Math.max(1, body.width))
|
|
137
|
+
: defaultWidth;
|
|
138
|
+
const color =
|
|
139
|
+
typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
|
|
140
|
+
? body.color
|
|
141
|
+
: 'currentColor';
|
|
142
|
+
const label =
|
|
143
|
+
typeof body.label === 'string' && body.label.trim().length > 0 ? body.label.trim().slice(0, 160) : undefined;
|
|
144
|
+
const text =
|
|
145
|
+
type === 'text' && typeof body.text === 'string' && body.text.trim().length > 0
|
|
146
|
+
? body.text.trim().slice(0, 240)
|
|
147
|
+
: undefined;
|
|
148
|
+
if (type === 'text' && !text) {
|
|
149
|
+
throw new OperationError('Text annotation requires text.');
|
|
150
|
+
}
|
|
151
|
+
const id =
|
|
152
|
+
typeof body.id === 'string' && body.id.trim().length > 0
|
|
153
|
+
? body.id.trim().slice(0, 120)
|
|
154
|
+
: `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
155
|
+
const annotation: CanvasAnnotation = {
|
|
156
|
+
id,
|
|
157
|
+
type,
|
|
158
|
+
points,
|
|
159
|
+
bounds: type === 'text' ? textAnnotationBounds(points[0]!, text!, width) : annotationBounds(points),
|
|
160
|
+
color,
|
|
161
|
+
width,
|
|
162
|
+
...(text ? { text } : {}),
|
|
163
|
+
...((label ?? text) ? { label: label ?? text } : {}),
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
canvasState.addAnnotation(annotation);
|
|
168
|
+
return { ok: true, annotation: summarizeCanvasAnnotation(annotation) };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const annotationOperations: Operation[] = [annotationRemoveOperation, annotationAddOperation];
|