@shirbarzur/planform-mcp-server 1.0.5 → 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:
@@ -592,6 +559,9 @@ export class PlanformMCPServer {
592
559
  if (pollResult.success) {
593
560
  deviceInfo.message = '✅ Authentication successful! Token received and stored.';
594
561
  deviceInfo.access_token_received = true;
562
+ if (response.session.approvedUserUuid) {
563
+ this.sessionUserUuid = response.session.approvedUserUuid;
564
+ }
595
565
  } else {
596
566
  deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
597
567
  deviceInfo.access_token_received = false;
@@ -644,7 +614,7 @@ export class PlanformMCPServer {
644
614
  this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
645
615
  return {
646
616
  success: false,
647
- 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.`
648
618
  };
649
619
  } else {
650
620
  this.logger.warn('Device code expired while waiting for approval');
@@ -691,6 +661,63 @@ export class PlanformMCPServer {
691
661
  }
692
662
  }
693
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
+
694
721
  /**
695
722
  * Start automatic background polling for device token (kept for backward compatibility)
696
723
  * @deprecated Use pollUntilApproved for synchronous polling
@@ -811,20 +838,24 @@ export class PlanformMCPServer {
811
838
  private async handleDiagramCreate(args: any): Promise<CallToolResult> {
812
839
  this.logger.info('Diagram create requested');
813
840
 
814
- if (!args.user_uuid || !args.title) {
815
- 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');
816
846
  }
817
847
 
818
848
  try {
819
- if (!args.type) {
820
- throw new Error('type is required');
821
- }
822
849
 
823
- 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, {
824
855
  title: args.title,
825
856
  type: args.type,
826
857
  });
827
-
858
+ this.currentDiagramUuid = response.uuid;
828
859
  // Return the response in the format specified by the design document
829
860
  const result = {
830
861
  diagram_uuid: response.uuid,
@@ -851,14 +882,33 @@ export class PlanformMCPServer {
851
882
 
852
883
  private async handleDiagramGet(args: any): Promise<CallToolResult> {
853
884
  this.logger.info('Diagram get requested');
854
-
855
- if (!args.diagram_uuid) {
856
- throw new Error('diagram_uuid is required');
857
- }
858
885
 
859
886
  try {
860
- const response = await this.apiClient.getDiagram(args.diagram_uuid);
861
-
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;
862
912
  // Return the response in the format specified by the design document
863
913
  const result = {
864
914
  diagram: {
@@ -874,7 +924,6 @@ export class PlanformMCPServer {
874
924
  links: response.links,
875
925
  },
876
926
  };
877
-
878
927
  return {
879
928
  content: [
880
929
  {
@@ -892,31 +941,31 @@ export class PlanformMCPServer {
892
941
 
893
942
  private async handleDiagramsList(args: any): Promise<CallToolResult> {
894
943
  this.logger.info('Diagrams list requested');
895
-
896
- if (!args.user_uuid) {
897
- 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.');
898
947
  }
899
948
 
900
949
  try {
901
950
  const response = await this.apiClient.getUserDiagrams(
902
- args.user_uuid,
951
+ userUuid,
903
952
  args.page || 1,
904
953
  args.page_size || 10
905
954
  );
906
-
907
955
  // Return the response in the format specified by the design document
908
956
  const result = {
909
- items: response.diagrams.map(diagram => ({
957
+ items: response.diagrams.map((diagram) => ({
910
958
  diagram_uuid: diagram.uuid,
911
- external_id: diagram.label, // Using label as external_id for now
959
+ external_id: diagram.label,
912
960
  title: diagram.title,
913
961
  type: diagram.type,
914
- version: 1, // Default version
962
+ version: 1,
915
963
  updated_at: diagram.updatedAt,
916
964
  })),
917
- next_page: response.pagination.page < response.pagination.totalPages
918
- ? (response.pagination.page + 1).toString()
919
- : undefined,
965
+ next_page:
966
+ response.pagination.page < response.pagination.totalPages
967
+ ? (response.pagination.page + 1).toString()
968
+ : undefined,
920
969
  };
921
970
 
922
971
  return {
@@ -936,9 +985,12 @@ export class PlanformMCPServer {
936
985
 
937
986
  private async handleNodesCreate(args: any): Promise<CallToolResult> {
938
987
  this.logger.info('Nodes create requested');
939
-
940
- if (!args.diagram_uuid || !args.kind) {
941
- 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.');
942
994
  }
943
995
 
944
996
  try {
@@ -953,12 +1005,11 @@ export class PlanformMCPServer {
953
1005
  meta: args.meta || null,
954
1006
  };
955
1007
 
956
- // Log the request payload for debugging enum values
957
1008
  if (args.kind === 'enum') {
958
1009
  this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
959
1010
  }
960
1011
 
961
- const response = await this.apiClient.createNode(args.diagram_uuid, requestPayload);
1012
+ const response = await this.apiClient.createNode(diagramUuid, requestPayload);
962
1013
 
963
1014
  return {
964
1015
  content: [
@@ -980,13 +1031,21 @@ export class PlanformMCPServer {
980
1031
 
981
1032
  private async handleNodeUpdate(args: any): Promise<CallToolResult> {
982
1033
  this.logger.info('Node update requested');
983
-
984
- if (!args.diagram_uuid || !args.node_id || !args.patch) {
985
- 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.');
986
1044
  }
987
1045
 
988
1046
  try {
989
- 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);
990
1049
 
991
1050
  return {
992
1051
  content: [
@@ -1008,13 +1067,21 @@ export class PlanformMCPServer {
1008
1067
 
1009
1068
  private async handleNodeVerify(args: any): Promise<CallToolResult> {
1010
1069
  this.logger.info('Node verify requested');
1011
-
1012
- if (!args.diagram_uuid || !args.node_id || !args.structural_fields) {
1013
- 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.');
1014
1080
  }
1015
1081
 
1016
1082
  try {
1017
- 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);
1018
1085
 
1019
1086
  return {
1020
1087
  content: [
@@ -1033,13 +1100,18 @@ export class PlanformMCPServer {
1033
1100
 
1034
1101
  private async handleNodeDelete(args: any): Promise<CallToolResult> {
1035
1102
  this.logger.info('Node delete requested');
1036
-
1037
- if (!args.diagram_uuid || !args.node_id) {
1038
- 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.');
1039
1110
  }
1040
1111
 
1041
1112
  try {
1042
- 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);
1043
1115
 
1044
1116
  return {
1045
1117
  content: [
@@ -1058,22 +1130,26 @@ export class PlanformMCPServer {
1058
1130
 
1059
1131
  private async handleLinksCreate(args: any): Promise<CallToolResult> {
1060
1132
  this.logger.info('Links create requested');
1061
-
1062
- if (!args.diagram_uuid || !args.kind || !args.from || !args.to) {
1063
- 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.');
1064
1139
  }
1065
1140
 
1066
- // Validate that "none" is not used for inheritance/implements/dependency links
1067
1141
  const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
1068
1142
  if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
1069
1143
  throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
1070
1144
  }
1071
1145
 
1072
1146
  try {
1147
+ const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
1148
+ const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
1073
1149
  const requestBody: any = {
1074
1150
  kind: args.kind,
1075
- fromNode: args.from,
1076
- toNode: args.to,
1151
+ fromNode: fromLabel,
1152
+ toNode: toLabel,
1077
1153
  name: args.label || null,
1078
1154
  meta: args.meta || null,
1079
1155
  fromMultiplicity: args.fromMultiplicity || null,
@@ -1084,8 +1160,7 @@ export class PlanformMCPServer {
1084
1160
  if (args.directional !== undefined) {
1085
1161
  requestBody.directional = args.directional;
1086
1162
  }
1087
-
1088
- const response = await this.apiClient.createLink(args.diagram_uuid, requestBody);
1163
+ const response = await this.apiClient.createLink(diagramUuid, requestBody);
1089
1164
 
1090
1165
  return {
1091
1166
  content: [
@@ -1107,13 +1182,16 @@ export class PlanformMCPServer {
1107
1182
 
1108
1183
  private async handleLinkUpdate(args: any): Promise<CallToolResult> {
1109
1184
  this.logger.info('Link update requested');
1110
-
1111
- if (!args.diagram_uuid || !args.link_id || !args.patch) {
1112
- 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.');
1113
1191
  }
1114
1192
 
1115
1193
  try {
1116
- 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);
1117
1195
 
1118
1196
  return {
1119
1197
  content: [
@@ -1135,13 +1213,16 @@ export class PlanformMCPServer {
1135
1213
 
1136
1214
  private async handleLinkVerify(args: any): Promise<CallToolResult> {
1137
1215
  this.logger.info('Link verify requested');
1138
-
1139
- if (!args.diagram_uuid || !args.link_id || !args.structural_fields) {
1140
- 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.');
1141
1222
  }
1142
1223
 
1143
1224
  try {
1144
- 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);
1145
1226
 
1146
1227
  return {
1147
1228
  content: [
@@ -1160,13 +1241,16 @@ export class PlanformMCPServer {
1160
1241
 
1161
1242
  private async handleLinkDelete(args: any): Promise<CallToolResult> {
1162
1243
  this.logger.info('Link delete requested');
1163
-
1164
- if (!args.diagram_uuid || !args.link_id) {
1165
- 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.');
1166
1250
  }
1167
1251
 
1168
1252
  try {
1169
- const response = await this.apiClient.deleteLink(args.diagram_uuid, args.link_id);
1253
+ const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
1170
1254
 
1171
1255
  return {
1172
1256
  content: [