@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/dist/server.js CHANGED
@@ -4,11 +4,19 @@ import { ApiClient } from './api-client.js';
4
4
  import { Logger } from './logger.js';
5
5
  import { USAGE_EXAMPLES } from './usage-examples.js';
6
6
  import open from 'open';
7
+ /** UUID v4 pattern for distinguishing UUIDs from names/titles */
8
+ 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;
9
+ /** Node label pattern (e.g. n-1, n-2) */
10
+ const NODE_LABEL_REGEX = /^n-\d+$/;
7
11
  export class PlanformMCPServer {
8
12
  server;
9
13
  apiClient;
10
14
  logger;
11
15
  activePolling = new Map();
16
+ /** Set after successful sign_in; used for list_diagrams and create_diagram so callers need not pass user_uuid */
17
+ sessionUserUuid = null;
18
+ /** Set after create_diagram or open_diagram; used for node/link tools so callers need not pass diagram_uuid */
19
+ currentDiagramUuid = null;
12
20
  constructor() {
13
21
  this.server = new Server({
14
22
  name: process.env.MCP_SERVER_NAME || 'planform-mcp',
@@ -37,37 +45,37 @@ export class PlanformMCPServer {
37
45
  },
38
46
  },
39
47
  {
40
- name: 'get_usage_examples',
41
- 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.',
48
+ name: 'get_usage_guide',
49
+ description: 'Get step-by-step instructions and the recommended flow for the Planform MCP. Call when unsure how to use the server.',
42
50
  inputSchema: {
43
51
  type: 'object',
44
52
  properties: {},
45
53
  },
46
54
  },
47
55
  {
48
- name: 'device_start',
49
- 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.',
56
+ name: 'sign_in',
57
+ 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.',
50
58
  inputSchema: {
51
59
  type: 'object',
52
60
  properties: {},
53
61
  },
54
62
  },
55
63
  {
56
- name: 'device_token_poll',
57
- description: '[Optional/Advanced] Manually poll for MCP JWT after user approves. Usually not needed since device_start polls automatically.',
64
+ name: 'poll_auth_token',
65
+ description: '[Optional/Advanced] Manually poll for auth token after user approves. Usually not needed since sign_in polls automatically.',
58
66
  inputSchema: {
59
67
  type: 'object',
60
68
  properties: {
61
69
  device_code: {
62
70
  type: 'string',
63
- description: 'Device code from device_start response',
71
+ description: 'Device code from sign_in response',
64
72
  },
65
73
  },
66
74
  required: ['device_code'],
67
75
  },
68
76
  },
69
77
  {
70
- name: 'backend_health_check',
78
+ name: 'check_backend_health',
71
79
  description: 'Check the health status of the Planform backend API',
72
80
  inputSchema: {
73
81
  type: 'object',
@@ -75,37 +83,11 @@ export class PlanformMCPServer {
75
83
  },
76
84
  },
77
85
  {
78
- name: 'diagram_create',
79
- description: 'Create a new diagram. Requires prior authentication via device_start; use approved_user_uuid from device_start response as user_uuid.',
86
+ name: 'list_diagrams',
87
+ description: 'List your diagrams. Uses your session from sign_in—no IDs to pass. Optional: filter by status, page, page_size.',
80
88
  inputSchema: {
81
89
  type: 'object',
82
90
  properties: {
83
- user_uuid: {
84
- type: 'string',
85
- description: 'User UUID who owns the diagram (use approved_user_uuid from device_start response)',
86
- },
87
- title: {
88
- type: 'string',
89
- description: 'Diagram title',
90
- },
91
- type: {
92
- type: 'string',
93
- description: 'Diagram type (e.g., "uml.class")',
94
- },
95
- },
96
- required: ['user_uuid', 'title', 'type'],
97
- },
98
- },
99
- {
100
- name: 'diagrams_list',
101
- 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.',
102
- inputSchema: {
103
- type: 'object',
104
- properties: {
105
- user_uuid: {
106
- type: 'string',
107
- description: 'User UUID (use approved_user_uuid from device_start response after authenticating)',
108
- },
109
91
  status: {
110
92
  type: 'string',
111
93
  description: 'Filter by status (active|archived)',
@@ -122,41 +104,53 @@ export class PlanformMCPServer {
122
104
  default: 10,
123
105
  },
124
106
  },
125
- required: ['user_uuid'],
126
107
  },
127
108
  },
128
109
  {
129
- name: 'diagram_get',
130
- description: 'Fetch full diagram (nodes + links) to mirror state locally. Requires prior authentication via device_start.',
110
+ name: 'create_diagram',
111
+ 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.',
131
112
  inputSchema: {
132
113
  type: 'object',
133
114
  properties: {
134
- diagram_uuid: {
115
+ title: {
116
+ type: 'string',
117
+ description: 'Diagram title (e.g. "My class diagram")',
118
+ },
119
+ type: {
135
120
  type: 'string',
136
- description: 'Diagram UUID',
121
+ description: 'Diagram type (e.g. "uml.class")',
137
122
  },
138
123
  },
139
- required: ['diagram_uuid'],
124
+ required: ['title', 'type'],
140
125
  },
141
126
  },
142
127
  {
143
- name: 'nodes_create',
144
- description: 'Create UML node with immutable grid placement',
128
+ name: 'open_diagram',
129
+ 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.',
145
130
  inputSchema: {
146
131
  type: 'object',
147
132
  properties: {
148
- diagram_uuid: {
133
+ diagram_identifier: {
149
134
  type: 'string',
150
- description: 'Diagram UUID',
135
+ description: 'Diagram title (e.g. "My class diagram") or diagram ID. Omit to refresh the current diagram.',
151
136
  },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: 'create_node',
142
+ 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.',
143
+ inputSchema: {
144
+ type: 'object',
145
+ properties: {
152
146
  kind: {
153
147
  type: 'string',
154
- description: 'Node kind',
148
+ description: 'Node kind: class, interface, or enum',
155
149
  enum: ['class', 'interface', 'enum'],
156
150
  },
157
151
  name: {
158
152
  type: 'string',
159
- description: 'Node name',
153
+ description: 'Node name (e.g. "User")',
160
154
  },
161
155
  fields: {
162
156
  type: 'array',
@@ -213,48 +207,40 @@ export class PlanformMCPServer {
213
207
  items: { type: 'string' },
214
208
  },
215
209
  },
216
- required: ['diagram_uuid', 'kind'],
210
+ required: ['kind'],
217
211
  },
218
212
  },
219
213
  {
220
- name: 'node_update',
221
- description: 'Update structural fields (MCP authoritative)',
214
+ name: 'update_node',
215
+ 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.",
222
216
  inputSchema: {
223
217
  type: 'object',
224
218
  properties: {
225
- diagram_uuid: {
226
- type: 'string',
227
- description: 'Diagram UUID',
228
- },
229
- node_id: {
219
+ node_identifier: {
230
220
  type: 'string',
231
- description: 'Node ID',
221
+ description: 'Node name (e.g. "User") or node ID. The server resolves the name to the node in the current diagram.',
232
222
  },
233
223
  patch: {
234
224
  type: 'object',
235
- description: 'Node fields to update',
225
+ description: 'Fields to update (name, fields, methods, etc.)',
236
226
  },
237
227
  if_version: {
238
228
  type: 'number',
239
- description: 'Version for optimistic concurrency',
229
+ description: 'Version for optimistic concurrency (optional)',
240
230
  },
241
231
  },
242
- required: ['diagram_uuid', 'node_id', 'patch'],
232
+ required: ['node_identifier', 'patch'],
243
233
  },
244
234
  },
245
235
  {
246
- name: 'node_verify',
247
- description: 'Mark a node as verified by MCP after confirming structure in code',
236
+ name: 'verify_node',
237
+ 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.',
248
238
  inputSchema: {
249
239
  type: 'object',
250
240
  properties: {
251
- diagram_uuid: {
241
+ node_identifier: {
252
242
  type: 'string',
253
- description: 'Diagram UUID',
254
- },
255
- node_id: {
256
- type: 'string',
257
- description: 'Node ID',
243
+ description: 'Node name (e.g. "User") or node ID.',
258
244
  },
259
245
  structural_fields: {
260
246
  type: 'object',
@@ -265,37 +251,29 @@ export class PlanformMCPServer {
265
251
  description: 'Whether differences were detected',
266
252
  },
267
253
  },
268
- required: ['diagram_uuid', 'node_id', 'structural_fields'],
254
+ required: ['node_identifier', 'structural_fields'],
269
255
  },
270
256
  },
271
257
  {
272
- name: 'node_delete',
273
- description: 'Delete node with status-based logic',
258
+ name: 'delete_node',
259
+ description: 'Delete a node from the current diagram. Refer to the node by its name (e.g. "User").',
274
260
  inputSchema: {
275
261
  type: 'object',
276
262
  properties: {
277
- diagram_uuid: {
263
+ node_identifier: {
278
264
  type: 'string',
279
- description: 'Diagram UUID',
280
- },
281
- node_id: {
282
- type: 'string',
283
- description: 'Node ID',
265
+ description: 'Node name (e.g. "User") or node ID.',
284
266
  },
285
267
  },
286
- required: ['diagram_uuid', 'node_id'],
268
+ required: ['node_identifier'],
287
269
  },
288
270
  },
289
271
  {
290
- name: 'links_create',
291
- 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.',
272
+ name: 'create_link',
273
+ 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.',
292
274
  inputSchema: {
293
275
  type: 'object',
294
276
  properties: {
295
- diagram_uuid: {
296
- type: 'string',
297
- description: 'Diagram UUID',
298
- },
299
277
  kind: {
300
278
  type: 'string',
301
279
  description: 'Link kind',
@@ -303,11 +281,11 @@ export class PlanformMCPServer {
303
281
  },
304
282
  from: {
305
283
  type: 'string',
306
- description: 'Source node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
284
+ description: 'Source node name (e.g. "User") or label (e.g. n-1).',
307
285
  },
308
286
  to: {
309
287
  type: 'string',
310
- description: 'Target node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
288
+ description: 'Target node name (e.g. "Account") or label (e.g. n-2).',
311
289
  },
312
290
  label: {
313
291
  type: 'string',
@@ -315,26 +293,22 @@ export class PlanformMCPServer {
315
293
  },
316
294
  directional: {
317
295
  type: 'string',
318
- 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.',
296
+ description: 'Link directionality: "none", "unidirectional", or "bidirectional". For inheritance/implements/dependency, use "unidirectional" or "bidirectional".',
319
297
  enum: ['none', 'unidirectional', 'bidirectional'],
320
298
  },
321
299
  },
322
- required: ['diagram_uuid', 'kind', 'from', 'to'],
300
+ required: ['kind', 'from', 'to'],
323
301
  },
324
302
  },
325
303
  {
326
- name: 'link_update',
327
- description: 'Update link fields (MCP authoritative)',
304
+ name: 'update_link',
305
+ 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.",
328
306
  inputSchema: {
329
307
  type: 'object',
330
308
  properties: {
331
- diagram_uuid: {
332
- type: 'string',
333
- description: 'Diagram UUID',
334
- },
335
309
  link_id: {
336
310
  type: 'string',
337
- description: 'Link ID',
311
+ description: 'Link ID (from create_link or open_diagram response).',
338
312
  },
339
313
  patch: {
340
314
  type: 'object',
@@ -342,25 +316,21 @@ export class PlanformMCPServer {
342
316
  },
343
317
  if_version: {
344
318
  type: 'number',
345
- description: 'Version for optimistic concurrency',
319
+ description: 'Version for optimistic concurrency (optional)',
346
320
  },
347
321
  },
348
- required: ['diagram_uuid', 'link_id', 'patch'],
322
+ required: ['link_id', 'patch'],
349
323
  },
350
324
  },
351
325
  {
352
- name: 'link_verify',
353
- description: 'Mark a link as verified by MCP after confirming relationship in code',
326
+ name: 'verify_link',
327
+ 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.',
354
328
  inputSchema: {
355
329
  type: 'object',
356
330
  properties: {
357
- diagram_uuid: {
358
- type: 'string',
359
- description: 'Diagram UUID',
360
- },
361
331
  link_id: {
362
332
  type: 'string',
363
- description: 'Link ID',
333
+ description: 'Link ID (from create_link or open_diagram response).',
364
334
  },
365
335
  structural_fields: {
366
336
  type: 'object',
@@ -371,25 +341,21 @@ export class PlanformMCPServer {
371
341
  description: 'Whether differences were detected',
372
342
  },
373
343
  },
374
- required: ['diagram_uuid', 'link_id', 'structural_fields'],
344
+ required: ['link_id', 'structural_fields'],
375
345
  },
376
346
  },
377
347
  {
378
- name: 'link_delete',
379
- description: 'Delete link with status-based logic',
348
+ name: 'delete_link',
349
+ description: 'Delete a link from the current diagram. link_id comes from create_link or open_diagram response.',
380
350
  inputSchema: {
381
351
  type: 'object',
382
352
  properties: {
383
- diagram_uuid: {
384
- type: 'string',
385
- description: 'Diagram UUID',
386
- },
387
353
  link_id: {
388
354
  type: 'string',
389
- description: 'Link ID',
355
+ description: 'Link ID (from create_link or open_diagram response).',
390
356
  },
391
357
  },
392
- required: ['diagram_uuid', 'link_id'],
358
+ required: ['link_id'],
393
359
  },
394
360
  },
395
361
  ];
@@ -402,35 +368,35 @@ export class PlanformMCPServer {
402
368
  switch (request.params.name) {
403
369
  case 'health_check':
404
370
  return await this.handleHealthCheck();
405
- case 'get_usage_examples':
371
+ case 'get_usage_guide':
406
372
  return await this.handleGetUsageExamples();
407
- case 'device_start':
373
+ case 'sign_in':
408
374
  return await this.handleDeviceStart();
409
- case 'device_token_poll':
375
+ case 'poll_auth_token':
410
376
  return await this.handleDeviceTokenPoll(request.params.arguments);
411
- case 'backend_health_check':
377
+ case 'check_backend_health':
412
378
  return await this.handleBackendHealthCheck();
413
- case 'diagram_create':
379
+ case 'create_diagram':
414
380
  return await this.handleDiagramCreate(request.params.arguments);
415
- case 'diagram_get':
381
+ case 'open_diagram':
416
382
  return await this.handleDiagramGet(request.params.arguments);
417
- case 'diagrams_list':
383
+ case 'list_diagrams':
418
384
  return await this.handleDiagramsList(request.params.arguments);
419
- case 'nodes_create':
385
+ case 'create_node':
420
386
  return await this.handleNodesCreate(request.params.arguments);
421
- case 'node_update':
387
+ case 'update_node':
422
388
  return await this.handleNodeUpdate(request.params.arguments);
423
- case 'node_verify':
389
+ case 'verify_node':
424
390
  return await this.handleNodeVerify(request.params.arguments);
425
- case 'node_delete':
391
+ case 'delete_node':
426
392
  return await this.handleNodeDelete(request.params.arguments);
427
- case 'links_create':
393
+ case 'create_link':
428
394
  return await this.handleLinksCreate(request.params.arguments);
429
- case 'link_update':
395
+ case 'update_link':
430
396
  return await this.handleLinkUpdate(request.params.arguments);
431
- case 'link_verify':
397
+ case 'verify_link':
432
398
  return await this.handleLinkVerify(request.params.arguments);
433
- case 'link_delete':
399
+ case 'delete_link':
434
400
  return await this.handleLinkDelete(request.params.arguments);
435
401
  default:
436
402
  throw new Error(`Unknown tool: ${request.params.name}`);
@@ -461,6 +427,7 @@ export class PlanformMCPServer {
461
427
  server: 'planform-mcp',
462
428
  version: process.env.MCP_SERVER_VERSION || '1.0.0',
463
429
  timestamp: new Date().toISOString(),
430
+ backend_base_url: process.env.BACKEND_BASE_URL || 'https://www.planform.io/api',
464
431
  }, null, 2),
465
432
  },
466
433
  ],
@@ -541,6 +508,9 @@ export class PlanformMCPServer {
541
508
  if (pollResult.success) {
542
509
  deviceInfo.message = '✅ Authentication successful! Token received and stored.';
543
510
  deviceInfo.access_token_received = true;
511
+ if (response.session.approvedUserUuid) {
512
+ this.sessionUserUuid = response.session.approvedUserUuid;
513
+ }
544
514
  }
545
515
  else {
546
516
  deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
@@ -584,7 +554,7 @@ export class PlanformMCPServer {
584
554
  this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
585
555
  return {
586
556
  success: false,
587
- error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use device_token_poll manually (optional), or try device_start again.`
557
+ error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
588
558
  };
589
559
  }
590
560
  else {
@@ -635,6 +605,56 @@ export class PlanformMCPServer {
635
605
  }
636
606
  }
637
607
  }
608
+ /**
609
+ * Returns the current diagram data (nodes, links, etc.). Throws if no diagram is selected.
610
+ * Used to resolve node names to uuids/labels under the hood.
611
+ */
612
+ async getCurrentDiagramData() {
613
+ const diagramUuid = this.currentDiagramUuid;
614
+ if (!diagramUuid) {
615
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
616
+ }
617
+ const diagram = await this.apiClient.getDiagram(diagramUuid);
618
+ return {
619
+ uuid: diagram.uuid,
620
+ nodes: diagram.nodes.map((n) => ({ uuid: n.uuid, label: n.label, name: n.name })),
621
+ links: diagram.links.map((l) => ({ uuid: l.uuid })),
622
+ };
623
+ }
624
+ /**
625
+ * Resolve node_identifier (name or UUID) to node UUID using the current diagram.
626
+ */
627
+ async resolveNodeToUuid(diagramUuid, nodeIdentifier) {
628
+ if (UUID_REGEX.test(nodeIdentifier)) {
629
+ return nodeIdentifier;
630
+ }
631
+ const data = await this.getCurrentDiagramData();
632
+ if (data.uuid !== diagramUuid) {
633
+ throw new Error('Current diagram changed; resolve again.');
634
+ }
635
+ const byName = data.nodes.find((n) => n.name === nodeIdentifier || n.name?.toLowerCase() === String(nodeIdentifier).toLowerCase());
636
+ if (!byName) {
637
+ throw new Error(`No node named "${nodeIdentifier}" in the current diagram. Use node names or UUIDs.`);
638
+ }
639
+ return byName.uuid;
640
+ }
641
+ /**
642
+ * Resolve node reference (name or label like n-1) to node label for create_link.
643
+ */
644
+ async resolveNodeToLabel(diagramUuid, nodeRef) {
645
+ if (NODE_LABEL_REGEX.test(nodeRef)) {
646
+ return nodeRef;
647
+ }
648
+ const data = await this.getCurrentDiagramData();
649
+ if (data.uuid !== diagramUuid) {
650
+ throw new Error('Current diagram changed; resolve again.');
651
+ }
652
+ const byName = data.nodes.find((n) => n.name === nodeRef || n.name?.toLowerCase() === String(nodeRef).toLowerCase());
653
+ if (!byName) {
654
+ throw new Error(`No node named "${nodeRef}" in the current diagram. Use node names (e.g. "User") or labels (e.g. n-1).`);
655
+ }
656
+ return byName.label;
657
+ }
638
658
  /**
639
659
  * Start automatic background polling for device token (kept for backward compatibility)
640
660
  * @deprecated Use pollUntilApproved for synchronous polling
@@ -747,17 +767,22 @@ export class PlanformMCPServer {
747
767
  }
748
768
  async handleDiagramCreate(args) {
749
769
  this.logger.info('Diagram create requested');
750
- if (!args.user_uuid || !args.title) {
751
- throw new Error('user_uuid and title are required');
770
+ if (!args.title) {
771
+ throw new Error('title is required');
772
+ }
773
+ if (!args.type) {
774
+ throw new Error('type is required');
752
775
  }
753
776
  try {
754
- if (!args.type) {
755
- throw new Error('type is required');
777
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
778
+ if (!userUuid) {
779
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
756
780
  }
757
- const response = await this.apiClient.createDiagram(args.user_uuid, {
781
+ const response = await this.apiClient.createDiagram(userUuid, {
758
782
  title: args.title,
759
783
  type: args.type,
760
784
  });
785
+ this.currentDiagramUuid = response.uuid;
761
786
  // Return the response in the format specified by the design document
762
787
  const result = {
763
788
  diagram_uuid: response.uuid,
@@ -783,11 +808,32 @@ export class PlanformMCPServer {
783
808
  }
784
809
  async handleDiagramGet(args) {
785
810
  this.logger.info('Diagram get requested');
786
- if (!args.diagram_uuid) {
787
- throw new Error('diagram_uuid is required');
788
- }
789
811
  try {
790
- const response = await this.apiClient.getDiagram(args.diagram_uuid);
812
+ let diagramUuid;
813
+ const identifier = args.diagram_identifier ?? args.diagram_uuid;
814
+ if (!identifier) {
815
+ diagramUuid = this.currentDiagramUuid ?? '';
816
+ if (!diagramUuid) {
817
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram(diagram_identifier) first.');
818
+ }
819
+ }
820
+ else if (UUID_REGEX.test(identifier)) {
821
+ diagramUuid = identifier;
822
+ }
823
+ else {
824
+ const userUuid = this.sessionUserUuid;
825
+ if (!userUuid) {
826
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
827
+ }
828
+ const listRes = await this.apiClient.getUserDiagrams(userUuid, 1, 100);
829
+ const byTitle = listRes.diagrams.find((d) => d.title === identifier || d.title?.toLowerCase() === String(identifier).toLowerCase());
830
+ if (!byTitle) {
831
+ throw new Error(`No diagram found with title "${identifier}". Use list_diagrams to see available diagrams.`);
832
+ }
833
+ diagramUuid = byTitle.uuid;
834
+ }
835
+ const response = await this.apiClient.getDiagram(diagramUuid);
836
+ this.currentDiagramUuid = response.uuid;
791
837
  // Return the response in the format specified by the design document
792
838
  const result = {
793
839
  diagram: {
@@ -820,19 +866,20 @@ export class PlanformMCPServer {
820
866
  }
821
867
  async handleDiagramsList(args) {
822
868
  this.logger.info('Diagrams list requested');
823
- if (!args.user_uuid) {
824
- throw new Error('user_uuid is required');
869
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
870
+ if (!userUuid) {
871
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
825
872
  }
826
873
  try {
827
- const response = await this.apiClient.getUserDiagrams(args.user_uuid, args.page || 1, args.page_size || 10);
874
+ const response = await this.apiClient.getUserDiagrams(userUuid, args.page || 1, args.page_size || 10);
828
875
  // Return the response in the format specified by the design document
829
876
  const result = {
830
- items: response.diagrams.map(diagram => ({
877
+ items: response.diagrams.map((diagram) => ({
831
878
  diagram_uuid: diagram.uuid,
832
- external_id: diagram.label, // Using label as external_id for now
879
+ external_id: diagram.label,
833
880
  title: diagram.title,
834
881
  type: diagram.type,
835
- version: 1, // Default version
882
+ version: 1,
836
883
  updated_at: diagram.updatedAt,
837
884
  })),
838
885
  next_page: response.pagination.page < response.pagination.totalPages
@@ -856,8 +903,12 @@ export class PlanformMCPServer {
856
903
  }
857
904
  async handleNodesCreate(args) {
858
905
  this.logger.info('Nodes create requested');
859
- if (!args.diagram_uuid || !args.kind) {
860
- throw new Error('diagram_uuid and kind are required');
906
+ if (!args.kind) {
907
+ throw new Error('kind is required');
908
+ }
909
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
910
+ if (!diagramUuid) {
911
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
861
912
  }
862
913
  try {
863
914
  const requestPayload = {
@@ -870,11 +921,10 @@ export class PlanformMCPServer {
870
921
  group: args.group || null,
871
922
  meta: args.meta || null,
872
923
  };
873
- // Log the request payload for debugging enum values
874
924
  if (args.kind === 'enum') {
875
925
  this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
876
926
  }
877
- const response = await this.apiClient.createNode(args.diagram_uuid, requestPayload);
927
+ const response = await this.apiClient.createNode(diagramUuid, requestPayload);
878
928
  return {
879
929
  content: [
880
930
  {
@@ -895,11 +945,20 @@ export class PlanformMCPServer {
895
945
  }
896
946
  async handleNodeUpdate(args) {
897
947
  this.logger.info('Node update requested');
898
- if (!args.diagram_uuid || !args.node_id || !args.patch) {
899
- throw new Error('diagram_uuid, node_id, and patch are required');
948
+ if (!args.patch) {
949
+ throw new Error('patch is required');
950
+ }
951
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
952
+ if (!nodeIdentifier) {
953
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
954
+ }
955
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
956
+ if (!diagramUuid) {
957
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
900
958
  }
901
959
  try {
902
- const response = await this.apiClient.updateNode(args.diagram_uuid, args.node_id, args.patch);
960
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
961
+ const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
903
962
  return {
904
963
  content: [
905
964
  {
@@ -920,11 +979,20 @@ export class PlanformMCPServer {
920
979
  }
921
980
  async handleNodeVerify(args) {
922
981
  this.logger.info('Node verify requested');
923
- if (!args.diagram_uuid || !args.node_id || !args.structural_fields) {
924
- throw new Error('diagram_uuid, node_id, and structural_fields are required');
982
+ if (!args.structural_fields) {
983
+ throw new Error('structural_fields is required');
984
+ }
985
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
986
+ if (!nodeIdentifier) {
987
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
988
+ }
989
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
990
+ if (!diagramUuid) {
991
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
925
992
  }
926
993
  try {
927
- const response = await this.apiClient.verifyNode(args.diagram_uuid, args.node_id, args.structural_fields);
994
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
995
+ const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
928
996
  return {
929
997
  content: [
930
998
  {
@@ -942,11 +1010,17 @@ export class PlanformMCPServer {
942
1010
  }
943
1011
  async handleNodeDelete(args) {
944
1012
  this.logger.info('Node delete requested');
945
- if (!args.diagram_uuid || !args.node_id) {
946
- throw new Error('diagram_uuid and node_id are required');
1013
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1014
+ if (!nodeIdentifier) {
1015
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1016
+ }
1017
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1018
+ if (!diagramUuid) {
1019
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
947
1020
  }
948
1021
  try {
949
- const response = await this.apiClient.deleteNode(args.diagram_uuid, args.node_id);
1022
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1023
+ const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
950
1024
  return {
951
1025
  content: [
952
1026
  {
@@ -964,19 +1038,24 @@ export class PlanformMCPServer {
964
1038
  }
965
1039
  async handleLinksCreate(args) {
966
1040
  this.logger.info('Links create requested');
967
- if (!args.diagram_uuid || !args.kind || !args.from || !args.to) {
968
- throw new Error('diagram_uuid, kind, from, and to are required');
1041
+ if (!args.kind || !args.from || !args.to) {
1042
+ throw new Error('kind, from, and to are required (from/to can be node names, e.g. "User" and "Account").');
1043
+ }
1044
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1045
+ if (!diagramUuid) {
1046
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
969
1047
  }
970
- // Validate that "none" is not used for inheritance/implements/dependency links
971
1048
  const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
972
1049
  if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
973
1050
  throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
974
1051
  }
975
1052
  try {
1053
+ const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
1054
+ const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
976
1055
  const requestBody = {
977
1056
  kind: args.kind,
978
- fromNode: args.from,
979
- toNode: args.to,
1057
+ fromNode: fromLabel,
1058
+ toNode: toLabel,
980
1059
  name: args.label || null,
981
1060
  meta: args.meta || null,
982
1061
  fromMultiplicity: args.fromMultiplicity || null,
@@ -986,7 +1065,7 @@ export class PlanformMCPServer {
986
1065
  if (args.directional !== undefined) {
987
1066
  requestBody.directional = args.directional;
988
1067
  }
989
- const response = await this.apiClient.createLink(args.diagram_uuid, requestBody);
1068
+ const response = await this.apiClient.createLink(diagramUuid, requestBody);
990
1069
  return {
991
1070
  content: [
992
1071
  {
@@ -1007,11 +1086,15 @@ export class PlanformMCPServer {
1007
1086
  }
1008
1087
  async handleLinkUpdate(args) {
1009
1088
  this.logger.info('Link update requested');
1010
- if (!args.diagram_uuid || !args.link_id || !args.patch) {
1011
- throw new Error('diagram_uuid, link_id, and patch are required');
1089
+ if (!args.link_id || !args.patch) {
1090
+ throw new Error('link_id and patch are required (link_id comes from create_link or open_diagram response).');
1091
+ }
1092
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1093
+ if (!diagramUuid) {
1094
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1012
1095
  }
1013
1096
  try {
1014
- const response = await this.apiClient.updateLink(args.diagram_uuid, args.link_id, args.patch);
1097
+ const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
1015
1098
  return {
1016
1099
  content: [
1017
1100
  {
@@ -1032,11 +1115,15 @@ export class PlanformMCPServer {
1032
1115
  }
1033
1116
  async handleLinkVerify(args) {
1034
1117
  this.logger.info('Link verify requested');
1035
- if (!args.diagram_uuid || !args.link_id || !args.structural_fields) {
1036
- throw new Error('diagram_uuid, link_id, and structural_fields are required');
1118
+ if (!args.link_id || !args.structural_fields) {
1119
+ throw new Error('link_id and structural_fields are required.');
1120
+ }
1121
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1122
+ if (!diagramUuid) {
1123
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1037
1124
  }
1038
1125
  try {
1039
- const response = await this.apiClient.verifyLink(args.diagram_uuid, args.link_id, args.structural_fields);
1126
+ const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
1040
1127
  return {
1041
1128
  content: [
1042
1129
  {
@@ -1054,11 +1141,15 @@ export class PlanformMCPServer {
1054
1141
  }
1055
1142
  async handleLinkDelete(args) {
1056
1143
  this.logger.info('Link delete requested');
1057
- if (!args.diagram_uuid || !args.link_id) {
1058
- throw new Error('diagram_uuid and link_id are required');
1144
+ if (!args.link_id) {
1145
+ throw new Error('link_id is required (from create_link or open_diagram response).');
1146
+ }
1147
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1148
+ if (!diagramUuid) {
1149
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1059
1150
  }
1060
1151
  try {
1061
- const response = await this.apiClient.deleteLink(args.diagram_uuid, args.link_id);
1152
+ const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
1062
1153
  return {
1063
1154
  content: [
1064
1155
  {