pmx-canvas 0.1.13 → 0.1.14

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.
@@ -33,6 +33,8 @@ import {
33
33
  ungroupCanvasNodes,
34
34
  validateCanvasNodePatch,
35
35
  hasStructuredNodeUpdateFields,
36
+ hasTraceNodeDataFields,
37
+ mergeTraceNodeDataFields,
36
38
  } from './canvas-operations.js';
37
39
  import { validateCanvasLayout } from './canvas-validation.js';
38
40
  import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
@@ -158,6 +160,12 @@ export class PmxCanvas extends EventEmitter {
158
160
  type: CanvasNodeState['type'];
159
161
  title?: string;
160
162
  content?: string;
163
+ toolName?: string;
164
+ category?: string;
165
+ status?: string;
166
+ duration?: string;
167
+ resultSummary?: string;
168
+ error?: string;
161
169
  x?: number;
162
170
  y?: number;
163
171
  width?: number;
@@ -243,9 +251,10 @@ export class PmxCanvas extends EventEmitter {
243
251
  patch.title !== undefined ||
244
252
  patch.content !== undefined ||
245
253
  typeof patch.arrangeLocked === 'boolean' ||
246
- typeof patch.strictSize === 'boolean'
254
+ typeof patch.strictSize === 'boolean' ||
255
+ (existing.type === 'trace' && hasTraceNodeDataFields(patch))
247
256
  ) {
248
- resolvedPatch.data = {
257
+ const nextData = {
249
258
  ...existing.data,
250
259
  ...(patch.data && typeof patch.data === 'object' && !Array.isArray(patch.data) ? patch.data : {}),
251
260
  ...(typeof patch.title === 'string' ? { title: patch.title } : {}),
@@ -253,6 +262,9 @@ export class PmxCanvas extends EventEmitter {
253
262
  ...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
254
263
  ...(typeof patch.strictSize === 'boolean' ? { strictSize: patch.strictSize } : {}),
255
264
  };
265
+ resolvedPatch.data = existing.type === 'trace'
266
+ ? mergeTraceNodeDataFields(nextData, patch)
267
+ : nextData;
256
268
  }
257
269
 
258
270
  const error = validateCanvasNodePatch({
@@ -520,21 +532,36 @@ export class PmxCanvas extends EventEmitter {
520
532
  transport: ExternalMcpTransportConfig;
521
533
  toolName: string;
522
534
  toolArguments?: Record<string, unknown>;
535
+ nodeId?: string;
523
536
  serverName?: string;
524
537
  title?: string;
525
538
  x?: number;
526
539
  y?: number;
527
540
  width?: number;
528
541
  height?: number;
542
+ timeoutMs?: number;
529
543
  }): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
544
+ const targetNode = input.nodeId ? canvasState.getNode(input.nodeId) : undefined;
545
+ if (input.nodeId && !targetNode) {
546
+ throw new Error(`Node "${input.nodeId}" not found.`);
547
+ }
548
+ if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
549
+ throw new Error(`Node "${input.nodeId}" is not an external app node.`);
550
+ }
551
+
530
552
  const opened = await openExternalMcpApp({
531
553
  transport: input.transport,
532
554
  toolName: input.toolName,
533
555
  ...(input.toolArguments ? { toolArguments: input.toolArguments } : {}),
534
556
  ...(input.serverName ? { serverName: input.serverName } : {}),
557
+ ...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
535
558
  });
536
559
  const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
537
- const nodeIdSeed = `ext-app-${toolCallId}`;
560
+ const previousSessionId = targetNode?.data.appSessionId;
561
+ if (typeof previousSessionId === 'string' && previousSessionId.trim().length > 0) {
562
+ closeMcpAppSession(previousSessionId);
563
+ }
564
+ const nodeIdSeed = input.nodeId ?? `ext-app-${toolCallId}`;
538
565
  const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
539
566
  ? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
540
567
  : opened.toolResult;
@@ -566,7 +593,7 @@ export class PmxCanvas extends EventEmitter {
566
593
  success: toolResult.isError !== true,
567
594
  result: toolResult,
568
595
  });
569
- const nodeId = this.findCanvasExtAppNodeId(toolCallId);
596
+ const nodeId = input.nodeId ?? this.findCanvasExtAppNodeId(toolCallId);
570
597
  return {
571
598
  ok: true,
572
599
  ...(nodeId ? { id: nodeId } : {}),
@@ -12,6 +12,7 @@ import type {
12
12
  TextResourceContents,
13
13
  Tool,
14
14
  } from '@modelcontextprotocol/sdk/types.js';
15
+ import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
15
16
  import {
16
17
  EXTENSION_ID,
17
18
  RESOURCE_MIME_TYPE,
@@ -47,6 +48,7 @@ export interface OpenMcpAppInput {
47
48
  toolName: string;
48
49
  toolArguments?: Record<string, unknown>;
49
50
  serverName?: string;
51
+ timeoutMs?: number;
50
52
  }
51
53
 
52
54
  export interface OpenMcpAppResult {
@@ -184,6 +186,12 @@ function normalizeServerName(raw: string | undefined, transport: ExternalMcpTran
184
186
  return trimmed.length > 0 ? trimmed : defaultServerName(transport);
185
187
  }
186
188
 
189
+ function requestOptions(timeoutMs: number | undefined): RequestOptions | undefined {
190
+ return typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
191
+ ? { timeout: timeoutMs }
192
+ : undefined;
193
+ }
194
+
187
195
  function buildTransport(config: ExternalMcpTransportConfig): RuntimeTransport {
188
196
  if (config.type === 'http') {
189
197
  return new StreamableHTTPClientTransport(new URL(config.url), {
@@ -209,15 +217,16 @@ function buildTransport(config: ExternalMcpTransportConfig): RuntimeTransport {
209
217
  async function createSession(
210
218
  transportConfig: ExternalMcpTransportConfig,
211
219
  serverName?: string,
220
+ timeoutMs?: number,
212
221
  ): Promise<McpAppSession> {
213
222
  const transport = buildTransport(transportConfig);
214
223
  const client = new Client(
215
224
  { name: 'pmx-canvas-app-host', version: '0.1.0' },
216
225
  { capabilities: clientCapabilities },
217
226
  );
218
- await client.connect(transport);
227
+ await client.connect(transport, requestOptions(timeoutMs));
219
228
 
220
- const toolList = await client.listTools();
229
+ const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
221
230
  const session: McpAppSession = {
222
231
  id: randomId('mcp-app-session'),
223
232
  serverName: normalizeServerName(serverName, transportConfig),
@@ -350,7 +359,8 @@ function prepareResourceHtml(html: string, meta: McpUiResourceMeta | undefined):
350
359
  }
351
360
 
352
361
  export async function openMcpApp(input: OpenMcpAppInput): Promise<OpenMcpAppResult> {
353
- const session = await createSession(input.transport, input.serverName);
362
+ const options = requestOptions(input.timeoutMs);
363
+ const session = await createSession(input.transport, input.serverName, input.timeoutMs);
354
364
  try {
355
365
  const tool = await findTool(session, input.toolName);
356
366
  const resourceUri = getToolUiResourceUri(tool);
@@ -362,9 +372,9 @@ export async function openMcpApp(input: OpenMcpAppInput): Promise<OpenMcpAppResu
362
372
  const rawToolResult = await session.client.callTool({
363
373
  name: tool.name,
364
374
  arguments: toolInput,
365
- });
375
+ }, undefined, options);
366
376
  const toolResult = normalizeExtAppToolResult({ result: rawToolResult });
367
- const readResult = await session.client.readResource({ uri: resourceUri });
377
+ const readResult = await session.client.readResource({ uri: resourceUri }, options);
368
378
  const resourceMeta = resourceMetaFromReadResult(readResult);
369
379
  const html = prepareResourceHtml(htmlContentFromReadResult(readResult, resourceUri), resourceMeta);
370
380
 
@@ -96,6 +96,8 @@ import {
96
96
  ungroupCanvasNodes,
97
97
  validateCanvasNodePatch,
98
98
  hasStructuredNodeUpdateFields,
99
+ hasTraceNodeDataFields,
100
+ mergeTraceNodeDataFields,
99
101
  } from './canvas-operations.js';
100
102
  import { validateCanvasLayout } from './canvas-validation.js';
101
103
  import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
@@ -1331,6 +1333,12 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
1331
1333
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
1332
1334
  ...(typeof content === 'string' ? { content } : {}),
1333
1335
  ...(extraData ? { data: extraData } : {}),
1336
+ ...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
1337
+ ...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
1338
+ ...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
1339
+ ...(type === 'trace' && typeof body.duration === 'string' ? { duration: body.duration } : {}),
1340
+ ...(type === 'trace' && typeof body.resultSummary === 'string' ? { resultSummary: body.resultSummary } : {}),
1341
+ ...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
1334
1342
  ...(body.strictSize === true ? { strictSize: true } : {}),
1335
1343
  ...geometry,
1336
1344
  defaultWidth: 360,
@@ -1495,7 +1503,14 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
1495
1503
  } catch (error) {
1496
1504
  return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1497
1505
  }
1498
- } else if (body.title !== undefined || body.content !== undefined || body.data || typeof body.arrangeLocked === 'boolean' || typeof body.strictSize === 'boolean') {
1506
+ } else if (
1507
+ body.title !== undefined ||
1508
+ body.content !== undefined ||
1509
+ body.data ||
1510
+ typeof body.arrangeLocked === 'boolean' ||
1511
+ typeof body.strictSize === 'boolean' ||
1512
+ (existing.type === 'trace' && hasTraceNodeDataFields(body))
1513
+ ) {
1499
1514
  const data = { ...existing.data };
1500
1515
  if (body.title !== undefined) {
1501
1516
  data.title = String(body.title);
@@ -1524,7 +1539,9 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
1524
1539
  }
1525
1540
  }
1526
1541
  }
1527
- patch.data = data;
1542
+ patch.data = existing.type === 'trace'
1543
+ ? mergeTraceNodeDataFields(data, body)
1544
+ : data;
1528
1545
  }
1529
1546
  const error = validateCanvasNodePatch({
1530
1547
  ...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
@@ -2111,29 +2128,44 @@ interface RunAndEmitOpenMcpAppParams {
2111
2128
  transport: ExternalMcpTransportConfig;
2112
2129
  toolName: string;
2113
2130
  toolArguments?: Record<string, unknown>;
2131
+ nodeId?: string;
2114
2132
  serverName?: string;
2115
2133
  title?: string;
2116
2134
  x?: number;
2117
2135
  y?: number;
2118
2136
  width?: number;
2119
2137
  height?: number;
2138
+ timeoutMs?: number;
2120
2139
  }
2121
2140
 
2122
2141
  async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise<Response> {
2123
2142
  try {
2143
+ const targetNode = params.nodeId ? canvasState.getNode(params.nodeId) : undefined;
2144
+ if (params.nodeId && !targetNode) {
2145
+ return responseJson({ ok: false, error: `Node "${params.nodeId}" not found.` }, 404);
2146
+ }
2147
+ if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
2148
+ return responseJson({ ok: false, error: `Node "${params.nodeId}" is not an external app node.` }, 400);
2149
+ }
2150
+
2124
2151
  const opened = await openMcpApp({
2125
2152
  transport: params.transport,
2126
2153
  toolName: params.toolName,
2127
2154
  ...(params.toolArguments ? { toolArguments: params.toolArguments } : {}),
2128
2155
  ...(params.serverName ? { serverName: params.serverName } : {}),
2156
+ ...(typeof params.timeoutMs === 'number' ? { timeoutMs: params.timeoutMs } : {}),
2129
2157
  });
2130
2158
 
2131
2159
  const toolCallId = randomExtAppToolCallId();
2132
- const nodeIdSeed = toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`;
2160
+ if (params.nodeId) closeNodeAppSession(targetNode);
2161
+ const nodeIdSeed = params.nodeId ?? (toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`);
2133
2162
  const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
2134
2163
  ? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
2135
2164
  : opened.toolResult;
2136
- const nodeTitle = params.title ?? opened.tool.title ?? opened.tool.name;
2165
+ const nodeTitle = params.title
2166
+ ?? (typeof targetNode?.data.title === 'string' ? targetNode.data.title : undefined)
2167
+ ?? opened.tool.title
2168
+ ?? opened.tool.name;
2137
2169
 
2138
2170
  emitPrimaryWorkbenchEvent('ext-app-open', {
2139
2171
  toolCallId,
@@ -2163,7 +2195,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
2163
2195
  success: toolResult.isError !== true,
2164
2196
  result: toolResult,
2165
2197
  });
2166
- const nodeId = findCanvasExtAppNodeId(toolCallId);
2198
+ const nodeId = params.nodeId ?? findCanvasExtAppNodeId(toolCallId);
2167
2199
 
2168
2200
  return responseJson({
2169
2201
  ok: true,
@@ -2202,17 +2234,22 @@ async function handleCanvasOpenMcpApp(req: Request): Promise<Response> {
2202
2234
  const requestedServerName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
2203
2235
  ? body.serverName.trim()
2204
2236
  : undefined;
2237
+ const requestedNodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
2238
+ ? body.nodeId.trim()
2239
+ : undefined;
2205
2240
 
2206
2241
  return runAndEmitOpenMcpApp({
2207
2242
  transport,
2208
2243
  toolName,
2209
2244
  ...(toolArguments ? { toolArguments } : {}),
2245
+ ...(requestedNodeId ? { nodeId: requestedNodeId } : {}),
2210
2246
  ...(requestedServerName ? { serverName: requestedServerName } : {}),
2211
2247
  ...(requestedTitle ? { title: requestedTitle } : {}),
2212
2248
  ...(typeof body.x === 'number' ? { x: body.x } : {}),
2213
2249
  ...(typeof body.y === 'number' ? { y: body.y } : {}),
2214
2250
  ...(typeof body.width === 'number' ? { width: body.width } : {}),
2215
2251
  ...(typeof body.height === 'number' ? { height: body.height } : {}),
2252
+ ...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
2216
2253
  });
2217
2254
  }
2218
2255
 
@@ -2222,11 +2259,13 @@ async function handleCanvasAddDiagram(req: Request): Promise<Response> {
2222
2259
  try {
2223
2260
  built = buildExcalidrawOpenMcpAppInput({
2224
2261
  elements: body.elements,
2262
+ ...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
2225
2263
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
2226
2264
  ...(typeof body.x === 'number' ? { x: body.x } : {}),
2227
2265
  ...(typeof body.y === 'number' ? { y: body.y } : {}),
2228
2266
  ...(typeof body.width === 'number' ? { width: body.width } : {}),
2229
2267
  ...(typeof body.height === 'number' ? { height: body.height } : {}),
2268
+ ...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
2230
2269
  });
2231
2270
  } catch (error) {
2232
2271
  return responseJson({
@@ -2239,11 +2278,13 @@ async function handleCanvasAddDiagram(req: Request): Promise<Response> {
2239
2278
  toolName: built.toolName,
2240
2279
  toolArguments: built.toolArguments,
2241
2280
  serverName: built.serverName,
2281
+ ...(built.nodeId ? { nodeId: built.nodeId } : {}),
2242
2282
  ...(built.title ? { title: built.title } : {}),
2243
2283
  ...(typeof built.x === 'number' ? { x: built.x } : {}),
2244
2284
  ...(typeof built.y === 'number' ? { y: built.y } : {}),
2245
2285
  ...(typeof built.width === 'number' ? { width: built.width } : {}),
2246
2286
  ...(typeof built.height === 'number' ? { height: built.height } : {}),
2287
+ ...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
2247
2288
  });
2248
2289
  }
2249
2290
 
@@ -3408,52 +3449,56 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3408
3449
  });
3409
3450
  }
3410
3451
  } else if (event === 'ext-app-update') {
3411
- const toolCallId = payload.toolCallId as string;
3412
- if (!toolCallId) return;
3413
- const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
3414
- const id =
3415
- (payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
3416
- findCanvasExtAppNodeId(toolCallId) ||
3417
- (typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
3418
- ? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
3419
- : null);
3420
- if (!id) return;
3421
- const existing = canvasState.getNode(id);
3422
- if (existing) {
3423
- canvasState.updateNode(id, { data: { ...existing.data, html: payload.html } });
3424
- }
3452
+ canvasState.withSuppressedRecording(() => {
3453
+ const toolCallId = payload.toolCallId as string;
3454
+ if (!toolCallId) return;
3455
+ const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
3456
+ const id =
3457
+ (payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
3458
+ findCanvasExtAppNodeId(toolCallId) ||
3459
+ (typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
3460
+ ? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
3461
+ : null);
3462
+ if (!id) return;
3463
+ const existing = canvasState.getNode(id);
3464
+ if (existing) {
3465
+ canvasState.updateNode(id, { data: { ...existing.data, html: payload.html } });
3466
+ }
3467
+ });
3425
3468
  } else if (event === 'ext-app-result') {
3426
- const toolCallId = payload.toolCallId as string;
3427
- if (!toolCallId) return;
3428
- const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
3429
- const id =
3430
- (payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
3431
- findCanvasExtAppNodeId(toolCallId) ||
3432
- (typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
3433
- ? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
3434
- : null);
3435
- if (!id) return;
3436
- if (payload.success === false) {
3437
- closeNodeAppSession(canvasState.getNode(id));
3438
- canvasState.removeNode(id);
3439
- return;
3440
- }
3441
- const existing = canvasState.getNode(id);
3442
- if (existing) {
3443
- canvasState.updateNode(id, {
3444
- data: {
3445
- ...existing.data,
3446
- toolResult: normalizeExtAppToolResult({
3447
- result: payload.result,
3448
- success: typeof payload.success === 'boolean' ? payload.success : undefined,
3449
- error: typeof payload.error === 'string' ? payload.error : undefined,
3450
- content: typeof payload.content === 'string' ? payload.content : undefined,
3451
- detailedContent:
3452
- typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
3453
- }),
3454
- },
3455
- });
3456
- }
3469
+ canvasState.withSuppressedRecording(() => {
3470
+ const toolCallId = payload.toolCallId as string;
3471
+ if (!toolCallId) return;
3472
+ const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
3473
+ const id =
3474
+ (payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
3475
+ findCanvasExtAppNodeId(toolCallId) ||
3476
+ (typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
3477
+ ? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
3478
+ : null);
3479
+ if (!id) return;
3480
+ if (payload.success === false) {
3481
+ closeNodeAppSession(canvasState.getNode(id));
3482
+ canvasState.removeNode(id);
3483
+ return;
3484
+ }
3485
+ const existing = canvasState.getNode(id);
3486
+ if (existing) {
3487
+ canvasState.updateNode(id, {
3488
+ data: {
3489
+ ...existing.data,
3490
+ toolResult: normalizeExtAppToolResult({
3491
+ result: payload.result,
3492
+ success: typeof payload.success === 'boolean' ? payload.success : undefined,
3493
+ error: typeof payload.error === 'string' ? payload.error : undefined,
3494
+ content: typeof payload.content === 'string' ? payload.content : undefined,
3495
+ detailedContent:
3496
+ typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
3497
+ }),
3498
+ },
3499
+ });
3500
+ }
3501
+ });
3457
3502
  } else if (event === 'context-cards') {
3458
3503
  syncContextNodeToCanvasState(
3459
3504
  { cards: Array.isArray(payload.cards) ? payload.cards : [] },