@shirbarzur/planform-mcp-server 1.0.4 → 1.0.6

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/server.ts CHANGED
@@ -12,11 +12,20 @@ import { Logger } from './logger.js';
12
12
  import { USAGE_EXAMPLES } from './usage-examples.js';
13
13
  import open from 'open';
14
14
 
15
+ /** UUID v4 pattern for distinguishing UUIDs from names/titles */
16
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
17
+ /** Node label pattern (e.g. n-1, n-2) */
18
+ const NODE_LABEL_REGEX = /^n-\d+$/;
19
+
15
20
  export class PlanformMCPServer {
16
21
  private server: Server;
17
22
  private apiClient: ApiClient;
18
23
  private logger: Logger;
19
24
  private activePolling: Map<string, NodeJS.Timeout> = new Map();
25
+ /** Set after successful sign_in; used for list_diagrams and create_diagram so callers need not pass user_uuid */
26
+ private sessionUserUuid: string | null = null;
27
+ /** Set after create_diagram or open_diagram; used for node/link tools so callers need not pass diagram_uuid */
28
+ private currentDiagramUuid: string | null = null;
20
29
 
21
30
  constructor() {
22
31
  this.server = new Server(
@@ -53,37 +62,37 @@ export class PlanformMCPServer {
53
62
  },
54
63
  },
55
64
  {
56
- name: 'get_usage_examples',
57
- description: 'Get usage examples and step-by-step instructions for the Planform MCP (auth flow, list diagrams, create diagram, etc.). Call this when unsure how to use the server.',
65
+ name: 'get_usage_guide',
66
+ description: 'Get step-by-step instructions and the recommended flow for the Planform MCP. Call when unsure how to use the server.',
58
67
  inputSchema: {
59
68
  type: 'object',
60
69
  properties: {},
61
70
  },
62
71
  },
63
72
  {
64
- name: 'device_start',
65
- description: 'REQUIRED FIRST before listing or managing diagrams. Start device code flow: opens browser for user to approve; after approval returns approved_user_uuid and stores the token. Use approved_user_uuid from the response as user_uuid for diagrams_list, diagram_create, etc.',
73
+ name: 'sign_in',
74
+ description: 'Call this first to sign in. Opens the browser for you to approve; after approval the server remembers your session. You do not need to pass or remember any IDs—later tools use your session and current diagram automatically.',
66
75
  inputSchema: {
67
76
  type: 'object',
68
77
  properties: {},
69
78
  },
70
79
  },
71
80
  {
72
- name: 'device_token_poll',
73
- description: '[Optional/Advanced] Manually poll for MCP JWT after user approves. Usually not needed since device_start polls automatically.',
81
+ name: 'poll_auth_token',
82
+ description: '[Optional/Advanced] Manually poll for auth token after user approves. Usually not needed since sign_in polls automatically.',
74
83
  inputSchema: {
75
84
  type: 'object',
76
85
  properties: {
77
86
  device_code: {
78
87
  type: 'string',
79
- description: 'Device code from device_start response',
88
+ description: 'Device code from sign_in response',
80
89
  },
81
90
  },
82
91
  required: ['device_code'],
83
92
  },
84
93
  },
85
94
  {
86
- name: 'backend_health_check',
95
+ name: 'check_backend_health',
87
96
  description: 'Check the health status of the Planform backend API',
88
97
  inputSchema: {
89
98
  type: 'object',
@@ -91,37 +100,11 @@ export class PlanformMCPServer {
91
100
  },
92
101
  },
93
102
  {
94
- name: 'diagram_create',
95
- description: 'Create a new diagram. Requires prior authentication via device_start; use approved_user_uuid from device_start response as user_uuid.',
96
- inputSchema: {
97
- type: 'object',
98
- properties: {
99
- user_uuid: {
100
- type: 'string',
101
- description: 'User UUID who owns the diagram (use approved_user_uuid from device_start response)',
102
- },
103
- title: {
104
- type: 'string',
105
- description: 'Diagram title',
106
- },
107
- type: {
108
- type: 'string',
109
- description: 'Diagram type (e.g., "uml.class")',
110
- },
111
- },
112
- required: ['user_uuid', 'title', 'type'],
113
- },
114
- },
115
- {
116
- name: 'diagrams_list',
117
- description: 'List all diagrams of the authenticated user. REQUIRES authentication first: call device_start, then use the approved_user_uuid from that response as user_uuid here.',
103
+ name: 'list_diagrams',
104
+ description: 'List your diagrams. Uses your session from sign_in—no IDs to pass. Optional: filter by status, page, page_size.',
118
105
  inputSchema: {
119
106
  type: 'object',
120
107
  properties: {
121
- user_uuid: {
122
- type: 'string',
123
- description: 'User UUID (use approved_user_uuid from device_start response after authenticating)',
124
- },
125
108
  status: {
126
109
  type: 'string',
127
110
  description: 'Filter by status (active|archived)',
@@ -138,41 +121,53 @@ export class PlanformMCPServer {
138
121
  default: 10,
139
122
  },
140
123
  },
141
- required: ['user_uuid'],
142
124
  },
143
125
  },
144
126
  {
145
- name: 'diagram_get',
146
- description: 'Fetch full diagram (nodes + links) to mirror state locally. Requires prior authentication via device_start.',
127
+ name: 'create_diagram',
128
+ description: 'Create a new diagram and set it as the current one. Uses your session—just pass title and type. After this you can add nodes and links without specifying a diagram.',
147
129
  inputSchema: {
148
130
  type: 'object',
149
131
  properties: {
150
- diagram_uuid: {
132
+ title: {
151
133
  type: 'string',
152
- description: 'Diagram UUID',
134
+ description: 'Diagram title (e.g. "My class diagram")',
135
+ },
136
+ type: {
137
+ type: 'string',
138
+ description: 'Diagram type (e.g. "uml.class")',
153
139
  },
154
140
  },
155
- required: ['diagram_uuid'],
141
+ required: ['title', 'type'],
156
142
  },
157
143
  },
158
144
  {
159
- name: 'nodes_create',
160
- description: 'Create UML node with immutable grid placement',
145
+ name: 'open_diagram',
146
+ description: 'Open a diagram by title or ID. If you omit the argument, re-loads the currently open diagram. The opened diagram becomes current for create_node and create_link.',
161
147
  inputSchema: {
162
148
  type: 'object',
163
149
  properties: {
164
- diagram_uuid: {
150
+ diagram_identifier: {
165
151
  type: 'string',
166
- description: 'Diagram UUID',
152
+ description: 'Diagram title (e.g. "My class diagram") or diagram ID. Omit to refresh the current diagram.',
167
153
  },
154
+ },
155
+ },
156
+ },
157
+ {
158
+ name: 'create_node',
159
+ description: 'Add a node (class, interface, or enum) to the current diagram. You only need kind and optionally name, fields, methods. The server uses the diagram you created or opened.',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
168
163
  kind: {
169
164
  type: 'string',
170
- description: 'Node kind',
165
+ description: 'Node kind: class, interface, or enum',
171
166
  enum: ['class', 'interface', 'enum'],
172
167
  },
173
168
  name: {
174
169
  type: 'string',
175
- description: 'Node name',
170
+ description: 'Node name (e.g. "User")',
176
171
  },
177
172
  fields: {
178
173
  type: 'array',
@@ -229,48 +224,40 @@ export class PlanformMCPServer {
229
224
  items: { type: 'string' },
230
225
  },
231
226
  },
232
- required: ['diagram_uuid', 'kind'],
227
+ required: ['kind'],
233
228
  },
234
229
  },
235
230
  {
236
- name: 'node_update',
237
- description: 'Update structural fields (MCP authoritative)',
231
+ name: 'update_node',
232
+ description: "Update a node's fields, methods, or name. Refer to the node by its name (e.g. 'User')—no IDs needed. Uses the current diagram.",
238
233
  inputSchema: {
239
234
  type: 'object',
240
235
  properties: {
241
- diagram_uuid: {
236
+ node_identifier: {
242
237
  type: 'string',
243
- description: 'Diagram UUID',
244
- },
245
- node_id: {
246
- type: 'string',
247
- description: 'Node ID',
238
+ description: 'Node name (e.g. "User") or node ID. The server resolves the name to the node in the current diagram.',
248
239
  },
249
240
  patch: {
250
241
  type: 'object',
251
- description: 'Node fields to update',
242
+ description: 'Fields to update (name, fields, methods, etc.)',
252
243
  },
253
244
  if_version: {
254
245
  type: 'number',
255
- description: 'Version for optimistic concurrency',
246
+ description: 'Version for optimistic concurrency (optional)',
256
247
  },
257
248
  },
258
- required: ['diagram_uuid', 'node_id', 'patch'],
249
+ required: ['node_identifier', 'patch'],
259
250
  },
260
251
  },
261
252
  {
262
- name: 'node_verify',
263
- description: 'Mark a node as verified by MCP after confirming structure in code',
253
+ name: 'verify_node',
254
+ description: 'Mark a node as verified (e.g. after confirming it matches code). Refer to the node by name (e.g. "User"). Uses the current diagram.',
264
255
  inputSchema: {
265
256
  type: 'object',
266
257
  properties: {
267
- diagram_uuid: {
268
- type: 'string',
269
- description: 'Diagram UUID',
270
- },
271
- node_id: {
258
+ node_identifier: {
272
259
  type: 'string',
273
- description: 'Node ID',
260
+ description: 'Node name (e.g. "User") or node ID.',
274
261
  },
275
262
  structural_fields: {
276
263
  type: 'object',
@@ -281,37 +268,29 @@ export class PlanformMCPServer {
281
268
  description: 'Whether differences were detected',
282
269
  },
283
270
  },
284
- required: ['diagram_uuid', 'node_id', 'structural_fields'],
271
+ required: ['node_identifier', 'structural_fields'],
285
272
  },
286
273
  },
287
274
  {
288
- name: 'node_delete',
289
- description: 'Delete node with status-based logic',
275
+ name: 'delete_node',
276
+ description: 'Delete a node from the current diagram. Refer to the node by its name (e.g. "User").',
290
277
  inputSchema: {
291
278
  type: 'object',
292
279
  properties: {
293
- diagram_uuid: {
280
+ node_identifier: {
294
281
  type: 'string',
295
- description: 'Diagram UUID',
296
- },
297
- node_id: {
298
- type: 'string',
299
- description: 'Node ID',
282
+ description: 'Node name (e.g. "User") or node ID.',
300
283
  },
301
284
  },
302
- required: ['diagram_uuid', 'node_id'],
285
+ required: ['node_identifier'],
303
286
  },
304
287
  },
305
288
  {
306
- name: 'links_create',
307
- description: 'Create link between nodes (never moves nodes). Note: from and to must be node labels (e.g. n-1, n-2), not UUIDs; using UUIDs returns 400.',
289
+ name: 'create_link',
290
+ description: 'Create a link between two nodes in the current diagram. Use node names (e.g. "User" and "Account") for from and to—the server resolves them. No diagram or node IDs needed.',
308
291
  inputSchema: {
309
292
  type: 'object',
310
293
  properties: {
311
- diagram_uuid: {
312
- type: 'string',
313
- description: 'Diagram UUID',
314
- },
315
294
  kind: {
316
295
  type: 'string',
317
296
  description: 'Link kind',
@@ -319,11 +298,11 @@ export class PlanformMCPServer {
319
298
  },
320
299
  from: {
321
300
  type: 'string',
322
- description: 'Source node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
301
+ description: 'Source node name (e.g. "User") or label (e.g. n-1).',
323
302
  },
324
303
  to: {
325
304
  type: 'string',
326
- description: 'Target node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
305
+ description: 'Target node name (e.g. "Account") or label (e.g. n-2).',
327
306
  },
328
307
  label: {
329
308
  type: 'string',
@@ -331,26 +310,22 @@ export class PlanformMCPServer {
331
310
  },
332
311
  directional: {
333
312
  type: 'string',
334
- description: 'Link directionality: "none", "unidirectional", or "bidirectional". For inheritance/implements/dependency links, must be "unidirectional" or "bidirectional" (not "none"). If omitted, backend will set appropriate defaults based on link kind.',
313
+ description: 'Link directionality: "none", "unidirectional", or "bidirectional". For inheritance/implements/dependency, use "unidirectional" or "bidirectional".',
335
314
  enum: ['none', 'unidirectional', 'bidirectional'],
336
315
  },
337
316
  },
338
- required: ['diagram_uuid', 'kind', 'from', 'to'],
317
+ required: ['kind', 'from', 'to'],
339
318
  },
340
319
  },
341
320
  {
342
- name: 'link_update',
343
- description: 'Update link fields (MCP authoritative)',
321
+ name: 'update_link',
322
+ description: "Update a link's label, direction, etc. Uses the current diagram. link_id comes from the response when you created the link or from open_diagram.",
344
323
  inputSchema: {
345
324
  type: 'object',
346
325
  properties: {
347
- diagram_uuid: {
348
- type: 'string',
349
- description: 'Diagram UUID',
350
- },
351
326
  link_id: {
352
327
  type: 'string',
353
- description: 'Link ID',
328
+ description: 'Link ID (from create_link or open_diagram response).',
354
329
  },
355
330
  patch: {
356
331
  type: 'object',
@@ -358,25 +333,21 @@ export class PlanformMCPServer {
358
333
  },
359
334
  if_version: {
360
335
  type: 'number',
361
- description: 'Version for optimistic concurrency',
336
+ description: 'Version for optimistic concurrency (optional)',
362
337
  },
363
338
  },
364
- required: ['diagram_uuid', 'link_id', 'patch'],
339
+ required: ['link_id', 'patch'],
365
340
  },
366
341
  },
367
342
  {
368
- name: 'link_verify',
369
- description: 'Mark a link as verified by MCP after confirming relationship in code',
343
+ name: 'verify_link',
344
+ description: 'Mark a link as verified (e.g. after confirming it matches code). Uses the current diagram. link_id from create_link or open_diagram.',
370
345
  inputSchema: {
371
346
  type: 'object',
372
347
  properties: {
373
- diagram_uuid: {
374
- type: 'string',
375
- description: 'Diagram UUID',
376
- },
377
348
  link_id: {
378
349
  type: 'string',
379
- description: 'Link ID',
350
+ description: 'Link ID (from create_link or open_diagram response).',
380
351
  },
381
352
  structural_fields: {
382
353
  type: 'object',
@@ -387,25 +358,21 @@ export class PlanformMCPServer {
387
358
  description: 'Whether differences were detected',
388
359
  },
389
360
  },
390
- required: ['diagram_uuid', 'link_id', 'structural_fields'],
361
+ required: ['link_id', 'structural_fields'],
391
362
  },
392
363
  },
393
364
  {
394
- name: 'link_delete',
395
- description: 'Delete link with status-based logic',
365
+ name: 'delete_link',
366
+ description: 'Delete a link from the current diagram. link_id comes from create_link or open_diagram response.',
396
367
  inputSchema: {
397
368
  type: 'object',
398
369
  properties: {
399
- diagram_uuid: {
400
- type: 'string',
401
- description: 'Diagram UUID',
402
- },
403
370
  link_id: {
404
371
  type: 'string',
405
- description: 'Link ID',
372
+ description: 'Link ID (from create_link or open_diagram response).',
406
373
  },
407
374
  },
408
- required: ['diagram_uuid', 'link_id'],
375
+ required: ['link_id'],
409
376
  },
410
377
  },
411
378
  ];
@@ -422,49 +389,49 @@ export class PlanformMCPServer {
422
389
  case 'health_check':
423
390
  return await this.handleHealthCheck();
424
391
 
425
- case 'get_usage_examples':
392
+ case 'get_usage_guide':
426
393
  return await this.handleGetUsageExamples();
427
394
 
428
- case 'device_start':
395
+ case 'sign_in':
429
396
  return await this.handleDeviceStart();
430
397
 
431
- case 'device_token_poll':
398
+ case 'poll_auth_token':
432
399
  return await this.handleDeviceTokenPoll(request.params.arguments);
433
400
 
434
- case 'backend_health_check':
401
+ case 'check_backend_health':
435
402
  return await this.handleBackendHealthCheck();
436
403
 
437
- case 'diagram_create':
404
+ case 'create_diagram':
438
405
  return await this.handleDiagramCreate(request.params.arguments);
439
406
 
440
- case 'diagram_get':
407
+ case 'open_diagram':
441
408
  return await this.handleDiagramGet(request.params.arguments);
442
409
 
443
- case 'diagrams_list':
410
+ case 'list_diagrams':
444
411
  return await this.handleDiagramsList(request.params.arguments);
445
412
 
446
- case 'nodes_create':
413
+ case 'create_node':
447
414
  return await this.handleNodesCreate(request.params.arguments);
448
415
 
449
- case 'node_update':
416
+ case 'update_node':
450
417
  return await this.handleNodeUpdate(request.params.arguments);
451
418
 
452
- case 'node_verify':
419
+ case 'verify_node':
453
420
  return await this.handleNodeVerify(request.params.arguments);
454
421
 
455
- case 'node_delete':
422
+ case 'delete_node':
456
423
  return await this.handleNodeDelete(request.params.arguments);
457
424
 
458
- case 'links_create':
425
+ case 'create_link':
459
426
  return await this.handleLinksCreate(request.params.arguments);
460
427
 
461
- case 'link_update':
428
+ case 'update_link':
462
429
  return await this.handleLinkUpdate(request.params.arguments);
463
430
 
464
- case 'link_verify':
431
+ case 'verify_link':
465
432
  return await this.handleLinkVerify(request.params.arguments);
466
433
 
467
- case 'link_delete':
434
+ case 'delete_link':
468
435
  return await this.handleLinkDelete(request.params.arguments);
469
436
 
470
437
  default:
@@ -497,6 +464,7 @@ export class PlanformMCPServer {
497
464
  server: 'planform-mcp',
498
465
  version: process.env.MCP_SERVER_VERSION || '1.0.0',
499
466
  timestamp: new Date().toISOString(),
467
+ backend_base_url: process.env.BACKEND_BASE_URL || 'https://www.planform.io/api',
500
468
  }, null, 2),
501
469
  },
502
470
  ],
@@ -591,6 +559,9 @@ export class PlanformMCPServer {
591
559
  if (pollResult.success) {
592
560
  deviceInfo.message = '✅ Authentication successful! Token received and stored.';
593
561
  deviceInfo.access_token_received = true;
562
+ if (response.session.approvedUserUuid) {
563
+ this.sessionUserUuid = response.session.approvedUserUuid;
564
+ }
594
565
  } else {
595
566
  deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
596
567
  deviceInfo.access_token_received = false;
@@ -643,7 +614,7 @@ export class PlanformMCPServer {
643
614
  this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
644
615
  return {
645
616
  success: false,
646
- error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use device_token_poll manually (optional), or try device_start again.`
617
+ error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
647
618
  };
648
619
  } else {
649
620
  this.logger.warn('Device code expired while waiting for approval');
@@ -690,6 +661,63 @@ export class PlanformMCPServer {
690
661
  }
691
662
  }
692
663
 
664
+ /**
665
+ * Returns the current diagram data (nodes, links, etc.). Throws if no diagram is selected.
666
+ * Used to resolve node names to uuids/labels under the hood.
667
+ */
668
+ private async getCurrentDiagramData(): Promise<{ uuid: string; nodes: Array<{ uuid: string; label: string; name: string | null }>; links: Array<{ uuid: string }> }> {
669
+ const diagramUuid = this.currentDiagramUuid;
670
+ if (!diagramUuid) {
671
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
672
+ }
673
+ const diagram = await this.apiClient.getDiagram(diagramUuid);
674
+ return {
675
+ uuid: diagram.uuid,
676
+ nodes: diagram.nodes.map((n) => ({ uuid: n.uuid, label: n.label, name: n.name })),
677
+ links: diagram.links.map((l) => ({ uuid: l.uuid })),
678
+ };
679
+ }
680
+
681
+ /**
682
+ * Resolve node_identifier (name or UUID) to node UUID using the current diagram.
683
+ */
684
+ private async resolveNodeToUuid(diagramUuid: string, nodeIdentifier: string): Promise<string> {
685
+ if (UUID_REGEX.test(nodeIdentifier)) {
686
+ return nodeIdentifier;
687
+ }
688
+ const data = await this.getCurrentDiagramData();
689
+ if (data.uuid !== diagramUuid) {
690
+ throw new Error('Current diagram changed; resolve again.');
691
+ }
692
+ const byName = data.nodes.find(
693
+ (n) => n.name === nodeIdentifier || n.name?.toLowerCase() === String(nodeIdentifier).toLowerCase()
694
+ );
695
+ if (!byName) {
696
+ throw new Error(`No node named "${nodeIdentifier}" in the current diagram. Use node names or UUIDs.`);
697
+ }
698
+ return byName.uuid;
699
+ }
700
+
701
+ /**
702
+ * Resolve node reference (name or label like n-1) to node label for create_link.
703
+ */
704
+ private async resolveNodeToLabel(diagramUuid: string, nodeRef: string): Promise<string> {
705
+ if (NODE_LABEL_REGEX.test(nodeRef)) {
706
+ return nodeRef;
707
+ }
708
+ const data = await this.getCurrentDiagramData();
709
+ if (data.uuid !== diagramUuid) {
710
+ throw new Error('Current diagram changed; resolve again.');
711
+ }
712
+ const byName = data.nodes.find(
713
+ (n) => n.name === nodeRef || n.name?.toLowerCase() === String(nodeRef).toLowerCase()
714
+ );
715
+ if (!byName) {
716
+ throw new Error(`No node named "${nodeRef}" in the current diagram. Use node names (e.g. "User") or labels (e.g. n-1).`);
717
+ }
718
+ return byName.label;
719
+ }
720
+
693
721
  /**
694
722
  * Start automatic background polling for device token (kept for backward compatibility)
695
723
  * @deprecated Use pollUntilApproved for synchronous polling
@@ -810,20 +838,24 @@ export class PlanformMCPServer {
810
838
  private async handleDiagramCreate(args: any): Promise<CallToolResult> {
811
839
  this.logger.info('Diagram create requested');
812
840
 
813
- if (!args.user_uuid || !args.title) {
814
- throw new Error('user_uuid and title are required');
841
+ if (!args.title) {
842
+ throw new Error('title is required');
843
+ }
844
+ if (!args.type) {
845
+ throw new Error('type is required');
815
846
  }
816
847
 
817
848
  try {
818
- if (!args.type) {
819
- throw new Error('type is required');
820
- }
821
849
 
822
- const response = await this.apiClient.createDiagram(args.user_uuid, {
850
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
851
+ if (!userUuid) {
852
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
853
+ }
854
+ const response = await this.apiClient.createDiagram(userUuid, {
823
855
  title: args.title,
824
856
  type: args.type,
825
857
  });
826
-
858
+ this.currentDiagramUuid = response.uuid;
827
859
  // Return the response in the format specified by the design document
828
860
  const result = {
829
861
  diagram_uuid: response.uuid,
@@ -850,14 +882,33 @@ export class PlanformMCPServer {
850
882
 
851
883
  private async handleDiagramGet(args: any): Promise<CallToolResult> {
852
884
  this.logger.info('Diagram get requested');
853
-
854
- if (!args.diagram_uuid) {
855
- throw new Error('diagram_uuid is required');
856
- }
857
885
 
858
886
  try {
859
- const response = await this.apiClient.getDiagram(args.diagram_uuid);
860
-
887
+ let diagramUuid: string;
888
+ const identifier = args.diagram_identifier ?? args.diagram_uuid;
889
+ if (!identifier) {
890
+ diagramUuid = this.currentDiagramUuid ?? '';
891
+ if (!diagramUuid) {
892
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram(diagram_identifier) first.');
893
+ }
894
+ } else if (UUID_REGEX.test(identifier)) {
895
+ diagramUuid = identifier;
896
+ } else {
897
+ const userUuid = this.sessionUserUuid;
898
+ if (!userUuid) {
899
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
900
+ }
901
+ const listRes = await this.apiClient.getUserDiagrams(userUuid, 1, 100);
902
+ const byTitle = listRes.diagrams.find(
903
+ (d) => d.title === identifier || d.title?.toLowerCase() === String(identifier).toLowerCase()
904
+ );
905
+ if (!byTitle) {
906
+ throw new Error(`No diagram found with title "${identifier}". Use list_diagrams to see available diagrams.`);
907
+ }
908
+ diagramUuid = byTitle.uuid;
909
+ }
910
+ const response = await this.apiClient.getDiagram(diagramUuid);
911
+ this.currentDiagramUuid = response.uuid;
861
912
  // Return the response in the format specified by the design document
862
913
  const result = {
863
914
  diagram: {
@@ -873,7 +924,6 @@ export class PlanformMCPServer {
873
924
  links: response.links,
874
925
  },
875
926
  };
876
-
877
927
  return {
878
928
  content: [
879
929
  {
@@ -891,31 +941,31 @@ export class PlanformMCPServer {
891
941
 
892
942
  private async handleDiagramsList(args: any): Promise<CallToolResult> {
893
943
  this.logger.info('Diagrams list requested');
894
-
895
- if (!args.user_uuid) {
896
- throw new Error('user_uuid is required');
944
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
945
+ if (!userUuid) {
946
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
897
947
  }
898
948
 
899
949
  try {
900
950
  const response = await this.apiClient.getUserDiagrams(
901
- args.user_uuid,
951
+ userUuid,
902
952
  args.page || 1,
903
953
  args.page_size || 10
904
954
  );
905
-
906
955
  // Return the response in the format specified by the design document
907
956
  const result = {
908
- items: response.diagrams.map(diagram => ({
957
+ items: response.diagrams.map((diagram) => ({
909
958
  diagram_uuid: diagram.uuid,
910
- external_id: diagram.label, // Using label as external_id for now
959
+ external_id: diagram.label,
911
960
  title: diagram.title,
912
961
  type: diagram.type,
913
- version: 1, // Default version
962
+ version: 1,
914
963
  updated_at: diagram.updatedAt,
915
964
  })),
916
- next_page: response.pagination.page < response.pagination.totalPages
917
- ? (response.pagination.page + 1).toString()
918
- : undefined,
965
+ next_page:
966
+ response.pagination.page < response.pagination.totalPages
967
+ ? (response.pagination.page + 1).toString()
968
+ : undefined,
919
969
  };
920
970
 
921
971
  return {
@@ -935,9 +985,12 @@ export class PlanformMCPServer {
935
985
 
936
986
  private async handleNodesCreate(args: any): Promise<CallToolResult> {
937
987
  this.logger.info('Nodes create requested');
938
-
939
- if (!args.diagram_uuid || !args.kind) {
940
- throw new Error('diagram_uuid and kind are required');
988
+ if (!args.kind) {
989
+ throw new Error('kind is required');
990
+ }
991
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
992
+ if (!diagramUuid) {
993
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
941
994
  }
942
995
 
943
996
  try {
@@ -952,12 +1005,11 @@ export class PlanformMCPServer {
952
1005
  meta: args.meta || null,
953
1006
  };
954
1007
 
955
- // Log the request payload for debugging enum values
956
1008
  if (args.kind === 'enum') {
957
1009
  this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
958
1010
  }
959
1011
 
960
- const response = await this.apiClient.createNode(args.diagram_uuid, requestPayload);
1012
+ const response = await this.apiClient.createNode(diagramUuid, requestPayload);
961
1013
 
962
1014
  return {
963
1015
  content: [
@@ -979,13 +1031,21 @@ export class PlanformMCPServer {
979
1031
 
980
1032
  private async handleNodeUpdate(args: any): Promise<CallToolResult> {
981
1033
  this.logger.info('Node update requested');
982
-
983
- if (!args.diagram_uuid || !args.node_id || !args.patch) {
984
- throw new Error('diagram_uuid, node_id, and patch are required');
1034
+ if (!args.patch) {
1035
+ throw new Error('patch is required');
1036
+ }
1037
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1038
+ if (!nodeIdentifier) {
1039
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1040
+ }
1041
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1042
+ if (!diagramUuid) {
1043
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
985
1044
  }
986
1045
 
987
1046
  try {
988
- const response = await this.apiClient.updateNode(args.diagram_uuid, args.node_id, args.patch);
1047
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1048
+ const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
989
1049
 
990
1050
  return {
991
1051
  content: [
@@ -1007,13 +1067,21 @@ export class PlanformMCPServer {
1007
1067
 
1008
1068
  private async handleNodeVerify(args: any): Promise<CallToolResult> {
1009
1069
  this.logger.info('Node verify requested');
1010
-
1011
- if (!args.diagram_uuid || !args.node_id || !args.structural_fields) {
1012
- throw new Error('diagram_uuid, node_id, and structural_fields are required');
1070
+ if (!args.structural_fields) {
1071
+ throw new Error('structural_fields is required');
1072
+ }
1073
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1074
+ if (!nodeIdentifier) {
1075
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1076
+ }
1077
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1078
+ if (!diagramUuid) {
1079
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1013
1080
  }
1014
1081
 
1015
1082
  try {
1016
- const response = await this.apiClient.verifyNode(args.diagram_uuid, args.node_id, args.structural_fields);
1083
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1084
+ const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
1017
1085
 
1018
1086
  return {
1019
1087
  content: [
@@ -1032,13 +1100,18 @@ export class PlanformMCPServer {
1032
1100
 
1033
1101
  private async handleNodeDelete(args: any): Promise<CallToolResult> {
1034
1102
  this.logger.info('Node delete requested');
1035
-
1036
- if (!args.diagram_uuid || !args.node_id) {
1037
- throw new Error('diagram_uuid and node_id are required');
1103
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1104
+ if (!nodeIdentifier) {
1105
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1106
+ }
1107
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1108
+ if (!diagramUuid) {
1109
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1038
1110
  }
1039
1111
 
1040
1112
  try {
1041
- const response = await this.apiClient.deleteNode(args.diagram_uuid, args.node_id);
1113
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1114
+ const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
1042
1115
 
1043
1116
  return {
1044
1117
  content: [
@@ -1057,22 +1130,26 @@ export class PlanformMCPServer {
1057
1130
 
1058
1131
  private async handleLinksCreate(args: any): Promise<CallToolResult> {
1059
1132
  this.logger.info('Links create requested');
1060
-
1061
- if (!args.diagram_uuid || !args.kind || !args.from || !args.to) {
1062
- throw new Error('diagram_uuid, kind, from, and to are required');
1133
+ if (!args.kind || !args.from || !args.to) {
1134
+ throw new Error('kind, from, and to are required (from/to can be node names, e.g. "User" and "Account").');
1135
+ }
1136
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1137
+ if (!diagramUuid) {
1138
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1063
1139
  }
1064
1140
 
1065
- // Validate that "none" is not used for inheritance/implements/dependency links
1066
1141
  const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
1067
1142
  if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
1068
1143
  throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
1069
1144
  }
1070
1145
 
1071
1146
  try {
1147
+ const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
1148
+ const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
1072
1149
  const requestBody: any = {
1073
1150
  kind: args.kind,
1074
- fromNode: args.from,
1075
- toNode: args.to,
1151
+ fromNode: fromLabel,
1152
+ toNode: toLabel,
1076
1153
  name: args.label || null,
1077
1154
  meta: args.meta || null,
1078
1155
  fromMultiplicity: args.fromMultiplicity || null,
@@ -1083,8 +1160,7 @@ export class PlanformMCPServer {
1083
1160
  if (args.directional !== undefined) {
1084
1161
  requestBody.directional = args.directional;
1085
1162
  }
1086
-
1087
- const response = await this.apiClient.createLink(args.diagram_uuid, requestBody);
1163
+ const response = await this.apiClient.createLink(diagramUuid, requestBody);
1088
1164
 
1089
1165
  return {
1090
1166
  content: [
@@ -1106,13 +1182,16 @@ export class PlanformMCPServer {
1106
1182
 
1107
1183
  private async handleLinkUpdate(args: any): Promise<CallToolResult> {
1108
1184
  this.logger.info('Link update requested');
1109
-
1110
- if (!args.diagram_uuid || !args.link_id || !args.patch) {
1111
- throw new Error('diagram_uuid, link_id, and patch are required');
1185
+ if (!args.link_id || !args.patch) {
1186
+ throw new Error('link_id and patch are required (link_id comes from create_link or open_diagram response).');
1187
+ }
1188
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1189
+ if (!diagramUuid) {
1190
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1112
1191
  }
1113
1192
 
1114
1193
  try {
1115
- const response = await this.apiClient.updateLink(args.diagram_uuid, args.link_id, args.patch);
1194
+ const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
1116
1195
 
1117
1196
  return {
1118
1197
  content: [
@@ -1134,13 +1213,16 @@ export class PlanformMCPServer {
1134
1213
 
1135
1214
  private async handleLinkVerify(args: any): Promise<CallToolResult> {
1136
1215
  this.logger.info('Link verify requested');
1137
-
1138
- if (!args.diagram_uuid || !args.link_id || !args.structural_fields) {
1139
- throw new Error('diagram_uuid, link_id, and structural_fields are required');
1216
+ if (!args.link_id || !args.structural_fields) {
1217
+ throw new Error('link_id and structural_fields are required.');
1218
+ }
1219
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1220
+ if (!diagramUuid) {
1221
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1140
1222
  }
1141
1223
 
1142
1224
  try {
1143
- const response = await this.apiClient.verifyLink(args.diagram_uuid, args.link_id, args.structural_fields);
1225
+ const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
1144
1226
 
1145
1227
  return {
1146
1228
  content: [
@@ -1159,13 +1241,16 @@ export class PlanformMCPServer {
1159
1241
 
1160
1242
  private async handleLinkDelete(args: any): Promise<CallToolResult> {
1161
1243
  this.logger.info('Link delete requested');
1162
-
1163
- if (!args.diagram_uuid || !args.link_id) {
1164
- throw new Error('diagram_uuid and link_id are required');
1244
+ if (!args.link_id) {
1245
+ throw new Error('link_id is required (from create_link or open_diagram response).');
1246
+ }
1247
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1248
+ if (!diagramUuid) {
1249
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1165
1250
  }
1166
1251
 
1167
1252
  try {
1168
- const response = await this.apiClient.deleteLink(args.diagram_uuid, args.link_id);
1253
+ const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
1169
1254
 
1170
1255
  return {
1171
1256
  content: [