pmx-canvas 0.1.11 → 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 +48 -0
- package/dist/types/mcp/canvas-access.d.ts +87 -0
- package/dist/types/server/server.d.ts +1 -0
- package/package.json +1 -1
- package/src/mcp/canvas-access.ts +651 -0
- package/src/mcp/server.ts +160 -95
- package/src/server/index.ts +2 -2
- package/src/server/server.ts +4 -1
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.` }],
|
|
@@ -184,9 +269,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
184
269
|
const nodeInput = input.type === 'image' && input.path && !input.content
|
|
185
270
|
? { ...input, content: input.path }
|
|
186
271
|
: input;
|
|
187
|
-
const id = c.addNode(nodeInput);
|
|
272
|
+
const id = await c.addNode(nodeInput);
|
|
188
273
|
return {
|
|
189
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
274
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
190
275
|
};
|
|
191
276
|
},
|
|
192
277
|
);
|
|
@@ -468,7 +553,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
468
553
|
async (input) => {
|
|
469
554
|
const c = await ensureCanvas();
|
|
470
555
|
try {
|
|
471
|
-
const result = c.addJsonRenderNode({
|
|
556
|
+
const result = await c.addJsonRenderNode({
|
|
472
557
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
473
558
|
spec: input.spec,
|
|
474
559
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
@@ -481,7 +566,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
481
566
|
content: [{
|
|
482
567
|
type: 'text',
|
|
483
568
|
text: JSON.stringify({
|
|
484
|
-
...createdNodePayload(c, result.id),
|
|
569
|
+
...await createdNodePayload(c, result.id),
|
|
485
570
|
url: result.url,
|
|
486
571
|
spec: result.spec,
|
|
487
572
|
}, null, 2),
|
|
@@ -530,7 +615,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
530
615
|
async (input) => {
|
|
531
616
|
const c = await ensureCanvas();
|
|
532
617
|
try {
|
|
533
|
-
const result = c.addGraphNode({
|
|
618
|
+
const result = await c.addGraphNode({
|
|
534
619
|
graphType: input.graphType,
|
|
535
620
|
data: input.data,
|
|
536
621
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
@@ -561,7 +646,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
561
646
|
content: [{
|
|
562
647
|
type: 'text',
|
|
563
648
|
text: JSON.stringify({
|
|
564
|
-
...createdNodePayload(c, result.id),
|
|
649
|
+
...await createdNodePayload(c, result.id),
|
|
565
650
|
url: result.url,
|
|
566
651
|
spec: result.spec,
|
|
567
652
|
}, null, 2),
|
|
@@ -599,7 +684,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
599
684
|
},
|
|
600
685
|
async ({ id, title, content, x, y, width, height, spec, graphType, data, xKey, yKey, chartHeight, collapsed, arrangeLocked }) => {
|
|
601
686
|
const c = await ensureCanvas();
|
|
602
|
-
const node = c.getNode(id);
|
|
687
|
+
const node = await c.getNode(id);
|
|
603
688
|
if (!node) {
|
|
604
689
|
return {
|
|
605
690
|
content: [{ type: 'text', text: `Node "${id}" not found.` }],
|
|
@@ -627,10 +712,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
627
712
|
if (arrangeLocked !== undefined) {
|
|
628
713
|
patch.arrangeLocked = arrangeLocked;
|
|
629
714
|
}
|
|
630
|
-
c.updateNode(id, patch);
|
|
631
|
-
const updated = c.getNode(id);
|
|
715
|
+
await c.updateNode(id, patch);
|
|
716
|
+
const updated = await c.getNode(id);
|
|
632
717
|
return {
|
|
633
|
-
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) }],
|
|
634
719
|
};
|
|
635
720
|
},
|
|
636
721
|
);
|
|
@@ -642,7 +727,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
642
727
|
{ id: z.string().describe('Node ID to remove') },
|
|
643
728
|
async ({ id }) => {
|
|
644
729
|
const c = await ensureCanvas();
|
|
645
|
-
c.removeNode(id);
|
|
730
|
+
await c.removeNode(id);
|
|
646
731
|
return {
|
|
647
732
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
648
733
|
};
|
|
@@ -678,8 +763,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
678
763
|
};
|
|
679
764
|
}
|
|
680
765
|
try {
|
|
681
|
-
const id = c.addEdge(input);
|
|
682
|
-
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);
|
|
683
768
|
return {
|
|
684
769
|
content: [{
|
|
685
770
|
type: 'text',
|
|
@@ -702,7 +787,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
702
787
|
{ id: z.string().describe('Edge ID to remove') },
|
|
703
788
|
async ({ id }) => {
|
|
704
789
|
const c = await ensureCanvas();
|
|
705
|
-
c.removeEdge(id);
|
|
790
|
+
await c.removeEdge(id);
|
|
706
791
|
return {
|
|
707
792
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, removed: id }) }],
|
|
708
793
|
};
|
|
@@ -718,7 +803,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
718
803
|
},
|
|
719
804
|
async ({ layout }) => {
|
|
720
805
|
const c = await ensureCanvas();
|
|
721
|
-
c.arrange(layout ?? 'grid');
|
|
806
|
+
await c.arrange(layout ?? 'grid');
|
|
722
807
|
return {
|
|
723
808
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: layout ?? 'grid' }) }],
|
|
724
809
|
};
|
|
@@ -738,7 +823,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
738
823
|
},
|
|
739
824
|
async ({ id, noPan }) => {
|
|
740
825
|
const c = await ensureCanvas();
|
|
741
|
-
const result = c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
826
|
+
const result = await c.focusNode(id, { ...(noPan === true ? { noPan: true } : {}) });
|
|
742
827
|
if (!result) {
|
|
743
828
|
return {
|
|
744
829
|
content: [
|
|
@@ -772,7 +857,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
772
857
|
},
|
|
773
858
|
async (input) => {
|
|
774
859
|
const c = await ensureCanvas();
|
|
775
|
-
const result = c.fitView({
|
|
860
|
+
const result = await c.fitView({
|
|
776
861
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
777
862
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
778
863
|
...(typeof input.padding === 'number' ? { padding: input.padding } : {}),
|
|
@@ -792,7 +877,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
792
877
|
{},
|
|
793
878
|
async () => {
|
|
794
879
|
const c = await ensureCanvas();
|
|
795
|
-
c.clear();
|
|
880
|
+
await c.clear();
|
|
796
881
|
return {
|
|
797
882
|
content: [{ type: 'text', text: JSON.stringify({ ok: true, cleared: true }) }],
|
|
798
883
|
};
|
|
@@ -808,8 +893,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
808
893
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
809
894
|
},
|
|
810
895
|
async ({ query, limit }) => {
|
|
811
|
-
await ensureCanvas();
|
|
812
|
-
const results =
|
|
896
|
+
const c = await ensureCanvas();
|
|
897
|
+
const results = await c.search(query);
|
|
813
898
|
const capped = results.slice(0, limit ?? 10);
|
|
814
899
|
return {
|
|
815
900
|
content: [{
|
|
@@ -828,8 +913,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
828
913
|
async () => {
|
|
829
914
|
const c = await ensureCanvas();
|
|
830
915
|
const result = await c.undo();
|
|
916
|
+
const history = await c.getHistory();
|
|
831
917
|
return {
|
|
832
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
918
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
833
919
|
};
|
|
834
920
|
},
|
|
835
921
|
);
|
|
@@ -842,8 +928,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
842
928
|
async () => {
|
|
843
929
|
const c = await ensureCanvas();
|
|
844
930
|
const result = await c.redo();
|
|
931
|
+
const history = await c.getHistory();
|
|
845
932
|
return {
|
|
846
|
-
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo:
|
|
933
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, canUndo: history.canUndo, canRedo: history.canRedo }) }],
|
|
847
934
|
};
|
|
848
935
|
},
|
|
849
936
|
);
|
|
@@ -856,15 +943,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
856
943
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
857
944
|
},
|
|
858
945
|
async ({ snapshot }) => {
|
|
859
|
-
await ensureCanvas();
|
|
860
|
-
const
|
|
861
|
-
if (!
|
|
946
|
+
const c = await ensureCanvas();
|
|
947
|
+
const result = await c.diffSnapshot(snapshot);
|
|
948
|
+
if (!result.ok) {
|
|
862
949
|
return { content: [{ type: 'text', text: `Snapshot "${snapshot}" not found. Use canvas_snapshot to save one first.` }], isError: true };
|
|
863
950
|
}
|
|
864
|
-
const current = canvasState.getLayout();
|
|
865
|
-
const diff = diffLayouts(snapData.name, snapData, current);
|
|
866
951
|
return {
|
|
867
|
-
content: [{ type: 'text', text:
|
|
952
|
+
content: [{ type: 'text', text: result.text ?? '' }],
|
|
868
953
|
};
|
|
869
954
|
},
|
|
870
955
|
);
|
|
@@ -877,7 +962,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
877
962
|
async () => {
|
|
878
963
|
const c = await ensureCanvas();
|
|
879
964
|
return {
|
|
880
|
-
content: [{ type: 'text', text: JSON.stringify(c.getAutomationWebViewStatus(), null, 2) }],
|
|
965
|
+
content: [{ type: 'text', text: JSON.stringify(await c.getAutomationWebViewStatus(), null, 2) }],
|
|
881
966
|
};
|
|
882
967
|
},
|
|
883
968
|
);
|
|
@@ -927,13 +1012,14 @@ export async function startMcpServer(): Promise<void> {
|
|
|
927
1012
|
const c = await ensureCanvas();
|
|
928
1013
|
try {
|
|
929
1014
|
const stopped = await c.stopAutomationWebView();
|
|
1015
|
+
const webview = await c.getAutomationWebViewStatus();
|
|
930
1016
|
return {
|
|
931
1017
|
content: [{
|
|
932
1018
|
type: 'text',
|
|
933
1019
|
text: JSON.stringify({
|
|
934
1020
|
ok: true,
|
|
935
1021
|
stopped,
|
|
936
|
-
webview
|
|
1022
|
+
webview,
|
|
937
1023
|
}, null, 2),
|
|
938
1024
|
}],
|
|
939
1025
|
};
|
|
@@ -1017,7 +1103,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1017
1103
|
...(format ? { format } : {}),
|
|
1018
1104
|
...(typeof quality === 'number' ? { quality } : {}),
|
|
1019
1105
|
});
|
|
1020
|
-
const status = c.getAutomationWebViewStatus();
|
|
1106
|
+
const status = await c.getAutomationWebViewStatus();
|
|
1021
1107
|
return {
|
|
1022
1108
|
content: [
|
|
1023
1109
|
{
|
|
@@ -1092,8 +1178,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1092
1178
|
},
|
|
1093
1179
|
async () => {
|
|
1094
1180
|
const c = await ensureCanvas();
|
|
1095
|
-
const pinnedIds =
|
|
1096
|
-
const layout = c.getLayout();
|
|
1181
|
+
const pinnedIds = new Set(await c.getPinnedNodeIds());
|
|
1182
|
+
const layout = await c.getLayout();
|
|
1097
1183
|
|
|
1098
1184
|
const pinnedNodes = layout.nodes.filter((n) => pinnedIds.has(n.id));
|
|
1099
1185
|
const pinnedEdges = layout.edges.filter(
|
|
@@ -1147,7 +1233,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1147
1233
|
},
|
|
1148
1234
|
async () => {
|
|
1149
1235
|
const c = await ensureCanvas();
|
|
1150
|
-
const layout = serializeCanvasLayout(c.getLayout());
|
|
1236
|
+
const layout = serializeCanvasLayout(await c.getLayout());
|
|
1151
1237
|
return {
|
|
1152
1238
|
contents: [
|
|
1153
1239
|
{
|
|
@@ -1170,13 +1256,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1170
1256
|
mimeType: 'application/json',
|
|
1171
1257
|
},
|
|
1172
1258
|
async () => {
|
|
1173
|
-
await ensureCanvas();
|
|
1259
|
+
const c = await ensureCanvas();
|
|
1174
1260
|
return {
|
|
1175
1261
|
contents: [
|
|
1176
1262
|
{
|
|
1177
1263
|
uri: 'canvas://summary',
|
|
1178
1264
|
mimeType: 'application/json',
|
|
1179
|
-
text: JSON.stringify(
|
|
1265
|
+
text: JSON.stringify(buildSummaryFromLayout(await c.getLayout(), await c.getPinnedNodeIds()), null, 2),
|
|
1180
1266
|
},
|
|
1181
1267
|
],
|
|
1182
1268
|
};
|
|
@@ -1196,9 +1282,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1196
1282
|
mimeType: 'application/json',
|
|
1197
1283
|
},
|
|
1198
1284
|
async () => {
|
|
1199
|
-
await ensureCanvas();
|
|
1200
|
-
const layout =
|
|
1201
|
-
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()));
|
|
1202
1288
|
return {
|
|
1203
1289
|
contents: [
|
|
1204
1290
|
{
|
|
@@ -1222,13 +1308,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1222
1308
|
mimeType: 'text/plain',
|
|
1223
1309
|
},
|
|
1224
1310
|
async () => {
|
|
1225
|
-
await ensureCanvas();
|
|
1311
|
+
const c = await ensureCanvas();
|
|
1226
1312
|
return {
|
|
1227
1313
|
contents: [
|
|
1228
1314
|
{
|
|
1229
1315
|
uri: 'canvas://history',
|
|
1230
1316
|
mimeType: 'text/plain',
|
|
1231
|
-
text:
|
|
1317
|
+
text: (await c.getHistory()).text,
|
|
1232
1318
|
},
|
|
1233
1319
|
],
|
|
1234
1320
|
};
|
|
@@ -1247,8 +1333,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1247
1333
|
mimeType: 'application/json',
|
|
1248
1334
|
},
|
|
1249
1335
|
async () => {
|
|
1250
|
-
await ensureCanvas();
|
|
1251
|
-
const summary =
|
|
1336
|
+
const c = await ensureCanvas();
|
|
1337
|
+
const summary = (await c.getCodeGraph()).summary;
|
|
1252
1338
|
return {
|
|
1253
1339
|
contents: [
|
|
1254
1340
|
{
|
|
@@ -1339,9 +1425,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1339
1425
|
},
|
|
1340
1426
|
async (input) => {
|
|
1341
1427
|
const c = await ensureCanvas();
|
|
1342
|
-
const id = c.createGroup(input);
|
|
1428
|
+
const id = await c.createGroup(input);
|
|
1343
1429
|
return {
|
|
1344
|
-
content: [{ type: 'text', text: JSON.stringify(createdNodePayload(c, id), null, 2) }],
|
|
1430
|
+
content: [{ type: 'text', text: JSON.stringify(await createdNodePayload(c, id), null, 2) }],
|
|
1345
1431
|
};
|
|
1346
1432
|
},
|
|
1347
1433
|
);
|
|
@@ -1357,7 +1443,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1357
1443
|
},
|
|
1358
1444
|
async ({ groupId, childIds, childLayout }) => {
|
|
1359
1445
|
const c = await ensureCanvas();
|
|
1360
|
-
const ok = c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1446
|
+
const ok = await c.groupNodes(groupId, childIds, childLayout ? { childLayout } : undefined);
|
|
1361
1447
|
if (!ok) {
|
|
1362
1448
|
return { content: [{ type: 'text', text: 'Group not found or no valid children.' }], isError: true };
|
|
1363
1449
|
}
|
|
@@ -1392,7 +1478,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1392
1478
|
async () => {
|
|
1393
1479
|
const c = await ensureCanvas();
|
|
1394
1480
|
return {
|
|
1395
|
-
content: [{ type: 'text', text: JSON.stringify(c.validate(), null, 2) }],
|
|
1481
|
+
content: [{ type: 'text', text: JSON.stringify(await c.validate(), null, 2) }],
|
|
1396
1482
|
};
|
|
1397
1483
|
},
|
|
1398
1484
|
);
|
|
@@ -1406,7 +1492,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1406
1492
|
},
|
|
1407
1493
|
async ({ groupId }) => {
|
|
1408
1494
|
const c = await ensureCanvas();
|
|
1409
|
-
const ok = c.ungroupNodes(groupId);
|
|
1495
|
+
const ok = await c.ungroupNodes(groupId);
|
|
1410
1496
|
if (!ok) {
|
|
1411
1497
|
return { content: [{ type: 'text', text: 'Group not found or already empty.' }], isError: true };
|
|
1412
1498
|
}
|
|
@@ -1425,9 +1511,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1425
1511
|
},
|
|
1426
1512
|
async ({ nodeIds, mode }) => {
|
|
1427
1513
|
const c = await ensureCanvas();
|
|
1428
|
-
const result = c.setContextPins(nodeIds, mode ?? 'set');
|
|
1429
|
-
|
|
1430
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1514
|
+
const result = await c.setContextPins(nodeIds, mode ?? 'set');
|
|
1431
1515
|
|
|
1432
1516
|
return {
|
|
1433
1517
|
content: [{
|
|
@@ -1450,7 +1534,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1450
1534
|
},
|
|
1451
1535
|
async (input) => {
|
|
1452
1536
|
const c = await ensureCanvas();
|
|
1453
|
-
const snapshot = c.saveSnapshot(input.name);
|
|
1537
|
+
const snapshot = await c.saveSnapshot(input.name);
|
|
1454
1538
|
if (!snapshot) {
|
|
1455
1539
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Failed to save snapshot' }) }] };
|
|
1456
1540
|
}
|
|
@@ -1466,7 +1550,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1466
1550
|
async () => {
|
|
1467
1551
|
const c = await ensureCanvas();
|
|
1468
1552
|
return {
|
|
1469
|
-
content: [{ type: 'text', text: JSON.stringify({ snapshots: c.listSnapshots() }, null, 2) }],
|
|
1553
|
+
content: [{ type: 'text', text: JSON.stringify({ snapshots: await c.listSnapshots() }, null, 2) }],
|
|
1470
1554
|
};
|
|
1471
1555
|
},
|
|
1472
1556
|
);
|
|
@@ -1484,9 +1568,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1484
1568
|
if (!result.ok) {
|
|
1485
1569
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }] };
|
|
1486
1570
|
}
|
|
1487
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1488
1571
|
return {
|
|
1489
|
-
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: serializeCanvasLayout(
|
|
1572
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, layout: serializeCanvasLayout(await c.getLayout()) }) }],
|
|
1490
1573
|
};
|
|
1491
1574
|
},
|
|
1492
1575
|
);
|
|
@@ -1500,7 +1583,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1500
1583
|
},
|
|
1501
1584
|
async ({ id }) => {
|
|
1502
1585
|
const c = await ensureCanvas();
|
|
1503
|
-
const result = c.deleteSnapshot(id);
|
|
1586
|
+
const result = await c.deleteSnapshot(id);
|
|
1504
1587
|
if (!result.ok) {
|
|
1505
1588
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }], isError: true };
|
|
1506
1589
|
}
|
|
@@ -1510,24 +1593,6 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1510
1593
|
},
|
|
1511
1594
|
);
|
|
1512
1595
|
|
|
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
1596
|
// Connect via stdio
|
|
1532
1597
|
const transport = new StdioServerTransport();
|
|
1533
1598
|
await server.connect(transport);
|
package/src/server/index.ts
CHANGED
|
@@ -97,7 +97,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
97
97
|
open?: boolean;
|
|
98
98
|
automationWebView?: boolean | CanvasAutomationWebViewOptions;
|
|
99
99
|
}): Promise<void> {
|
|
100
|
-
const base = startCanvasServer({ port: this._port });
|
|
100
|
+
const base = startCanvasServer({ port: this._port, allowPortFallback: false });
|
|
101
101
|
if (!base) {
|
|
102
102
|
throw new Error(`Failed to start canvas server on port ${this._port}`);
|
|
103
103
|
}
|
|
@@ -605,7 +605,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
605
605
|
async startAutomationWebView(
|
|
606
606
|
options: CanvasAutomationWebViewOptions = {},
|
|
607
607
|
): Promise<CanvasAutomationWebViewStatus> {
|
|
608
|
-
const base = this._server ?? startCanvasServer({ port: this._port });
|
|
608
|
+
const base = this._server ?? startCanvasServer({ port: this._port, allowPortFallback: false });
|
|
609
609
|
if (!base) {
|
|
610
610
|
throw new Error(`Failed to start canvas server on port ${this._port}`);
|
|
611
611
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -3727,6 +3727,7 @@ export interface CanvasServerOptions {
|
|
|
3727
3727
|
port?: number;
|
|
3728
3728
|
workspaceRoot?: string;
|
|
3729
3729
|
autoOpenBrowser?: boolean;
|
|
3730
|
+
allowPortFallback?: boolean;
|
|
3730
3731
|
}
|
|
3731
3732
|
|
|
3732
3733
|
export function startCanvasServer(options: CanvasServerOptions = {}): string | null {
|
|
@@ -3764,7 +3765,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3764
3765
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3765
3766
|
|
|
3766
3767
|
const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
|
|
3767
|
-
const portCandidates =
|
|
3768
|
+
const portCandidates = options.allowPortFallback === false
|
|
3769
|
+
? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
|
|
3770
|
+
: buildPortCandidates(preferredPort);
|
|
3768
3771
|
|
|
3769
3772
|
for (const portCandidate of portCandidates) {
|
|
3770
3773
|
try {
|