pmx-canvas 0.1.2 → 0.1.4
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 +144 -0
- package/Readme.md +35 -8
- package/dist/canvas/index.js +69 -69
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +12 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -1
- package/dist/types/client/types.d.ts +3 -0
- package/dist/types/json-render/charts/components.d.ts +2 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/diagram-presets.d.ts +13 -0
- package/dist/types/server/ext-app-lookup.d.ts +22 -0
- package/dist/types/server/index.d.ts +8 -1
- package/dist/types/server/web-artifacts.d.ts +1 -0
- package/package.json +2 -1
- package/skills/pmx-canvas/SKILL.md +35 -10
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +66 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +10 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +1 -1
- package/src/cli/agent.ts +114 -21
- package/src/cli/index.ts +3 -1
- package/src/client/App.tsx +2 -1
- package/src/client/canvas/CanvasNode.tsx +3 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +6 -1
- package/src/client/nodes/ExtAppFrame.tsx +97 -26
- package/src/client/state/canvas-store.ts +63 -1
- package/src/client/state/sse-bridge.ts +19 -4
- package/src/client/types.ts +12 -0
- package/src/json-render/charts/components.tsx +6 -4
- package/src/json-render/charts/extra-components.tsx +5 -5
- package/src/json-render/renderer/index.css +14 -0
- package/src/mcp/server.ts +44 -5
- package/src/server/canvas-operations.ts +43 -5
- package/src/server/canvas-schema.ts +16 -14
- package/src/server/canvas-serialization.ts +19 -1
- package/src/server/diagram-presets.ts +219 -4
- package/src/server/ext-app-lookup.ts +49 -0
- package/src/server/index.ts +33 -25
- package/src/server/server.ts +199 -45
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +10 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +1 -1
- package/src/server/web-artifacts.ts +44 -1
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import type { ExternalMcpTransportConfig } from './mcp-app-runtime.js';
|
|
2
3
|
|
|
3
4
|
export const EXCALIDRAW_MCP_URL = 'https://mcp.excalidraw.com/mcp';
|
|
4
5
|
export const EXCALIDRAW_SERVER_NAME = 'Excalidraw';
|
|
5
6
|
export const EXCALIDRAW_CREATE_VIEW_TOOL = 'create_view';
|
|
7
|
+
export const EXCALIDRAW_SAVE_CHECKPOINT_TOOL = 'save_checkpoint';
|
|
8
|
+
export const EXCALIDRAW_READ_CHECKPOINT_TOOL = 'read_checkpoint';
|
|
9
|
+
const EXCALIDRAW_CAMERA_PADDING = 80;
|
|
10
|
+
const EXCALIDRAW_MIN_CAMERA_WIDTH = 320;
|
|
11
|
+
const EXCALIDRAW_MIN_CAMERA_HEIGHT = 240;
|
|
12
|
+
const EXCALIDRAW_CAMERA_ASPECT_RATIO = 4 / 3;
|
|
13
|
+
const EXCALIDRAW_CAMERA_SIZES = [
|
|
14
|
+
{ width: 400, height: 300 },
|
|
15
|
+
{ width: 600, height: 450 },
|
|
16
|
+
{ width: 800, height: 600 },
|
|
17
|
+
{ width: 1200, height: 900 },
|
|
18
|
+
{ width: 1600, height: 1200 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_EXCALIDRAW_ELEMENTS: ReadonlyArray<Record<string, unknown>> = [
|
|
22
|
+
{
|
|
23
|
+
type: 'rectangle',
|
|
24
|
+
id: 'pmx-start',
|
|
25
|
+
x: 80,
|
|
26
|
+
y: 80,
|
|
27
|
+
width: 280,
|
|
28
|
+
height: 120,
|
|
29
|
+
roundness: { type: 3 },
|
|
30
|
+
backgroundColor: '#a5d8ff',
|
|
31
|
+
fillStyle: 'solid',
|
|
32
|
+
label: {
|
|
33
|
+
text: 'PMX Canvas',
|
|
34
|
+
fontSize: 24,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
6
38
|
|
|
7
39
|
export const EXCALIDRAW_MCP_TRANSPORT: ExternalMcpTransportConfig = {
|
|
8
40
|
type: 'http',
|
|
@@ -30,7 +62,11 @@ export interface ExcalidrawOpenMcpAppInput {
|
|
|
30
62
|
height?: number;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
|
-
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseExcalidrawElements(elements: unknown): Array<Record<string, unknown>> {
|
|
34
70
|
if (typeof elements === 'string') {
|
|
35
71
|
const trimmed = elements.trim();
|
|
36
72
|
if (!trimmed) {
|
|
@@ -46,16 +82,195 @@ export function normalizeExcalidrawElements(elements: unknown): string {
|
|
|
46
82
|
if (!Array.isArray(parsed)) {
|
|
47
83
|
throw new Error('diagram.elements string must encode a JSON array.');
|
|
48
84
|
}
|
|
49
|
-
return
|
|
85
|
+
return parsed.filter(isRecord);
|
|
50
86
|
}
|
|
87
|
+
|
|
51
88
|
if (Array.isArray(elements)) {
|
|
52
|
-
return
|
|
89
|
+
return elements.filter(isRecord);
|
|
53
90
|
}
|
|
91
|
+
|
|
54
92
|
throw new Error('diagram.elements must be a JSON array string or an array of Excalidraw elements.');
|
|
55
93
|
}
|
|
56
94
|
|
|
95
|
+
function parseExcalidrawCheckpointElements(data: unknown): Array<Record<string, unknown>> | null {
|
|
96
|
+
let parsed: unknown = data;
|
|
97
|
+
if (typeof data === 'string') {
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(data);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(parsed)) return parsed.filter(isRecord);
|
|
106
|
+
if (isRecord(parsed) && Array.isArray(parsed.elements)) return parsed.elements.filter(isRecord);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function finiteNumber(value: unknown): number | null {
|
|
111
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function elementHasCameraUpdate(elements: Array<Record<string, unknown>>): boolean {
|
|
115
|
+
return elements.some((element) => element.type === 'cameraUpdate');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveExcalidrawCameraSize(width: number, height: number): { width: number; height: number } {
|
|
119
|
+
const requiredWidth = Math.max(EXCALIDRAW_MIN_CAMERA_WIDTH, width);
|
|
120
|
+
const requiredHeight = Math.max(EXCALIDRAW_MIN_CAMERA_HEIGHT, height);
|
|
121
|
+
const standard = EXCALIDRAW_CAMERA_SIZES.find(
|
|
122
|
+
(size) => size.width >= requiredWidth && size.height >= requiredHeight,
|
|
123
|
+
);
|
|
124
|
+
if (standard) return standard;
|
|
125
|
+
|
|
126
|
+
const heightFromWidth = requiredWidth / EXCALIDRAW_CAMERA_ASPECT_RATIO;
|
|
127
|
+
const widthFromHeight = requiredHeight * EXCALIDRAW_CAMERA_ASPECT_RATIO;
|
|
128
|
+
const cameraWidth = Math.ceil(Math.max(requiredWidth, widthFromHeight));
|
|
129
|
+
return {
|
|
130
|
+
width: cameraWidth,
|
|
131
|
+
height: Math.ceil(cameraWidth / EXCALIDRAW_CAMERA_ASPECT_RATIO),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function inferExcalidrawCameraUpdate(
|
|
136
|
+
elements: Array<Record<string, unknown>>,
|
|
137
|
+
): Record<string, unknown> | null {
|
|
138
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
139
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
140
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
141
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
142
|
+
|
|
143
|
+
const includePoint = (x: number, y: number) => {
|
|
144
|
+
minX = Math.min(minX, x);
|
|
145
|
+
minY = Math.min(minY, y);
|
|
146
|
+
maxX = Math.max(maxX, x);
|
|
147
|
+
maxY = Math.max(maxY, y);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (const element of elements) {
|
|
151
|
+
if (element.isDeleted === true || element.type === 'cameraUpdate' || element.type === 'restoreCheckpoint' || element.type === 'delete') {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const x = finiteNumber(element.x);
|
|
156
|
+
const y = finiteNumber(element.y);
|
|
157
|
+
if (x === null || y === null) continue;
|
|
158
|
+
|
|
159
|
+
includePoint(x, y);
|
|
160
|
+
const width = finiteNumber(element.width) ?? 0;
|
|
161
|
+
const height = finiteNumber(element.height) ?? 0;
|
|
162
|
+
includePoint(x + width, y + height);
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(element.points)) {
|
|
165
|
+
for (const point of element.points) {
|
|
166
|
+
if (!Array.isArray(point)) continue;
|
|
167
|
+
const pointX = finiteNumber(point[0]);
|
|
168
|
+
const pointY = finiteNumber(point[1]);
|
|
169
|
+
if (pointX === null || pointY === null) continue;
|
|
170
|
+
includePoint(x + pointX, y + pointY);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const contentWidth = Math.max(1, maxX - minX);
|
|
180
|
+
const contentHeight = Math.max(1, maxY - minY);
|
|
181
|
+
const padding = Math.max(
|
|
182
|
+
EXCALIDRAW_CAMERA_PADDING,
|
|
183
|
+
Math.round(Math.max(contentWidth, contentHeight) * 0.18),
|
|
184
|
+
);
|
|
185
|
+
const camera = resolveExcalidrawCameraSize(contentWidth + padding * 2, contentHeight + padding * 2);
|
|
186
|
+
const centerX = minX + contentWidth / 2;
|
|
187
|
+
const centerY = minY + contentHeight / 2;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
type: 'cameraUpdate',
|
|
191
|
+
x: Math.round(centerX - camera.width / 2),
|
|
192
|
+
y: Math.round(centerY - camera.height / 2),
|
|
193
|
+
width: camera.width,
|
|
194
|
+
height: camera.height,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function withInferredCameraUpdate(
|
|
199
|
+
elements: Array<Record<string, unknown>>,
|
|
200
|
+
): Array<Record<string, unknown>> {
|
|
201
|
+
if (elementHasCameraUpdate(elements)) return elements;
|
|
202
|
+
const camera = inferExcalidrawCameraUpdate(elements);
|
|
203
|
+
return camera ? [camera, ...elements] : elements;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeExcalidrawElements(elements: unknown): string {
|
|
207
|
+
const parsed = parseExcalidrawElements(elements);
|
|
208
|
+
return JSON.stringify(parsed.length > 0 ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function normalizeExcalidrawElementsForToolInput(elements: unknown): string {
|
|
212
|
+
const parsed = parseExcalidrawElements(elements);
|
|
213
|
+
const seeded = parsed.length > 0 ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
|
|
214
|
+
return JSON.stringify(withInferredCameraUpdate(seeded));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function normalizeExcalidrawCheckpointDataForToolInput(data: unknown): string | null {
|
|
218
|
+
const elements = parseExcalidrawCheckpointElements(data);
|
|
219
|
+
|
|
220
|
+
return elements ? JSON.stringify(withInferredCameraUpdate(elements)) : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function buildExcalidrawRestoreCheckpointToolInput(checkpointId: string, data?: unknown): string {
|
|
224
|
+
const elements = parseExcalidrawCheckpointElements(data);
|
|
225
|
+
const camera = elements ? inferExcalidrawCameraUpdate(elements) : null;
|
|
226
|
+
return JSON.stringify([
|
|
227
|
+
{ type: 'restoreCheckpoint', id: checkpointId },
|
|
228
|
+
...(camera ? [camera] : []),
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function isExcalidrawCreateView(serverName: unknown, toolName: unknown): boolean {
|
|
233
|
+
return serverName === EXCALIDRAW_SERVER_NAME && toolName === EXCALIDRAW_CREATE_VIEW_TOOL;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildExcalidrawCheckpointId(seed: string): string {
|
|
237
|
+
const safe = seed.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96);
|
|
238
|
+
return `pmx-${safe || 'checkpoint'}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getExcalidrawCheckpointIdFromToolResult(result: unknown): string | null {
|
|
242
|
+
if (!isRecord(result) || !isRecord(result.structuredContent)) return null;
|
|
243
|
+
const checkpointId = result.structuredContent.checkpointId;
|
|
244
|
+
return typeof checkpointId === 'string' && checkpointId.trim().length > 0 ? checkpointId.trim() : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function withExcalidrawCheckpointId(
|
|
248
|
+
result: CallToolResult,
|
|
249
|
+
checkpointId: string,
|
|
250
|
+
): CallToolResult {
|
|
251
|
+
const structuredContent = isRecord(result.structuredContent) ? result.structuredContent : {};
|
|
252
|
+
return {
|
|
253
|
+
...result,
|
|
254
|
+
structuredContent: {
|
|
255
|
+
...structuredContent,
|
|
256
|
+
checkpointId,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function ensureExcalidrawCheckpointId(
|
|
262
|
+
result: CallToolResult,
|
|
263
|
+
seed: string,
|
|
264
|
+
checkpointId?: string | null,
|
|
265
|
+
): CallToolResult {
|
|
266
|
+
return withExcalidrawCheckpointId(
|
|
267
|
+
result,
|
|
268
|
+
checkpointId ?? getExcalidrawCheckpointIdFromToolResult(result) ?? buildExcalidrawCheckpointId(seed),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
57
272
|
export function buildExcalidrawOpenMcpAppInput(input: DiagramPresetOpenInput): ExcalidrawOpenMcpAppInput {
|
|
58
|
-
const elements =
|
|
273
|
+
const elements = normalizeExcalidrawElementsForToolInput(input.elements);
|
|
59
274
|
const out: ExcalidrawOpenMcpAppInput = {
|
|
60
275
|
transport: EXCALIDRAW_MCP_TRANSPORT,
|
|
61
276
|
toolName: EXCALIDRAW_CREATE_VIEW_TOOL,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the canvas node ID for a given ext-app `toolCallId`.
|
|
3
|
+
*
|
|
4
|
+
* v0.1.4 fixed a long-standing `ext-app-ext-app-…` double-prefix bug where
|
|
5
|
+
* both `nodeId` and `toolCallId` carried the `ext-app-` prefix. This helper
|
|
6
|
+
* encodes the lookup contract so it doesn't drift between the
|
|
7
|
+
* `PmxCanvas` SDK class and the HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. The direct prefixed form (`ext-app-<toolCallId>` if not already
|
|
11
|
+
* prefixed, otherwise `toolCallId` as-is).
|
|
12
|
+
* 2. The legacy `ext-app-ext-app-…` form, for canvases persisted before
|
|
13
|
+
* v0.1.4 and still on disk. Remove this fallback in v0.2.x.
|
|
14
|
+
* 3. A scan of the layout for any `mcp-app` ext-app node carrying that
|
|
15
|
+
* `toolCallId` in its data.
|
|
16
|
+
*/
|
|
17
|
+
import type { CanvasNodeState } from './canvas-state.js';
|
|
18
|
+
|
|
19
|
+
export interface ExtAppLookupSource {
|
|
20
|
+
getNode(id: string): CanvasNodeState | undefined;
|
|
21
|
+
listNodes(): readonly CanvasNodeState[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function findCanvasExtAppNodeId(
|
|
25
|
+
toolCallId: string,
|
|
26
|
+
source: ExtAppLookupSource,
|
|
27
|
+
): string | null {
|
|
28
|
+
const directId = toolCallId.startsWith('ext-app-')
|
|
29
|
+
? toolCallId
|
|
30
|
+
: `ext-app-${toolCallId}`;
|
|
31
|
+
if (source.getNode(directId)) return directId;
|
|
32
|
+
|
|
33
|
+
const legacyDirectId = `ext-app-${toolCallId}`;
|
|
34
|
+
if (legacyDirectId !== directId && source.getNode(legacyDirectId)) {
|
|
35
|
+
return legacyDirectId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const node of source.listNodes()) {
|
|
39
|
+
if (
|
|
40
|
+
node.type === 'mcp-app' &&
|
|
41
|
+
node.data.mode === 'ext-app' &&
|
|
42
|
+
node.data.toolCallId === toolCallId
|
|
43
|
+
) {
|
|
44
|
+
return node.id;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
|
|
3
3
|
import type { CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
|
|
4
|
+
import { findCanvasExtAppNodeId } from './ext-app-lookup.js';
|
|
4
5
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
5
6
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
6
7
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
} from './mcp-app-runtime.js';
|
|
45
46
|
import {
|
|
46
47
|
buildExcalidrawOpenMcpAppInput,
|
|
48
|
+
ensureExcalidrawCheckpointId,
|
|
49
|
+
isExcalidrawCreateView,
|
|
47
50
|
type DiagramPresetOpenInput,
|
|
48
51
|
} from './diagram-presets.js';
|
|
49
52
|
import {
|
|
@@ -323,16 +326,22 @@ export class PmxCanvas extends EventEmitter {
|
|
|
323
326
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
focusNode(id: string):
|
|
329
|
+
focusNode(id: string, options?: { noPan?: boolean }): { focused: string; panned: boolean } | null {
|
|
327
330
|
const node = canvasState.getNode(id);
|
|
328
|
-
if (!node) return;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
if (!node) return null;
|
|
332
|
+
const noPan = options?.noPan === true;
|
|
333
|
+
if (!noPan) {
|
|
334
|
+
canvasState.setViewport({
|
|
335
|
+
x: node.position.x - 100,
|
|
336
|
+
y: node.position.y - 100,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
emitPrimaryWorkbenchEvent('canvas-focus-node', { nodeId: id, noPan });
|
|
340
|
+
if (!noPan) {
|
|
341
|
+
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
342
|
+
}
|
|
335
343
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
344
|
+
return { focused: id, panned: !noPan };
|
|
336
345
|
}
|
|
337
346
|
|
|
338
347
|
getLayout(): CanvasLayout {
|
|
@@ -432,18 +441,10 @@ export class PmxCanvas extends EventEmitter {
|
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
private findCanvasExtAppNodeId(toolCallId: string): string | null {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
node.type === 'mcp-app' &&
|
|
440
|
-
node.data.mode === 'ext-app' &&
|
|
441
|
-
node.data.toolCallId === toolCallId
|
|
442
|
-
) {
|
|
443
|
-
return node.id;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return null;
|
|
444
|
+
return findCanvasExtAppNodeId(toolCallId, {
|
|
445
|
+
getNode: (id) => canvasState.getNode(id),
|
|
446
|
+
listNodes: () => canvasState.getLayout().nodes,
|
|
447
|
+
});
|
|
447
448
|
}
|
|
448
449
|
|
|
449
450
|
describeSchema() {
|
|
@@ -480,16 +481,21 @@ export class PmxCanvas extends EventEmitter {
|
|
|
480
481
|
y?: number;
|
|
481
482
|
width?: number;
|
|
482
483
|
height?: number;
|
|
483
|
-
}): Promise<{ ok: true; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
484
|
+
}): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
484
485
|
const opened = await openExternalMcpApp({
|
|
485
486
|
transport: input.transport,
|
|
486
487
|
toolName: input.toolName,
|
|
487
488
|
...(input.toolArguments ? { toolArguments: input.toolArguments } : {}),
|
|
488
489
|
...(input.serverName ? { serverName: input.serverName } : {}),
|
|
489
490
|
});
|
|
490
|
-
const toolCallId =
|
|
491
|
+
const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
492
|
+
const nodeIdSeed = `ext-app-${toolCallId}`;
|
|
493
|
+
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
494
|
+
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
495
|
+
: opened.toolResult;
|
|
491
496
|
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
492
497
|
toolCallId,
|
|
498
|
+
nodeId: nodeIdSeed,
|
|
493
499
|
title: input.title ?? opened.tool.title ?? opened.tool.name,
|
|
494
500
|
html: opened.html,
|
|
495
501
|
toolInput: opened.toolInput,
|
|
@@ -509,14 +515,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
509
515
|
});
|
|
510
516
|
emitPrimaryWorkbenchEvent('ext-app-result', {
|
|
511
517
|
toolCallId,
|
|
518
|
+
nodeId: nodeIdSeed,
|
|
512
519
|
serverName: opened.serverName,
|
|
513
520
|
toolName: opened.toolName,
|
|
514
|
-
success:
|
|
515
|
-
result:
|
|
521
|
+
success: toolResult.isError !== true,
|
|
522
|
+
result: toolResult,
|
|
516
523
|
});
|
|
517
524
|
const nodeId = this.findCanvasExtAppNodeId(toolCallId);
|
|
518
525
|
return {
|
|
519
526
|
ok: true,
|
|
527
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
520
528
|
nodeId,
|
|
521
529
|
toolCallId,
|
|
522
530
|
sessionId: opened.sessionId,
|
|
@@ -526,7 +534,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
526
534
|
|
|
527
535
|
async addDiagram(
|
|
528
536
|
input: DiagramPresetOpenInput,
|
|
529
|
-
): Promise<{ ok: true; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
537
|
+
): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
530
538
|
const built = buildExcalidrawOpenMcpAppInput(input);
|
|
531
539
|
return this.openMcpApp(built);
|
|
532
540
|
}
|