pmx-canvas 0.1.11 → 0.1.13
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 +130 -0
- package/dist/canvas/index.js +34 -34
- package/dist/types/client/nodes/trace-model.d.ts +9 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +88 -0
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +1 -0
- package/package.json +1 -1
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +9 -8
- package/src/client/nodes/TraceNode.tsx +2 -6
- package/src/client/nodes/trace-model.ts +19 -0
- package/src/client/state/canvas-store.ts +5 -2
- package/src/client/state/sse-bridge.ts +2 -1
- package/src/mcp/canvas-access.ts +676 -0
- package/src/mcp/server.ts +184 -97
- package/src/server/canvas-operations.ts +1 -0
- package/src/server/canvas-schema.ts +11 -0
- package/src/server/diagram-presets.ts +6 -28
- package/src/server/index.ts +2 -2
- package/src/server/server.ts +5 -1
- package/src/server/web-artifacts/scripts/init-artifact.sh +9 -8
- package/src/server/web-artifacts.ts +14 -1
package/src/mcp/server.ts
CHANGED
|
@@ -24,22 +24,18 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
24
24
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
25
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
26
26
|
import { z } from 'zod';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
canvasState,
|
|
30
|
-
describeCanvasSchema,
|
|
31
|
-
validateStructuredCanvasPayload,
|
|
32
|
-
type PmxCanvas,
|
|
33
|
-
} from '../server/index.js';
|
|
27
|
+
import { canvasState, describeCanvasSchema, validateStructuredCanvasPayload } from '../server/index.js';
|
|
28
|
+
import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
34
29
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import { buildCodeGraphSummary, formatCodeGraph } from '../server/code-graph.js';
|
|
39
|
-
import { buildCanvasSummary, serializeCanvasLayout, serializeCanvasNode } from '../server/canvas-serialization.js';
|
|
30
|
+
import { wrapCanvasAutomationScript } from '../server/server.js';
|
|
31
|
+
import { buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
|
|
32
|
+
import { getCanvasNodeTitle, serializeCanvasLayout, serializeCanvasNode } from '../server/canvas-serialization.js';
|
|
40
33
|
import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js';
|
|
41
34
|
|
|
42
|
-
let canvas:
|
|
35
|
+
let canvas: CanvasAccess | null = null;
|
|
36
|
+
let resourceNotificationServer: McpServer | null = null;
|
|
37
|
+
let localResourceNotificationsStarted = false;
|
|
38
|
+
let remoteResourceNotificationsBaseUrl: string | null = null;
|
|
43
39
|
|
|
44
40
|
const jsonRenderSpecSchema = z.union([
|
|
45
41
|
z.object({
|
|
@@ -80,31 +76,140 @@ function safeWorkspacePath(pathLike: string): string {
|
|
|
80
76
|
return resolved;
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
async function ensureCanvas(): Promise<
|
|
79
|
+
async function ensureCanvas(): Promise<CanvasAccess> {
|
|
84
80
|
if (!canvas) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
canvas = await createCanvasAccess();
|
|
82
|
+
} else {
|
|
83
|
+
canvas = await refreshCanvasAccess(canvas);
|
|
88
84
|
}
|
|
85
|
+
startResourceNotifications(canvas);
|
|
89
86
|
return canvas;
|
|
90
87
|
}
|
|
91
88
|
|
|
89
|
+
function sleep(ms: number): Promise<void> {
|
|
90
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sendCanvasResourceNotifications(type: 'nodes' | 'pins' = 'nodes'): void {
|
|
94
|
+
const server = resourceNotificationServer;
|
|
95
|
+
if (!server) return;
|
|
96
|
+
try {
|
|
97
|
+
if (type === 'pins') {
|
|
98
|
+
server.server.sendResourceUpdated({ uri: 'canvas://pinned-context' });
|
|
99
|
+
}
|
|
100
|
+
server.server.sendResourceUpdated({ uri: 'canvas://layout' });
|
|
101
|
+
server.server.sendResourceUpdated({ uri: 'canvas://summary' });
|
|
102
|
+
server.server.sendResourceUpdated({ uri: 'canvas://spatial-context' });
|
|
103
|
+
server.server.sendResourceUpdated({ uri: 'canvas://history' });
|
|
104
|
+
server.server.sendResourceUpdated({ uri: 'canvas://code-graph' });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.debug('[mcp] resource notification failed', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleRemoteSseFrame(frame: string): void {
|
|
111
|
+
const eventLine = frame.split('\n').find((line) => line.startsWith('event: '));
|
|
112
|
+
const event = eventLine?.slice('event: '.length).trim() ?? '';
|
|
113
|
+
if (!event || event === 'connected' || event === 'ping') return;
|
|
114
|
+
sendCanvasResourceNotifications(event === 'context-pins-changed' ? 'pins' : 'nodes');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function watchRemoteCanvasEvents(baseUrl: string): Promise<void> {
|
|
118
|
+
const decoder = new TextDecoder();
|
|
119
|
+
while (true) {
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`${baseUrl}/api/workbench/events`);
|
|
122
|
+
if (!response.ok || !response.body) {
|
|
123
|
+
await sleep(1_000);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reader = response.body.getReader();
|
|
128
|
+
let buffer = '';
|
|
129
|
+
while (true) {
|
|
130
|
+
const { done, value } = await reader.read();
|
|
131
|
+
if (done) break;
|
|
132
|
+
buffer += decoder.decode(value, { stream: true });
|
|
133
|
+
const frames = buffer.split('\n\n');
|
|
134
|
+
buffer = frames.pop() ?? '';
|
|
135
|
+
for (const frame of frames) handleRemoteSseFrame(frame);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.debug('[mcp] remote canvas event stream failed', error);
|
|
139
|
+
}
|
|
140
|
+
await sleep(1_000);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function startResourceNotifications(c: CanvasAccess): void {
|
|
145
|
+
const server = resourceNotificationServer;
|
|
146
|
+
if (!server) return;
|
|
147
|
+
|
|
148
|
+
if (c.remoteBaseUrl) {
|
|
149
|
+
if (remoteResourceNotificationsBaseUrl !== c.remoteBaseUrl) {
|
|
150
|
+
remoteResourceNotificationsBaseUrl = c.remoteBaseUrl;
|
|
151
|
+
void watchRemoteCanvasEvents(c.remoteBaseUrl);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (localResourceNotificationsStarted) return;
|
|
157
|
+
localResourceNotificationsStarted = true;
|
|
158
|
+
|
|
159
|
+
canvasState.onChange((type) => {
|
|
160
|
+
sendCanvasResourceNotifications(type);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
92
164
|
function encodeBase64(bytes: Uint8Array): string {
|
|
93
165
|
return Buffer.from(bytes).toString('base64');
|
|
94
166
|
}
|
|
95
167
|
|
|
96
|
-
function createdNodePayload(c:
|
|
97
|
-
const node = c.getNode(id);
|
|
168
|
+
async function createdNodePayload(c: CanvasAccess, id: string): Promise<Record<string, unknown>> {
|
|
169
|
+
const node = await c.getNode(id);
|
|
98
170
|
if (!node) return { ok: true, id };
|
|
99
171
|
const serialized = serializeCanvasNode(node);
|
|
100
172
|
return { ok: true, node: serialized, ...serialized };
|
|
101
173
|
}
|
|
102
174
|
|
|
175
|
+
function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>, pinnedIds: string[]): Record<string, unknown> {
|
|
176
|
+
const pinned = new Set(pinnedIds);
|
|
177
|
+
const nodesByType: Record<string, number> = {};
|
|
178
|
+
const pinnedTitles: string[] = [];
|
|
179
|
+
for (const node of layout.nodes) {
|
|
180
|
+
const serialized = serializeCanvasNode(node);
|
|
181
|
+
nodesByType[serialized.kind] = (nodesByType[serialized.kind] ?? 0) + 1;
|
|
182
|
+
if (pinned.has(node.id)) pinnedTitles.push(getCanvasNodeTitle(node) ?? node.id);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
totalNodes: layout.nodes.length,
|
|
186
|
+
totalEdges: layout.edges.length,
|
|
187
|
+
nodesByType,
|
|
188
|
+
pinnedCount: pinned.size,
|
|
189
|
+
pinnedTitles,
|
|
190
|
+
viewport: layout.viewport,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildSnapshotRestoreSummary(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>): Record<string, unknown> {
|
|
195
|
+
const nodesByType: Record<string, number> = {};
|
|
196
|
+
for (const node of layout.nodes) {
|
|
197
|
+
nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
nodeCount: layout.nodes.length,
|
|
201
|
+
edgeCount: layout.edges.length,
|
|
202
|
+
nodesByType,
|
|
203
|
+
viewport: layout.viewport,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
103
207
|
export async function startMcpServer(): Promise<void> {
|
|
104
208
|
const server = new McpServer({
|
|
105
209
|
name: 'pmx-canvas',
|
|
106
210
|
version: '0.1.0',
|
|
107
211
|
});
|
|
212
|
+
resourceNotificationServer = server;
|
|
108
213
|
|
|
109
214
|
// ── canvas_get_layout ──────────────────────────────────────────
|
|
110
215
|
server.tool(
|
|
@@ -113,7 +218,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
113
218
|
{},
|
|
114
219
|
async () => {
|
|
115
220
|
const c = await ensureCanvas();
|
|
116
|
-
const layout = serializeCanvasLayout(c.getLayout());
|
|
221
|
+
const layout = serializeCanvasLayout(await c.getLayout());
|
|
117
222
|
return {
|
|
118
223
|
content: [{ type: 'text', text: JSON.stringify(layout, null, 2) }],
|
|
119
224
|
};
|
|
@@ -127,7 +232,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
127
232
|
{ id: z.string().describe('The node ID to retrieve') },
|
|
128
233
|
async ({ id }) => {
|
|
129
234
|
const c = await ensureCanvas();
|
|
130
|
-
const node = c.getNode(id);
|
|
235
|
+
const node = await c.getNode(id);
|
|
131
236
|
if (!node) {
|
|
132
237
|
return {
|
|
133
238
|
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
@@ -184,9 +289,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
184
289
|
const nodeInput = input.type === 'image' && input.path && !input.content
|
|
185
290
|
? { ...input, content: input.path }
|
|
186
291
|
: input;
|
|
187
|
-
const id = c.addNode(nodeInput);
|
|
292
|
+
const id = await c.addNode(nodeInput);
|
|
188
293
|
return {
|
|
189
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
294
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
190
295
|
};
|
|
191
296
|
},
|
|
192
297
|
);
|
|
@@ -378,7 +483,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
378
483
|
// ── canvas_build_web_artifact ───────────────────────────────
|
|
379
484
|
server.tool(
|
|
380
485
|
'canvas_build_web_artifact',
|
|
381
|
-
'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds
|
|
486
|
+
'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds can exceed default 60s MCP client timeouts on cold workspaces; set a long client timeout or retry with the same projectPath/outputPath if the client times out. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
|
|
382
487
|
{
|
|
383
488
|
title: z.string().describe('Artifact title used for default project and output paths'),
|
|
384
489
|
appTsx: z.string().describe('Contents for src/App.tsx'),
|
|
@@ -429,6 +534,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
429
534
|
bytes: result.fileSize,
|
|
430
535
|
projectPath: result.projectPath,
|
|
431
536
|
openedInCanvas: result.openedInCanvas,
|
|
537
|
+
completedAt: result.completedAt,
|
|
432
538
|
// `id` only present when a canvas node was actually created.
|
|
433
539
|
// See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
|
|
434
540
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
@@ -468,7 +574,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
468
574
|
async (input) => {
|
|
469
575
|
const c = await ensureCanvas();
|
|
470
576
|
try {
|
|
471
|
-
const result = c.addJsonRenderNode({
|
|
577
|
+
const result = await c.addJsonRenderNode({
|
|
472
578
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
473
579
|
spec: input.spec,
|
|
474
580
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
@@ -481,7 +587,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
481
587
|
content: [{
|
|
482
588
|
type: 'text',
|
|
483
589
|
text: JSON.stringify({
|
|
484
|
-
...createdNodePayload(c, result.id),
|
|
590
|
+
...await createdNodePayload(c, result.id),
|
|
485
591
|
url: result.url,
|
|
486
592
|
spec: result.spec,
|
|
487
593
|
}, null, 2),
|
|
@@ -530,7 +636,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
530
636
|
async (input) => {
|
|
531
637
|
const c = await ensureCanvas();
|
|
532
638
|
try {
|
|
533
|
-
const result = c.addGraphNode({
|
|
639
|
+
const result = await c.addGraphNode({
|
|
534
640
|
graphType: input.graphType,
|
|
535
641
|
data: input.data,
|
|
536
642
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
@@ -561,7 +667,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
561
667
|
content: [{
|
|
562
668
|
type: 'text',
|
|
563
669
|
text: JSON.stringify({
|
|
564
|
-
...createdNodePayload(c, result.id),
|
|
670
|
+
...await createdNodePayload(c, result.id),
|
|
565
671
|
url: result.url,
|
|
566
672
|
spec: result.spec,
|
|
567
673
|
}, null, 2),
|
|
@@ -599,7 +705,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
599
705
|
},
|
|
600
706
|
async ({ id, title, content, x, y, width, height, spec, graphType, data, xKey, yKey, chartHeight, collapsed, arrangeLocked }) => {
|
|
601
707
|
const c = await ensureCanvas();
|
|
602
|
-
const node = c.getNode(id);
|
|
708
|
+
const node = await c.getNode(id);
|
|
603
709
|
if (!node) {
|
|
604
710
|
return {
|
|
605
711
|
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
@@ -627,10 +733,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
627
733
|
if (arrangeLocked !== undefined) {
|
|
628
734
|
patch.arrangeLocked = arrangeLocked;
|
|
629
735
|
}
|
|
630
|
-
c.updateNode(id, patch);
|
|
631
|
-
const updated = c.getNode(id);
|
|
736
|
+
await c.updateNode(id, patch);
|
|
737
|
+
const updated = await c.getNode(id);
|
|
632
738
|
return {
|
|
633
|
-
content: [{ type: 'text', text: JSON.stringify(updated ? createdNodePayload(c, id) : { ok: true, id }, null, 2) }],
|
|
739
|
+
content: [{ type: 'text', text: JSON.stringify(updated ? await createdNodePayload(c, id) : { ok: true, id }, null, 2) }],
|
|
634
740
|
};
|
|
635
741
|
},
|
|
636
742
|
);
|
|
@@ -642,7 +748,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
642
748
|
{ id: z.string().describe('Node ID to remove') },
|
|
643
749
|
async ({ id }) => {
|
|
644
750
|
const c = await ensureCanvas();
|
|
645
|
-
c.removeNode(id);
|
|
751
|
+
await c.removeNode(id);
|
|
646
752
|
return {
|
|
647
753
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
648
754
|
};
|
|
@@ -678,8 +784,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
678
784
|
};
|
|
679
785
|
}
|
|
680
786
|
try {
|
|
681
|
-
const id = c.addEdge(input);
|
|
682
|
-
const edge = c.getLayout().edges.find((entry) => entry.id === id);
|
|
787
|
+
const id = await c.addEdge(input);
|
|
788
|
+
const edge = (await c.getLayout()).edges.find((entry) => entry.id === id);
|
|
683
789
|
return {
|
|
684
790
|
content: [{
|
|
685
791
|
type: 'text',
|
|
@@ -702,7 +808,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
702
808
|
{ id: z.string().describe('Edge ID to remove') },
|
|
703
809
|
async ({ id }) => {
|
|
704
810
|
const c = await ensureCanvas();
|
|
705
|
-
c.removeEdge(id);
|
|
811
|
+
await c.removeEdge(id);
|
|
706
812
|
return {
|
|
707
813
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
708
814
|
};
|
|
@@ -718,7 +824,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
718
824
|
},
|
|
719
825
|
async ({ layout }) => {
|
|
720
826
|
const c = await ensureCanvas();
|
|
721
|
-
c.arrange(layout ?? 'grid');
|
|
827
|
+
await c.arrange(layout ?? 'grid');
|
|
722
828
|
return {
|
|
723
829
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: layout ?? 'grid' }) }],
|
|
724
830
|
};
|
|
@@ -738,7 +844,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
738
844
|
},
|
|
739
845
|
async ({ id, noPan }) => {
|
|
740
846
|
const c = await ensureCanvas();
|
|
741
|
-
const result = c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
847
|
+
const result = await c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
742
848
|
if (!result) {
|
|
743
849
|
return {
|
|
744
850
|
content: [
|
|
@@ -772,7 +878,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
772
878
|
},
|
|
773
879
|
async (input) => {
|
|
774
880
|
const c = await ensureCanvas();
|
|
775
|
-
const result = c.fitView({
|
|
881
|
+
const result = await c.fitView({
|
|
776
882
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
777
883
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
778
884
|
...(typeof input.padding === 'number' ? { padding: input.padding } : {}),
|
|
@@ -792,7 +898,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
792
898
|
{},
|
|
793
899
|
async () => {
|
|
794
900
|
const c = await ensureCanvas();
|
|
795
|
-
c.clear();
|
|
901
|
+
await c.clear();
|
|
796
902
|
return {
|
|
797
903
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, cleared: true }) }],
|
|
798
904
|
};
|
|
@@ -808,8 +914,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
808
914
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
809
915
|
},
|
|
810
916
|
async ({ query, limit }) => {
|
|
811
|
-
await ensureCanvas();
|
|
812
|
-
const results =
|
|
917
|
+
const c = await ensureCanvas();
|
|
918
|
+
const results = await c.search(query);
|
|
813
919
|
const capped = results.slice(0, limit ?? 10);
|
|
814
920
|
return {
|
|
815
921
|
content: [{
|
|
@@ -828,8 +934,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
828
934
|
async () => {
|
|
829
935
|
const c = await ensureCanvas();
|
|
830
936
|
const result = await c.undo();
|
|
937
|
+
const history = await c.getHistory();
|
|
831
938
|
return {
|
|
832
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
939
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
833
940
|
};
|
|
834
941
|
},
|
|
835
942
|
);
|
|
@@ -842,8 +949,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
842
949
|
async () => {
|
|
843
950
|
const c = await ensureCanvas();
|
|
844
951
|
const result = await c.redo();
|
|
952
|
+
const history = await c.getHistory();
|
|
845
953
|
return {
|
|
846
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
954
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
847
955
|
};
|
|
848
956
|
},
|
|
849
957
|
);
|
|
@@ -856,15 +964,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
856
964
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
857
965
|
},
|
|
858
966
|
async ({ snapshot }) => {
|
|
859
|
-
await ensureCanvas();
|
|
860
|
-
const
|
|
861
|
-
if (!
|
|
967
|
+
const c = await ensureCanvas();
|
|
968
|
+
const result = await c.diffSnapshot(snapshot);
|
|
969
|
+
if (!result.ok) {
|
|
862
970
|
return { content: [{ type: 'text', text: `Snapshot "${snapshot}" not found. Use canvas_snapshot to save one first.` }], isError: true };
|
|
863
971
|
}
|
|
864
|
-
const current = canvasState.getLayout();
|
|
865
|
-
const diff = diffLayouts(snapData.name, snapData, current);
|
|
866
972
|
return {
|
|
867
|
-
content: [{ type: 'text', text:
|
|
973
|
+
content: [{ type: 'text', text: result.text ?? '' }],
|
|
868
974
|
};
|
|
869
975
|
},
|
|
870
976
|
);
|
|
@@ -877,7 +983,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
877
983
|
async () => {
|
|
878
984
|
const c = await ensureCanvas();
|
|
879
985
|
return {
|
|
880
|
-
content: [{ type: 'text', text: JSON.stringify(c.getAutomationWebViewStatus(), null, 2) }],
|
|
986
|
+
content: [{ type: 'text', text: JSON.stringify(await c.getAutomationWebViewStatus(), null, 2) }],
|
|
881
987
|
};
|
|
882
988
|
},
|
|
883
989
|
);
|
|
@@ -927,13 +1033,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
927
1033
|
const c = await ensureCanvas();
|
|
928
1034
|
try {
|
|
929
1035
|
const stopped = await c.stopAutomationWebView();
|
|
1036
|
+
const webview = await c.getAutomationWebViewStatus();
|
|
930
1037
|
return {
|
|
931
1038
|
content: [{
|
|
932
1039
|
type: 'text',
|
|
933
1040
|
text: JSON.stringify({
|
|
934
1041
|
ok: true,
|
|
935
1042
|
stopped,
|
|
936
|
-
webview
|
|
1043
|
+
webview,
|
|
937
1044
|
}, null, 2),
|
|
938
1045
|
}],
|
|
939
1046
|
};
|
|
@@ -1017,7 +1124,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1017
1124
|
...(format ? { format } : {}),
|
|
1018
1125
|
...(typeof quality === 'number' ? { quality } : {}),
|
|
1019
1126
|
});
|
|
1020
|
-
const status = c.getAutomationWebViewStatus();
|
|
1127
|
+
const status = await c.getAutomationWebViewStatus();
|
|
1021
1128
|
return {
|
|
1022
1129
|
content: [
|
|
1023
1130
|
{
|
|
@@ -1092,8 +1199,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1092
1199
|
},
|
|
1093
1200
|
async () => {
|
|
1094
1201
|
const c = await ensureCanvas();
|
|
1095
|
-
const pinnedIds =
|
|
1096
|
-
const layout = c.getLayout();
|
|
1202
|
+
const pinnedIds = new Set(await c.getPinnedNodeIds());
|
|
1203
|
+
const layout = await c.getLayout();
|
|
1097
1204
|
|
|
1098
1205
|
const pinnedNodes = layout.nodes.filter((n) => pinnedIds.has(n.id));
|
|
1099
1206
|
const pinnedEdges = layout.edges.filter(
|
|
@@ -1147,7 +1254,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1147
1254
|
},
|
|
1148
1255
|
async () => {
|
|
1149
1256
|
const c = await ensureCanvas();
|
|
1150
|
-
const layout = serializeCanvasLayout(c.getLayout());
|
|
1257
|
+
const layout = serializeCanvasLayout(await c.getLayout());
|
|
1151
1258
|
return {
|
|
1152
1259
|
contents: [
|
|
1153
1260
|
{
|
|
@@ -1170,13 +1277,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1170
1277
|
mimeType: 'application/json',
|
|
1171
1278
|
},
|
|
1172
1279
|
async () => {
|
|
1173
|
-
await ensureCanvas();
|
|
1280
|
+
const c = await ensureCanvas();
|
|
1174
1281
|
return {
|
|
1175
1282
|
contents: [
|
|
1176
1283
|
{
|
|
1177
1284
|
uri: 'canvas://summary',
|
|
1178
1285
|
mimeType: 'application/json',
|
|
1179
|
-
text: JSON.stringify(
|
|
1286
|
+
text: JSON.stringify(buildSummaryFromLayout(await c.getLayout(), await c.getPinnedNodeIds()), null, 2),
|
|
1180
1287
|
},
|
|
1181
1288
|
],
|
|
1182
1289
|
};
|
|
@@ -1196,9 +1303,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1196
1303
|
mimeType: 'application/json',
|
|
1197
1304
|
},
|
|
1198
1305
|
async () => {
|
|
1199
|
-
await ensureCanvas();
|
|
1200
|
-
const layout =
|
|
1201
|
-
const spatial = buildSpatialContext(layout.nodes, layout.edges,
|
|
1306
|
+
const c = await ensureCanvas();
|
|
1307
|
+
const layout = await c.getLayout();
|
|
1308
|
+
const spatial = buildSpatialContext(layout.nodes, layout.edges, new Set(await c.getPinnedNodeIds()));
|
|
1202
1309
|
return {
|
|
1203
1310
|
contents: [
|
|
1204
1311
|
{
|
|
@@ -1222,13 +1329,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1222
1329
|
mimeType: 'text/plain',
|
|
1223
1330
|
},
|
|
1224
1331
|
async () => {
|
|
1225
|
-
await ensureCanvas();
|
|
1332
|
+
const c = await ensureCanvas();
|
|
1226
1333
|
return {
|
|
1227
1334
|
contents: [
|
|
1228
1335
|
{
|
|
1229
1336
|
uri: 'canvas://history',
|
|
1230
1337
|
mimeType: 'text/plain',
|
|
1231
|
-
text:
|
|
1338
|
+
text: (await c.getHistory()).text,
|
|
1232
1339
|
},
|
|
1233
1340
|
],
|
|
1234
1341
|
};
|
|
@@ -1247,8 +1354,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1247
1354
|
mimeType: 'application/json',
|
|
1248
1355
|
},
|
|
1249
1356
|
async () => {
|
|
1250
|
-
await ensureCanvas();
|
|
1251
|
-
const summary =
|
|
1357
|
+
const c = await ensureCanvas();
|
|
1358
|
+
const summary = (await c.getCodeGraph()).summary;
|
|
1252
1359
|
return {
|
|
1253
1360
|
contents: [
|
|
1254
1361
|
{
|
|
@@ -1339,9 +1446,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1339
1446
|
},
|
|
1340
1447
|
async (input) => {
|
|
1341
1448
|
const c = await ensureCanvas();
|
|
1342
|
-
const id = c.createGroup(input);
|
|
1449
|
+
const id = await c.createGroup(input);
|
|
1343
1450
|
return {
|
|
1344
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
1451
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
1345
1452
|
};
|
|
1346
1453
|
},
|
|
1347
1454
|
);
|
|
@@ -1357,7 +1464,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1357
1464
|
},
|
|
1358
1465
|
async ({ groupId, childIds, childLayout }) => {
|
|
1359
1466
|
const c = await ensureCanvas();
|
|
1360
|
-
const ok = c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1467
|
+
const ok = await c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1361
1468
|
if (!ok) {
|
|
1362
1469
|
return { content: [{ type: 'text', text: 'Group not found or no valid children.' }], isError: true };
|
|
1363
1470
|
}
|
|
@@ -1367,7 +1474,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1367
1474
|
|
|
1368
1475
|
server.tool(
|
|
1369
1476
|
'canvas_batch',
|
|
1370
|
-
'Run a batch of canvas operations with optional assigned references. Supports node.add, node.update, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
1477
|
+
'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
1371
1478
|
{
|
|
1372
1479
|
operations: z.array(z.object({
|
|
1373
1480
|
op: z.string().describe('Operation name, e.g. "node.add" or "edge.add"'),
|
|
@@ -1392,7 +1499,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1392
1499
|
async () => {
|
|
1393
1500
|
const c = await ensureCanvas();
|
|
1394
1501
|
return {
|
|
1395
|
-
content: [{ type: 'text', text: JSON.stringify(c.validate(), null, 2) }],
|
|
1502
|
+
content: [{ type: 'text', text: JSON.stringify(await c.validate(), null, 2) }],
|
|
1396
1503
|
};
|
|
1397
1504
|
},
|
|
1398
1505
|
);
|
|
@@ -1406,7 +1513,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1406
1513
|
},
|
|
1407
1514
|
async ({ groupId }) => {
|
|
1408
1515
|
const c = await ensureCanvas();
|
|
1409
|
-
const ok = c.ungroupNodes(groupId);
|
|
1516
|
+
const ok = await c.ungroupNodes(groupId);
|
|
1410
1517
|
if (!ok) {
|
|
1411
1518
|
return { content: [{ type: 'text', text: 'Group not found or already empty.' }], isError: true };
|
|
1412
1519
|
}
|
|
@@ -1425,9 +1532,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1425
1532
|
},
|
|
1426
1533
|
async ({ nodeIds, mode }) => {
|
|
1427
1534
|
const c = await ensureCanvas();
|
|
1428
|
-
const result = c.setContextPins(nodeIds, mode ?? 'set');
|
|
1429
|
-
|
|
1430
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1535
|
+
const result = await c.setContextPins(nodeIds, mode ?? 'set');
|
|
1431
1536
|
|
|
1432
1537
|
return {
|
|
1433
1538
|
content: [{
|
|
@@ -1450,7 +1555,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1450
1555
|
},
|
|
1451
1556
|
async (input) => {
|
|
1452
1557
|
const c = await ensureCanvas();
|
|
1453
|
-
const snapshot = c.saveSnapshot(input.name);
|
|
1558
|
+
const snapshot = await c.saveSnapshot(input.name);
|
|
1454
1559
|
if (!snapshot) {
|
|
1455
1560
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Failed to save snapshot' }) }] };
|
|
1456
1561
|
}
|
|
@@ -1466,7 +1571,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1466
1571
|
async () => {
|
|
1467
1572
|
const c = await ensureCanvas();
|
|
1468
1573
|
return {
|
|
1469
|
-
content: [{ type: 'text', text: JSON.stringify({ snapshots: c.listSnapshots() }, null, 2) }],
|
|
1574
|
+
content: [{ type: 'text', text: JSON.stringify({ snapshots: await c.listSnapshots() }, null, 2) }],
|
|
1470
1575
|
};
|
|
1471
1576
|
},
|
|
1472
1577
|
);
|
|
@@ -1484,9 +1589,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1484
1589
|
if (!result.ok) {
|
|
1485
1590
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }] };
|
|
1486
1591
|
}
|
|
1487
|
-
|
|
1592
|
+
const layout = await c.getLayout();
|
|
1488
1593
|
return {
|
|
1489
|
-
content: [{ type: 'text', text: JSON.stringify({ ok: true,
|
|
1594
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, restored: input.id, summary: buildSnapshotRestoreSummary(layout) }, null, 2) }],
|
|
1490
1595
|
};
|
|
1491
1596
|
},
|
|
1492
1597
|
);
|
|
@@ -1500,7 +1605,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1500
1605
|
},
|
|
1501
1606
|
async ({ id }) => {
|
|
1502
1607
|
const c = await ensureCanvas();
|
|
1503
|
-
const result = c.deleteSnapshot(id);
|
|
1608
|
+
const result = await c.deleteSnapshot(id);
|
|
1504
1609
|
if (!result.ok) {
|
|
1505
1610
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }], isError: true };
|
|
1506
1611
|
}
|
|
@@ -1510,24 +1615,6 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1510
1615
|
},
|
|
1511
1616
|
);
|
|
1512
1617
|
|
|
1513
|
-
// ── Resource change notifications ──────────────────────────
|
|
1514
|
-
// When canvas state changes (nodes, edges, pins), notify MCP clients
|
|
1515
|
-
// so they can re-read resources like canvas://pinned-context.
|
|
1516
|
-
canvasState.onChange((type) => {
|
|
1517
|
-
try {
|
|
1518
|
-
if (type === 'pins') {
|
|
1519
|
-
server.server.sendResourceUpdated({ uri: 'canvas://pinned-context' });
|
|
1520
|
-
}
|
|
1521
|
-
server.server.sendResourceUpdated({ uri: 'canvas://layout' });
|
|
1522
|
-
server.server.sendResourceUpdated({ uri: 'canvas://summary' });
|
|
1523
|
-
server.server.sendResourceUpdated({ uri: 'canvas://spatial-context' });
|
|
1524
|
-
server.server.sendResourceUpdated({ uri: 'canvas://history' });
|
|
1525
|
-
server.server.sendResourceUpdated({ uri: 'canvas://code-graph' });
|
|
1526
|
-
} catch (error) {
|
|
1527
|
-
console.debug('[mcp] resource notification failed', error);
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
1618
|
// Connect via stdio
|
|
1532
1619
|
const transport = new StdioServerTransport();
|
|
1533
1620
|
await server.connect(transport);
|
|
@@ -1373,6 +1373,7 @@ function resolveBatchRefs(value: unknown, refs: Record<string, unknown>): unknow
|
|
|
1373
1373
|
if (typeof value === 'string' && value.startsWith('$')) {
|
|
1374
1374
|
const path = value.slice(1).split('.');
|
|
1375
1375
|
let current: unknown = refs[path[0] ?? ''];
|
|
1376
|
+
if (path.length === 1 && isPlainRecord(current) && typeof current.id === 'string') return current.id;
|
|
1376
1377
|
for (const segment of path.slice(1)) {
|
|
1377
1378
|
if (!isPlainRecord(current) && !Array.isArray(current)) return undefined;
|
|
1378
1379
|
current = (current as Record<string, unknown>)[segment];
|
|
@@ -140,11 +140,18 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
140
140
|
fields: [
|
|
141
141
|
{ name: 'title', type: 'string', required: false, description: 'Optional title.' },
|
|
142
142
|
{ name: 'content', type: 'string', required: false, description: 'Trace summary.' },
|
|
143
|
+
{ name: 'toolName', type: 'string', required: false, description: 'Tool or operation label shown in the trace pill; defaults to title.' },
|
|
144
|
+
{ name: 'category', type: 'string', required: false, description: 'Trace category color key: mcp, file, subagent, or other.' },
|
|
145
|
+
{ name: 'status', type: 'string', required: false, description: 'Trace status: running, success, or failed.' },
|
|
146
|
+
{ name: 'duration', type: 'string', required: false, description: 'Optional duration badge text.' },
|
|
147
|
+
{ name: 'resultSummary', type: 'string', required: false, description: 'Short trace result summary; defaults to content.' },
|
|
148
|
+
{ name: 'error', type: 'string', required: false, description: 'Short error message shown in failed traces.' },
|
|
143
149
|
],
|
|
144
150
|
example: {
|
|
145
151
|
type: 'trace',
|
|
146
152
|
title: 'Execution Trace',
|
|
147
153
|
content: 'Canvas actions and tool events.',
|
|
154
|
+
status: 'success',
|
|
148
155
|
},
|
|
149
156
|
},
|
|
150
157
|
{
|
|
@@ -378,12 +385,16 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
378
385
|
{ name: 'openInCanvas', type: 'boolean', required: false, description: 'Open the built artifact on the canvas (default true).' },
|
|
379
386
|
{ name: 'includeLogs', type: 'boolean', required: false, description: 'Include raw build stdout/stderr in the response (default false).' },
|
|
380
387
|
{ name: 'deps', type: 'string[]', required: false, description: 'Optional npm dependencies to add before bundling, e.g. recharts.', aliases: ['deps'] },
|
|
388
|
+
{ name: 'timeoutMs', type: 'number', required: false, description: 'Build command timeout in milliseconds. This controls subprocess timeout, not the MCP client request timeout.' },
|
|
381
389
|
],
|
|
382
390
|
example: {
|
|
383
391
|
title: 'Dashboard Artifact',
|
|
384
392
|
appTsx: 'export default function App() { return <main>Artifact</main>; }',
|
|
385
393
|
indexCss: 'body { background: #123456; color: white; }',
|
|
386
394
|
},
|
|
395
|
+
notes: [
|
|
396
|
+
'Cold builds can exceed default 60s MCP client timeouts; configure a longer MCP call timeout or retry with the same projectPath/outputPath if the first call times out.',
|
|
397
|
+
],
|
|
387
398
|
},
|
|
388
399
|
];
|
|
389
400
|
|