pmx-canvas 0.1.10 → 0.1.12
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 +131 -0
- package/dist/canvas/index.js +30 -30
- package/dist/json-render/index.js +115 -115
- package/dist/types/json-render/catalog.d.ts +10 -0
- package/dist/types/json-render/charts/components.d.ts +18 -0
- package/dist/types/json-render/charts/definitions.d.ts +4 -0
- package/dist/types/json-render/charts/extra-components.d.ts +3 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +6 -0
- package/dist/types/json-render/server.d.ts +4 -0
- package/dist/types/mcp/canvas-access.d.ts +87 -0
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/server.d.ts +1 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +9 -0
- package/src/cli/agent.ts +78 -2
- package/src/cli/index.ts +6 -3
- package/src/client/canvas/CanvasNode.tsx +3 -1
- package/src/client/canvas/auto-fit.ts +1 -1
- package/src/json-render/charts/components.tsx +18 -10
- package/src/json-render/charts/definitions.ts +4 -0
- package/src/json-render/charts/extra-components.tsx +23 -16
- package/src/json-render/charts/extra-definitions.ts +6 -0
- package/src/json-render/server.ts +11 -0
- package/src/mcp/canvas-access.ts +651 -0
- package/src/mcp/server.ts +170 -95
- package/src/server/canvas-operations.ts +21 -1
- package/src/server/canvas-schema.ts +5 -0
- package/src/server/diagram-presets.ts +44 -12
- package/src/server/index.ts +9 -3
- package/src/server/server.ts +37 -3
package/src/mcp/server.ts
CHANGED
|
@@ -24,22 +24,17 @@ 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, 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 resourceNotificationsStarted = false;
|
|
43
38
|
|
|
44
39
|
const jsonRenderSpecSchema = z.union([
|
|
45
40
|
z.object({
|
|
@@ -80,31 +75,121 @@ function safeWorkspacePath(pathLike: string): string {
|
|
|
80
75
|
return resolved;
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
async function ensureCanvas(): Promise<
|
|
78
|
+
async function ensureCanvas(): Promise<CanvasAccess> {
|
|
84
79
|
if (!canvas) {
|
|
85
|
-
|
|
86
|
-
canvas = createCanvas({ port });
|
|
87
|
-
await canvas.start({ open: true });
|
|
80
|
+
canvas = await createCanvasAccess();
|
|
88
81
|
}
|
|
82
|
+
startResourceNotifications(canvas);
|
|
89
83
|
return canvas;
|
|
90
84
|
}
|
|
91
85
|
|
|
86
|
+
function sleep(ms: number): Promise<void> {
|
|
87
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sendCanvasResourceNotifications(type: 'nodes' | 'pins' = 'nodes'): void {
|
|
91
|
+
const server = resourceNotificationServer;
|
|
92
|
+
if (!server) return;
|
|
93
|
+
try {
|
|
94
|
+
if (type === 'pins') {
|
|
95
|
+
server.server.sendResourceUpdated({ uri: 'canvas://pinned-context' });
|
|
96
|
+
}
|
|
97
|
+
server.server.sendResourceUpdated({ uri: 'canvas://layout' });
|
|
98
|
+
server.server.sendResourceUpdated({ uri: 'canvas://summary' });
|
|
99
|
+
server.server.sendResourceUpdated({ uri: 'canvas://spatial-context' });
|
|
100
|
+
server.server.sendResourceUpdated({ uri: 'canvas://history' });
|
|
101
|
+
server.server.sendResourceUpdated({ uri: 'canvas://code-graph' });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.debug('[mcp] resource notification failed', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleRemoteSseFrame(frame: string): void {
|
|
108
|
+
const eventLine = frame.split('\n').find((line) => line.startsWith('event: '));
|
|
109
|
+
const event = eventLine?.slice('event: '.length).trim() ?? '';
|
|
110
|
+
if (!event || event === 'connected' || event === 'ping') return;
|
|
111
|
+
sendCanvasResourceNotifications(event === 'context-pins-changed' ? 'pins' : 'nodes');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function watchRemoteCanvasEvents(baseUrl: string): Promise<void> {
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
while (true) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(`${baseUrl}/api/workbench/events`);
|
|
119
|
+
if (!response.ok || !response.body) {
|
|
120
|
+
await sleep(1_000);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const reader = response.body.getReader();
|
|
125
|
+
let buffer = '';
|
|
126
|
+
while (true) {
|
|
127
|
+
const { done, value } = await reader.read();
|
|
128
|
+
if (done) break;
|
|
129
|
+
buffer += decoder.decode(value, { stream: true });
|
|
130
|
+
const frames = buffer.split('\n\n');
|
|
131
|
+
buffer = frames.pop() ?? '';
|
|
132
|
+
for (const frame of frames) handleRemoteSseFrame(frame);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.debug('[mcp] remote canvas event stream failed', error);
|
|
136
|
+
}
|
|
137
|
+
await sleep(1_000);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function startResourceNotifications(c: CanvasAccess): void {
|
|
142
|
+
if (resourceNotificationsStarted) return;
|
|
143
|
+
const server = resourceNotificationServer;
|
|
144
|
+
if (!server) return;
|
|
145
|
+
resourceNotificationsStarted = true;
|
|
146
|
+
|
|
147
|
+
if (c.remoteBaseUrl) {
|
|
148
|
+
void watchRemoteCanvasEvents(c.remoteBaseUrl);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
canvasState.onChange((type) => {
|
|
153
|
+
sendCanvasResourceNotifications(type);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
92
157
|
function encodeBase64(bytes: Uint8Array): string {
|
|
93
158
|
return Buffer.from(bytes).toString('base64');
|
|
94
159
|
}
|
|
95
160
|
|
|
96
|
-
function createdNodePayload(c:
|
|
97
|
-
const node = c.getNode(id);
|
|
161
|
+
async function createdNodePayload(c: CanvasAccess, id: string): Promise<Record<string, unknown>> {
|
|
162
|
+
const node = await c.getNode(id);
|
|
98
163
|
if (!node) return { ok: true, id };
|
|
99
164
|
const serialized = serializeCanvasNode(node);
|
|
100
165
|
return { ok: true, node: serialized, ...serialized };
|
|
101
166
|
}
|
|
102
167
|
|
|
168
|
+
function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>, pinnedIds: string[]): Record<string, unknown> {
|
|
169
|
+
const pinned = new Set(pinnedIds);
|
|
170
|
+
const nodesByType: Record<string, number> = {};
|
|
171
|
+
const pinnedTitles: string[] = [];
|
|
172
|
+
for (const node of layout.nodes) {
|
|
173
|
+
const serialized = serializeCanvasNode(node);
|
|
174
|
+
nodesByType[serialized.kind] = (nodesByType[serialized.kind] ?? 0) + 1;
|
|
175
|
+
if (pinned.has(node.id)) pinnedTitles.push(getCanvasNodeTitle(node) ?? node.id);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
totalNodes: layout.nodes.length,
|
|
179
|
+
totalEdges: layout.edges.length,
|
|
180
|
+
nodesByType,
|
|
181
|
+
pinnedCount: pinned.size,
|
|
182
|
+
pinnedTitles,
|
|
183
|
+
viewport: layout.viewport,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
103
187
|
export async function startMcpServer(): Promise<void> {
|
|
104
188
|
const server = new McpServer({
|
|
105
189
|
name: 'pmx-canvas',
|
|
106
190
|
version: '0.1.0',
|
|
107
191
|
});
|
|
192
|
+
resourceNotificationServer = server;
|
|
108
193
|
|
|
109
194
|
// ── canvas_get_layout ──────────────────────────────────────────
|
|
110
195
|
server.tool(
|
|
@@ -113,7 +198,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
113
198
|
{},
|
|
114
199
|
async () => {
|
|
115
200
|
const c = await ensureCanvas();
|
|
116
|
-
const layout = serializeCanvasLayout(c.getLayout());
|
|
201
|
+
const layout = serializeCanvasLayout(await c.getLayout());
|
|
117
202
|
return {
|
|
118
203
|
content: [{ type: 'text', text: JSON.stringify(layout, null, 2) }],
|
|
119
204
|
};
|
|
@@ -127,7 +212,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
127
212
|
{ id: z.string().describe('The node ID to retrieve') },
|
|
128
213
|
async ({ id }) => {
|
|
129
214
|
const c = await ensureCanvas();
|
|
130
|
-
const node = c.getNode(id);
|
|
215
|
+
const node = await c.getNode(id);
|
|
131
216
|
if (!node) {
|
|
132
217
|
return {
|
|
133
218
|
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
@@ -155,6 +240,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
155
240
|
y: z.number().optional().describe('Y position (auto-placed if omitted)'),
|
|
156
241
|
width: z.number().optional().describe('Width in pixels (default: 720)'),
|
|
157
242
|
height: z.number().optional().describe('Height in pixels (default: 600)'),
|
|
243
|
+
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
|
|
158
244
|
},
|
|
159
245
|
async (input) => {
|
|
160
246
|
const c = await ensureCanvas();
|
|
@@ -173,6 +259,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
173
259
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
174
260
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
175
261
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
262
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
176
263
|
});
|
|
177
264
|
return {
|
|
178
265
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
@@ -182,9 +269,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
182
269
|
const nodeInput = input.type === 'image' && input.path && !input.content
|
|
183
270
|
? { ...input, content: input.path }
|
|
184
271
|
: input;
|
|
185
|
-
const id = c.addNode(nodeInput);
|
|
272
|
+
const id = await c.addNode(nodeInput);
|
|
186
273
|
return {
|
|
187
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
274
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
188
275
|
};
|
|
189
276
|
},
|
|
190
277
|
);
|
|
@@ -461,23 +548,25 @@ export async function startMcpServer(): Promise<void> {
|
|
|
461
548
|
y: z.number().optional().describe('Optional Y position'),
|
|
462
549
|
width: z.number().optional().describe('Optional node width'),
|
|
463
550
|
height: z.number().optional().describe('Optional node height'),
|
|
551
|
+
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
|
|
464
552
|
},
|
|
465
553
|
async (input) => {
|
|
466
554
|
const c = await ensureCanvas();
|
|
467
555
|
try {
|
|
468
|
-
const result = c.addJsonRenderNode({
|
|
556
|
+
const result = await c.addJsonRenderNode({
|
|
469
557
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
470
558
|
spec: input.spec,
|
|
471
559
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
472
560
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
473
561
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
474
562
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
563
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
475
564
|
});
|
|
476
565
|
return {
|
|
477
566
|
content: [{
|
|
478
567
|
type: 'text',
|
|
479
568
|
text: JSON.stringify({
|
|
480
|
-
...createdNodePayload(c, result.id),
|
|
569
|
+
...await createdNodePayload(c, result.id),
|
|
481
570
|
url: result.url,
|
|
482
571
|
spec: result.spec,
|
|
483
572
|
}, null, 2),
|
|
@@ -515,15 +604,18 @@ export async function startMcpServer(): Promise<void> {
|
|
|
515
604
|
barColor: z.string().optional().describe('Optional bar color for composed charts'),
|
|
516
605
|
lineColor: z.string().optional().describe('Optional line color for composed charts'),
|
|
517
606
|
height: z.number().optional().describe('Optional chart content height'),
|
|
607
|
+
showLegend: z.boolean().optional().describe('Show chart legend when supported; pass false for compact node layouts'),
|
|
608
|
+
showLabels: z.boolean().optional().describe('Show direct labels when supported, such as pie slice labels (defaults to true)'),
|
|
518
609
|
x: z.number().optional().describe('Optional X position'),
|
|
519
610
|
y: z.number().optional().describe('Optional Y position'),
|
|
520
611
|
width: z.number().optional().describe('Optional node width'),
|
|
521
612
|
nodeHeight: z.number().optional().describe('Optional node height'),
|
|
613
|
+
strictSize: z.boolean().optional().describe('Keep explicit node size fixed and scroll overflowing content instead of browser auto-fitting'),
|
|
522
614
|
},
|
|
523
615
|
async (input) => {
|
|
524
616
|
const c = await ensureCanvas();
|
|
525
617
|
try {
|
|
526
|
-
const result = c.addGraphNode({
|
|
618
|
+
const result = await c.addGraphNode({
|
|
527
619
|
graphType: input.graphType,
|
|
528
620
|
data: input.data,
|
|
529
621
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
@@ -542,16 +634,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
542
634
|
...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
|
|
543
635
|
...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
|
|
544
636
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
637
|
+
...(typeof input.showLegend === 'boolean' ? { showLegend: input.showLegend } : {}),
|
|
638
|
+
...(typeof input.showLabels === 'boolean' ? { showLabels: input.showLabels } : {}),
|
|
545
639
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
546
640
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
547
641
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
548
642
|
...(typeof input.nodeHeight === 'number' ? { heightPx: input.nodeHeight } : {}),
|
|
643
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
549
644
|
});
|
|
550
645
|
return {
|
|
551
646
|
content: [{
|
|
552
647
|
type: 'text',
|
|
553
648
|
text: JSON.stringify({
|
|
554
|
-
...createdNodePayload(c, result.id),
|
|
649
|
+
...await createdNodePayload(c, result.id),
|
|
555
650
|
url: result.url,
|
|
556
651
|
spec: result.spec,
|
|
557
652
|
}, null, 2),
|
|
@@ -589,7 +684,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
589
684
|
},
|
|
590
685
|
async ({ id, title, content, x, y, width, height, spec, graphType, data, xKey, yKey, chartHeight, collapsed, arrangeLocked }) => {
|
|
591
686
|
const c = await ensureCanvas();
|
|
592
|
-
const node = c.getNode(id);
|
|
687
|
+
const node = await c.getNode(id);
|
|
593
688
|
if (!node) {
|
|
594
689
|
return {
|
|
595
690
|
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
@@ -617,10 +712,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
617
712
|
if (arrangeLocked !== undefined) {
|
|
618
713
|
patch.arrangeLocked = arrangeLocked;
|
|
619
714
|
}
|
|
620
|
-
c.updateNode(id, patch);
|
|
621
|
-
const updated = c.getNode(id);
|
|
715
|
+
await c.updateNode(id, patch);
|
|
716
|
+
const updated = await c.getNode(id);
|
|
622
717
|
return {
|
|
623
|
-
content: [{ type: 'text', text: JSON.stringify(updated ? createdNodePayload(c, id) : { ok: true, id }, null, 2) }],
|
|
718
|
+
content: [{ type: 'text', text: JSON.stringify(updated ? await createdNodePayload(c, id) : { ok: true, id }, null, 2) }],
|
|
624
719
|
};
|
|
625
720
|
},
|
|
626
721
|
);
|
|
@@ -632,7 +727,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
632
727
|
{ id: z.string().describe('Node ID to remove') },
|
|
633
728
|
async ({ id }) => {
|
|
634
729
|
const c = await ensureCanvas();
|
|
635
|
-
c.removeNode(id);
|
|
730
|
+
await c.removeNode(id);
|
|
636
731
|
return {
|
|
637
732
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
638
733
|
};
|
|
@@ -668,8 +763,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
668
763
|
};
|
|
669
764
|
}
|
|
670
765
|
try {
|
|
671
|
-
const id = c.addEdge(input);
|
|
672
|
-
const edge = c.getLayout().edges.find((entry) => entry.id === id);
|
|
766
|
+
const id = await c.addEdge(input);
|
|
767
|
+
const edge = (await c.getLayout()).edges.find((entry) => entry.id === id);
|
|
673
768
|
return {
|
|
674
769
|
content: [{
|
|
675
770
|
type: 'text',
|
|
@@ -692,7 +787,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
692
787
|
{ id: z.string().describe('Edge ID to remove') },
|
|
693
788
|
async ({ id }) => {
|
|
694
789
|
const c = await ensureCanvas();
|
|
695
|
-
c.removeEdge(id);
|
|
790
|
+
await c.removeEdge(id);
|
|
696
791
|
return {
|
|
697
792
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
698
793
|
};
|
|
@@ -708,7 +803,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
708
803
|
},
|
|
709
804
|
async ({ layout }) => {
|
|
710
805
|
const c = await ensureCanvas();
|
|
711
|
-
c.arrange(layout ?? 'grid');
|
|
806
|
+
await c.arrange(layout ?? 'grid');
|
|
712
807
|
return {
|
|
713
808
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: layout ?? 'grid' }) }],
|
|
714
809
|
};
|
|
@@ -728,7 +823,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
728
823
|
},
|
|
729
824
|
async ({ id, noPan }) => {
|
|
730
825
|
const c = await ensureCanvas();
|
|
731
|
-
const result = c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
826
|
+
const result = await c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
732
827
|
if (!result) {
|
|
733
828
|
return {
|
|
734
829
|
content: [
|
|
@@ -762,7 +857,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
762
857
|
},
|
|
763
858
|
async (input) => {
|
|
764
859
|
const c = await ensureCanvas();
|
|
765
|
-
const result = c.fitView({
|
|
860
|
+
const result = await c.fitView({
|
|
766
861
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
767
862
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
768
863
|
...(typeof input.padding === 'number' ? { padding: input.padding } : {}),
|
|
@@ -782,7 +877,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
782
877
|
{},
|
|
783
878
|
async () => {
|
|
784
879
|
const c = await ensureCanvas();
|
|
785
|
-
c.clear();
|
|
880
|
+
await c.clear();
|
|
786
881
|
return {
|
|
787
882
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, cleared: true }) }],
|
|
788
883
|
};
|
|
@@ -798,8 +893,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
798
893
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
799
894
|
},
|
|
800
895
|
async ({ query, limit }) => {
|
|
801
|
-
await ensureCanvas();
|
|
802
|
-
const results =
|
|
896
|
+
const c = await ensureCanvas();
|
|
897
|
+
const results = await c.search(query);
|
|
803
898
|
const capped = results.slice(0, limit ?? 10);
|
|
804
899
|
return {
|
|
805
900
|
content: [{
|
|
@@ -818,8 +913,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
818
913
|
async () => {
|
|
819
914
|
const c = await ensureCanvas();
|
|
820
915
|
const result = await c.undo();
|
|
916
|
+
const history = await c.getHistory();
|
|
821
917
|
return {
|
|
822
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
918
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
823
919
|
};
|
|
824
920
|
},
|
|
825
921
|
);
|
|
@@ -832,8 +928,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
832
928
|
async () => {
|
|
833
929
|
const c = await ensureCanvas();
|
|
834
930
|
const result = await c.redo();
|
|
931
|
+
const history = await c.getHistory();
|
|
835
932
|
return {
|
|
836
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
933
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
837
934
|
};
|
|
838
935
|
},
|
|
839
936
|
);
|
|
@@ -846,15 +943,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
846
943
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
847
944
|
},
|
|
848
945
|
async ({ snapshot }) => {
|
|
849
|
-
await ensureCanvas();
|
|
850
|
-
const
|
|
851
|
-
if (!
|
|
946
|
+
const c = await ensureCanvas();
|
|
947
|
+
const result = await c.diffSnapshot(snapshot);
|
|
948
|
+
if (!result.ok) {
|
|
852
949
|
return { content: [{ type: 'text', text: `Snapshot "${snapshot}" not found. Use canvas_snapshot to save one first.` }], isError: true };
|
|
853
950
|
}
|
|
854
|
-
const current = canvasState.getLayout();
|
|
855
|
-
const diff = diffLayouts(snapData.name, snapData, current);
|
|
856
951
|
return {
|
|
857
|
-
content: [{ type: 'text', text:
|
|
952
|
+
content: [{ type: 'text', text: result.text ?? '' }],
|
|
858
953
|
};
|
|
859
954
|
},
|
|
860
955
|
);
|
|
@@ -867,7 +962,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
867
962
|
async () => {
|
|
868
963
|
const c = await ensureCanvas();
|
|
869
964
|
return {
|
|
870
|
-
content: [{ type: 'text', text: JSON.stringify(c.getAutomationWebViewStatus(), null, 2) }],
|
|
965
|
+
content: [{ type: 'text', text: JSON.stringify(await c.getAutomationWebViewStatus(), null, 2) }],
|
|
871
966
|
};
|
|
872
967
|
},
|
|
873
968
|
);
|
|
@@ -917,13 +1012,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
917
1012
|
const c = await ensureCanvas();
|
|
918
1013
|
try {
|
|
919
1014
|
const stopped = await c.stopAutomationWebView();
|
|
1015
|
+
const webview = await c.getAutomationWebViewStatus();
|
|
920
1016
|
return {
|
|
921
1017
|
content: [{
|
|
922
1018
|
type: 'text',
|
|
923
1019
|
text: JSON.stringify({
|
|
924
1020
|
ok: true,
|
|
925
1021
|
stopped,
|
|
926
|
-
webview
|
|
1022
|
+
webview,
|
|
927
1023
|
}, null, 2),
|
|
928
1024
|
}],
|
|
929
1025
|
};
|
|
@@ -1007,7 +1103,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1007
1103
|
...(format ? { format } : {}),
|
|
1008
1104
|
...(typeof quality === 'number' ? { quality } : {}),
|
|
1009
1105
|
});
|
|
1010
|
-
const status = c.getAutomationWebViewStatus();
|
|
1106
|
+
const status = await c.getAutomationWebViewStatus();
|
|
1011
1107
|
return {
|
|
1012
1108
|
content: [
|
|
1013
1109
|
{
|
|
@@ -1082,8 +1178,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1082
1178
|
},
|
|
1083
1179
|
async () => {
|
|
1084
1180
|
const c = await ensureCanvas();
|
|
1085
|
-
const pinnedIds =
|
|
1086
|
-
const layout = c.getLayout();
|
|
1181
|
+
const pinnedIds = new Set(await c.getPinnedNodeIds());
|
|
1182
|
+
const layout = await c.getLayout();
|
|
1087
1183
|
|
|
1088
1184
|
const pinnedNodes = layout.nodes.filter((n) => pinnedIds.has(n.id));
|
|
1089
1185
|
const pinnedEdges = layout.edges.filter(
|
|
@@ -1137,7 +1233,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1137
1233
|
},
|
|
1138
1234
|
async () => {
|
|
1139
1235
|
const c = await ensureCanvas();
|
|
1140
|
-
const layout = serializeCanvasLayout(c.getLayout());
|
|
1236
|
+
const layout = serializeCanvasLayout(await c.getLayout());
|
|
1141
1237
|
return {
|
|
1142
1238
|
contents: [
|
|
1143
1239
|
{
|
|
@@ -1160,13 +1256,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1160
1256
|
mimeType: 'application/json',
|
|
1161
1257
|
},
|
|
1162
1258
|
async () => {
|
|
1163
|
-
await ensureCanvas();
|
|
1259
|
+
const c = await ensureCanvas();
|
|
1164
1260
|
return {
|
|
1165
1261
|
contents: [
|
|
1166
1262
|
{
|
|
1167
1263
|
uri: 'canvas://summary',
|
|
1168
1264
|
mimeType: 'application/json',
|
|
1169
|
-
text: JSON.stringify(
|
|
1265
|
+
text: JSON.stringify(buildSummaryFromLayout(await c.getLayout(), await c.getPinnedNodeIds()), null, 2),
|
|
1170
1266
|
},
|
|
1171
1267
|
],
|
|
1172
1268
|
};
|
|
@@ -1186,9 +1282,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1186
1282
|
mimeType: 'application/json',
|
|
1187
1283
|
},
|
|
1188
1284
|
async () => {
|
|
1189
|
-
await ensureCanvas();
|
|
1190
|
-
const layout =
|
|
1191
|
-
const spatial = buildSpatialContext(layout.nodes, layout.edges,
|
|
1285
|
+
const c = await ensureCanvas();
|
|
1286
|
+
const layout = await c.getLayout();
|
|
1287
|
+
const spatial = buildSpatialContext(layout.nodes, layout.edges, new Set(await c.getPinnedNodeIds()));
|
|
1192
1288
|
return {
|
|
1193
1289
|
contents: [
|
|
1194
1290
|
{
|
|
@@ -1212,13 +1308,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1212
1308
|
mimeType: 'text/plain',
|
|
1213
1309
|
},
|
|
1214
1310
|
async () => {
|
|
1215
|
-
await ensureCanvas();
|
|
1311
|
+
const c = await ensureCanvas();
|
|
1216
1312
|
return {
|
|
1217
1313
|
contents: [
|
|
1218
1314
|
{
|
|
1219
1315
|
uri: 'canvas://history',
|
|
1220
1316
|
mimeType: 'text/plain',
|
|
1221
|
-
text:
|
|
1317
|
+
text: (await c.getHistory()).text,
|
|
1222
1318
|
},
|
|
1223
1319
|
],
|
|
1224
1320
|
};
|
|
@@ -1237,8 +1333,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1237
1333
|
mimeType: 'application/json',
|
|
1238
1334
|
},
|
|
1239
1335
|
async () => {
|
|
1240
|
-
await ensureCanvas();
|
|
1241
|
-
const summary =
|
|
1336
|
+
const c = await ensureCanvas();
|
|
1337
|
+
const summary = (await c.getCodeGraph()).summary;
|
|
1242
1338
|
return {
|
|
1243
1339
|
contents: [
|
|
1244
1340
|
{
|
|
@@ -1329,9 +1425,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1329
1425
|
},
|
|
1330
1426
|
async (input) => {
|
|
1331
1427
|
const c = await ensureCanvas();
|
|
1332
|
-
const id = c.createGroup(input);
|
|
1428
|
+
const id = await c.createGroup(input);
|
|
1333
1429
|
return {
|
|
1334
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
1430
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
1335
1431
|
};
|
|
1336
1432
|
},
|
|
1337
1433
|
);
|
|
@@ -1347,7 +1443,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1347
1443
|
},
|
|
1348
1444
|
async ({ groupId, childIds, childLayout }) => {
|
|
1349
1445
|
const c = await ensureCanvas();
|
|
1350
|
-
const ok = c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1446
|
+
const ok = await c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1351
1447
|
if (!ok) {
|
|
1352
1448
|
return { content: [{ type: 'text', text: 'Group not found or no valid children.' }], isError: true };
|
|
1353
1449
|
}
|
|
@@ -1382,7 +1478,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1382
1478
|
async () => {
|
|
1383
1479
|
const c = await ensureCanvas();
|
|
1384
1480
|
return {
|
|
1385
|
-
content: [{ type: 'text', text: JSON.stringify(c.validate(), null, 2) }],
|
|
1481
|
+
content: [{ type: 'text', text: JSON.stringify(await c.validate(), null, 2) }],
|
|
1386
1482
|
};
|
|
1387
1483
|
},
|
|
1388
1484
|
);
|
|
@@ -1396,7 +1492,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1396
1492
|
},
|
|
1397
1493
|
async ({ groupId }) => {
|
|
1398
1494
|
const c = await ensureCanvas();
|
|
1399
|
-
const ok = c.ungroupNodes(groupId);
|
|
1495
|
+
const ok = await c.ungroupNodes(groupId);
|
|
1400
1496
|
if (!ok) {
|
|
1401
1497
|
return { content: [{ type: 'text', text: 'Group not found or already empty.' }], isError: true };
|
|
1402
1498
|
}
|
|
@@ -1415,9 +1511,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1415
1511
|
},
|
|
1416
1512
|
async ({ nodeIds, mode }) => {
|
|
1417
1513
|
const c = await ensureCanvas();
|
|
1418
|
-
const result = c.setContextPins(nodeIds, mode ?? 'set');
|
|
1419
|
-
|
|
1420
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1514
|
+
const result = await c.setContextPins(nodeIds, mode ?? 'set');
|
|
1421
1515
|
|
|
1422
1516
|
return {
|
|
1423
1517
|
content: [{
|
|
@@ -1440,7 +1534,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1440
1534
|
},
|
|
1441
1535
|
async (input) => {
|
|
1442
1536
|
const c = await ensureCanvas();
|
|
1443
|
-
const snapshot = c.saveSnapshot(input.name);
|
|
1537
|
+
const snapshot = await c.saveSnapshot(input.name);
|
|
1444
1538
|
if (!snapshot) {
|
|
1445
1539
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Failed to save snapshot' }) }] };
|
|
1446
1540
|
}
|
|
@@ -1456,7 +1550,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1456
1550
|
async () => {
|
|
1457
1551
|
const c = await ensureCanvas();
|
|
1458
1552
|
return {
|
|
1459
|
-
content: [{ type: 'text', text: JSON.stringify({ snapshots: c.listSnapshots() }, null, 2) }],
|
|
1553
|
+
content: [{ type: 'text', text: JSON.stringify({ snapshots: await c.listSnapshots() }, null, 2) }],
|
|
1460
1554
|
};
|
|
1461
1555
|
},
|
|
1462
1556
|
);
|
|
@@ -1474,9 +1568,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1474
1568
|
if (!result.ok) {
|
|
1475
1569
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }] };
|
|
1476
1570
|
}
|
|
1477
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1478
1571
|
return {
|
|
1479
|
-
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: serializeCanvasLayout(
|
|
1572
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: serializeCanvasLayout(await c.getLayout()) }) }],
|
|
1480
1573
|
};
|
|
1481
1574
|
},
|
|
1482
1575
|
);
|
|
@@ -1490,7 +1583,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1490
1583
|
},
|
|
1491
1584
|
async ({ id }) => {
|
|
1492
1585
|
const c = await ensureCanvas();
|
|
1493
|
-
const result = c.deleteSnapshot(id);
|
|
1586
|
+
const result = await c.deleteSnapshot(id);
|
|
1494
1587
|
if (!result.ok) {
|
|
1495
1588
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }], isError: true };
|
|
1496
1589
|
}
|
|
@@ -1500,24 +1593,6 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1500
1593
|
},
|
|
1501
1594
|
);
|
|
1502
1595
|
|
|
1503
|
-
// ── Resource change notifications ──────────────────────────
|
|
1504
|
-
// When canvas state changes (nodes, edges, pins), notify MCP clients
|
|
1505
|
-
// so they can re-read resources like canvas://pinned-context.
|
|
1506
|
-
canvasState.onChange((type) => {
|
|
1507
|
-
try {
|
|
1508
|
-
if (type === 'pins') {
|
|
1509
|
-
server.server.sendResourceUpdated({ uri: 'canvas://pinned-context' });
|
|
1510
|
-
}
|
|
1511
|
-
server.server.sendResourceUpdated({ uri: 'canvas://layout' });
|
|
1512
|
-
server.server.sendResourceUpdated({ uri: 'canvas://summary' });
|
|
1513
|
-
server.server.sendResourceUpdated({ uri: 'canvas://spatial-context' });
|
|
1514
|
-
server.server.sendResourceUpdated({ uri: 'canvas://history' });
|
|
1515
|
-
server.server.sendResourceUpdated({ uri: 'canvas://code-graph' });
|
|
1516
|
-
} catch (error) {
|
|
1517
|
-
console.debug('[mcp] resource notification failed', error);
|
|
1518
|
-
}
|
|
1519
|
-
});
|
|
1520
|
-
|
|
1521
1596
|
// Connect via stdio
|
|
1522
1597
|
const transport = new StdioServerTransport();
|
|
1523
1598
|
await server.connect(transport);
|