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/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
- createCanvas,
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 { emitPrimaryWorkbenchEvent, wrapCanvasAutomationScript } from '../server/server.js';
36
- import { searchNodes, buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
37
- import { mutationHistory, diffLayouts, formatDiff } from '../server/mutation-history.js';
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: PmxCanvas | null = null;
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<PmxCanvas> {
78
+ async function ensureCanvas(): Promise<CanvasAccess> {
84
79
  if (!canvas) {
85
- const port = parseInt(process.env.PMX_CANVAS_PORT ?? '4313');
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: PmxCanvas, id: string): Record<string, unknown> {
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 = searchNodes(canvasState.getLayout().nodes, query);
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: mutationHistory.canUndo(), canRedo: mutationHistory.canRedo() }) }],
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: mutationHistory.canUndo(), canRedo: mutationHistory.canRedo() }) }],
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 snapData = canvasState.getSnapshotData(snapshot);
851
- if (!snapData) {
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: formatDiff(diff) }],
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: c.getAutomationWebViewStatus(),
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 = canvasState.contextPinnedNodeIds;
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(buildCanvasSummary(), null, 2),
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 = canvasState.getLayout();
1191
- const spatial = buildSpatialContext(layout.nodes, layout.edges, canvasState.contextPinnedNodeIds);
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: mutationHistory.toHumanReadable(),
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 = buildCodeGraphSummary();
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(canvasState.getLayout()) }) }],
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);