@shirbarzur/planform-mcp-server 1.0.5 → 1.0.7

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
@@ -2,13 +2,22 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { ApiClient } from './api-client.js';
4
4
  import { Logger } from './logger.js';
5
+ import { loadToken, saveToken } from './token-store.js';
5
6
  import { USAGE_EXAMPLES } from './usage-examples.js';
6
7
  import open from 'open';
8
+ /** UUID v4 pattern for distinguishing UUIDs from names/titles */
9
+ 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;
10
+ /** Node label pattern (e.g. n-1, n-2) */
11
+ const NODE_LABEL_REGEX = /^n-\d+$/;
7
12
  export class PlanformMCPServer {
8
13
  server;
9
14
  apiClient;
10
15
  logger;
11
16
  activePolling = new Map();
17
+ /** Set after successful sign_in; used for list_diagrams and create_diagram so callers need not pass user_uuid */
18
+ sessionUserUuid = null;
19
+ /** Set after create_diagram or open_diagram; used for node/link tools so callers need not pass diagram_uuid */
20
+ currentDiagramUuid = null;
12
21
  constructor() {
13
22
  this.server = new Server({
14
23
  name: process.env.MCP_SERVER_NAME || 'planform-mcp',
@@ -21,8 +30,41 @@ export class PlanformMCPServer {
21
30
  });
22
31
  this.apiClient = new ApiClient();
23
32
  this.logger = new Logger();
33
+ this.restoreSessionFromDisk();
24
34
  this.setupHandlers();
25
35
  }
36
+ /**
37
+ * Load token and user UUID from disk and apply to apiClient and sessionUserUuid.
38
+ * Call at server init and at the start of each tool call so auth persists across process restarts.
39
+ * If user_uuid is missing from storage, we try to read it from the JWT "sub" claim (many backends put user id there).
40
+ */
41
+ restoreSessionFromDisk() {
42
+ const stored = loadToken();
43
+ if (!stored)
44
+ return;
45
+ this.apiClient.setAccessToken(stored.access_token);
46
+ if (stored.user_uuid) {
47
+ this.sessionUserUuid = stored.user_uuid;
48
+ }
49
+ else {
50
+ const sub = this.decodeJwtSub(stored.access_token);
51
+ if (sub)
52
+ this.sessionUserUuid = sub;
53
+ }
54
+ }
55
+ /** Decode JWT payload and return "sub" claim if present (no signature verification). */
56
+ decodeJwtSub(token) {
57
+ try {
58
+ const parts = token.split('.');
59
+ if (parts.length !== 3)
60
+ return null;
61
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
62
+ return typeof payload.sub === 'string' ? payload.sub : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
26
68
  setupHandlers() {
27
69
  // List available tools
28
70
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -37,37 +79,37 @@ export class PlanformMCPServer {
37
79
  },
38
80
  },
39
81
  {
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.',
82
+ name: 'get_usage_guide',
83
+ description: 'Get step-by-step instructions and the recommended flow for the Planform MCP. Call when unsure how to use the server.',
42
84
  inputSchema: {
43
85
  type: 'object',
44
86
  properties: {},
45
87
  },
46
88
  },
47
89
  {
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.',
90
+ name: 'sign_in',
91
+ 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
92
  inputSchema: {
51
93
  type: 'object',
52
94
  properties: {},
53
95
  },
54
96
  },
55
97
  {
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.',
98
+ name: 'poll_auth_token',
99
+ description: '[Optional/Advanced] Manually poll for auth token after user approves. Usually not needed since sign_in polls automatically.',
58
100
  inputSchema: {
59
101
  type: 'object',
60
102
  properties: {
61
103
  device_code: {
62
104
  type: 'string',
63
- description: 'Device code from device_start response',
105
+ description: 'Device code from sign_in response',
64
106
  },
65
107
  },
66
108
  required: ['device_code'],
67
109
  },
68
110
  },
69
111
  {
70
- name: 'backend_health_check',
112
+ name: 'check_backend_health',
71
113
  description: 'Check the health status of the Planform backend API',
72
114
  inputSchema: {
73
115
  type: 'object',
@@ -75,37 +117,11 @@ export class PlanformMCPServer {
75
117
  },
76
118
  },
77
119
  {
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.',
120
+ name: 'list_diagrams',
121
+ description: 'List your diagrams. Uses your session from sign_in—no IDs to pass. Optional: filter by status, page, page_size.',
80
122
  inputSchema: {
81
123
  type: 'object',
82
124
  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
125
  status: {
110
126
  type: 'string',
111
127
  description: 'Filter by status (active|archived)',
@@ -122,41 +138,53 @@ export class PlanformMCPServer {
122
138
  default: 10,
123
139
  },
124
140
  },
125
- required: ['user_uuid'],
126
141
  },
127
142
  },
128
143
  {
129
- name: 'diagram_get',
130
- description: 'Fetch full diagram (nodes + links) to mirror state locally. Requires prior authentication via device_start.',
144
+ name: 'create_diagram',
145
+ 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
146
  inputSchema: {
132
147
  type: 'object',
133
148
  properties: {
134
- diagram_uuid: {
149
+ title: {
135
150
  type: 'string',
136
- description: 'Diagram UUID',
151
+ description: 'Diagram title (e.g. "My class diagram")',
152
+ },
153
+ type: {
154
+ type: 'string',
155
+ description: 'Diagram type (e.g. "uml.class")',
137
156
  },
138
157
  },
139
- required: ['diagram_uuid'],
158
+ required: ['title', 'type'],
140
159
  },
141
160
  },
142
161
  {
143
- name: 'nodes_create',
144
- description: 'Create UML node with immutable grid placement',
162
+ name: 'open_diagram',
163
+ 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
164
  inputSchema: {
146
165
  type: 'object',
147
166
  properties: {
148
- diagram_uuid: {
167
+ diagram_identifier: {
149
168
  type: 'string',
150
- description: 'Diagram UUID',
169
+ description: 'Diagram title (e.g. "My class diagram") or diagram ID. Omit to refresh the current diagram.',
151
170
  },
171
+ },
172
+ },
173
+ },
174
+ {
175
+ name: 'create_node',
176
+ 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.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
152
180
  kind: {
153
181
  type: 'string',
154
- description: 'Node kind',
182
+ description: 'Node kind: class, interface, or enum',
155
183
  enum: ['class', 'interface', 'enum'],
156
184
  },
157
185
  name: {
158
186
  type: 'string',
159
- description: 'Node name',
187
+ description: 'Node name (e.g. "User")',
160
188
  },
161
189
  fields: {
162
190
  type: 'array',
@@ -213,48 +241,40 @@ export class PlanformMCPServer {
213
241
  items: { type: 'string' },
214
242
  },
215
243
  },
216
- required: ['diagram_uuid', 'kind'],
244
+ required: ['kind'],
217
245
  },
218
246
  },
219
247
  {
220
- name: 'node_update',
221
- description: 'Update structural fields (MCP authoritative)',
248
+ name: 'update_node',
249
+ 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
250
  inputSchema: {
223
251
  type: 'object',
224
252
  properties: {
225
- diagram_uuid: {
253
+ node_identifier: {
226
254
  type: 'string',
227
- description: 'Diagram UUID',
228
- },
229
- node_id: {
230
- type: 'string',
231
- description: 'Node ID',
255
+ description: 'Node name (e.g. "User") or node ID. The server resolves the name to the node in the current diagram.',
232
256
  },
233
257
  patch: {
234
258
  type: 'object',
235
- description: 'Node fields to update',
259
+ description: 'Fields to update (name, fields, methods, etc.)',
236
260
  },
237
261
  if_version: {
238
262
  type: 'number',
239
- description: 'Version for optimistic concurrency',
263
+ description: 'Version for optimistic concurrency (optional)',
240
264
  },
241
265
  },
242
- required: ['diagram_uuid', 'node_id', 'patch'],
266
+ required: ['node_identifier', 'patch'],
243
267
  },
244
268
  },
245
269
  {
246
- name: 'node_verify',
247
- description: 'Mark a node as verified by MCP after confirming structure in code',
270
+ name: 'verify_node',
271
+ 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
272
  inputSchema: {
249
273
  type: 'object',
250
274
  properties: {
251
- diagram_uuid: {
275
+ node_identifier: {
252
276
  type: 'string',
253
- description: 'Diagram UUID',
254
- },
255
- node_id: {
256
- type: 'string',
257
- description: 'Node ID',
277
+ description: 'Node name (e.g. "User") or node ID.',
258
278
  },
259
279
  structural_fields: {
260
280
  type: 'object',
@@ -265,37 +285,29 @@ export class PlanformMCPServer {
265
285
  description: 'Whether differences were detected',
266
286
  },
267
287
  },
268
- required: ['diagram_uuid', 'node_id', 'structural_fields'],
288
+ required: ['node_identifier', 'structural_fields'],
269
289
  },
270
290
  },
271
291
  {
272
- name: 'node_delete',
273
- description: 'Delete node with status-based logic',
292
+ name: 'delete_node',
293
+ description: 'Delete a node from the current diagram. Refer to the node by its name (e.g. "User").',
274
294
  inputSchema: {
275
295
  type: 'object',
276
296
  properties: {
277
- diagram_uuid: {
278
- type: 'string',
279
- description: 'Diagram UUID',
280
- },
281
- node_id: {
297
+ node_identifier: {
282
298
  type: 'string',
283
- description: 'Node ID',
299
+ description: 'Node name (e.g. "User") or node ID.',
284
300
  },
285
301
  },
286
- required: ['diagram_uuid', 'node_id'],
302
+ required: ['node_identifier'],
287
303
  },
288
304
  },
289
305
  {
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.',
306
+ name: 'create_link',
307
+ 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
308
  inputSchema: {
293
309
  type: 'object',
294
310
  properties: {
295
- diagram_uuid: {
296
- type: 'string',
297
- description: 'Diagram UUID',
298
- },
299
311
  kind: {
300
312
  type: 'string',
301
313
  description: 'Link kind',
@@ -303,11 +315,11 @@ export class PlanformMCPServer {
303
315
  },
304
316
  from: {
305
317
  type: 'string',
306
- description: 'Source node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
318
+ description: 'Source node name (e.g. "User") or label (e.g. n-1).',
307
319
  },
308
320
  to: {
309
321
  type: 'string',
310
- description: 'Target node label (e.g. n-1, n-2). Backend expects labels, not node UUIDs.',
322
+ description: 'Target node name (e.g. "Account") or label (e.g. n-2).',
311
323
  },
312
324
  label: {
313
325
  type: 'string',
@@ -315,26 +327,22 @@ export class PlanformMCPServer {
315
327
  },
316
328
  directional: {
317
329
  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.',
330
+ description: 'Link directionality: "none", "unidirectional", or "bidirectional". For inheritance/implements/dependency, use "unidirectional" or "bidirectional".',
319
331
  enum: ['none', 'unidirectional', 'bidirectional'],
320
332
  },
321
333
  },
322
- required: ['diagram_uuid', 'kind', 'from', 'to'],
334
+ required: ['kind', 'from', 'to'],
323
335
  },
324
336
  },
325
337
  {
326
- name: 'link_update',
327
- description: 'Update link fields (MCP authoritative)',
338
+ name: 'update_link',
339
+ 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
340
  inputSchema: {
329
341
  type: 'object',
330
342
  properties: {
331
- diagram_uuid: {
332
- type: 'string',
333
- description: 'Diagram UUID',
334
- },
335
343
  link_id: {
336
344
  type: 'string',
337
- description: 'Link ID',
345
+ description: 'Link ID (from create_link or open_diagram response).',
338
346
  },
339
347
  patch: {
340
348
  type: 'object',
@@ -342,25 +350,21 @@ export class PlanformMCPServer {
342
350
  },
343
351
  if_version: {
344
352
  type: 'number',
345
- description: 'Version for optimistic concurrency',
353
+ description: 'Version for optimistic concurrency (optional)',
346
354
  },
347
355
  },
348
- required: ['diagram_uuid', 'link_id', 'patch'],
356
+ required: ['link_id', 'patch'],
349
357
  },
350
358
  },
351
359
  {
352
- name: 'link_verify',
353
- description: 'Mark a link as verified by MCP after confirming relationship in code',
360
+ name: 'verify_link',
361
+ 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
362
  inputSchema: {
355
363
  type: 'object',
356
364
  properties: {
357
- diagram_uuid: {
358
- type: 'string',
359
- description: 'Diagram UUID',
360
- },
361
365
  link_id: {
362
366
  type: 'string',
363
- description: 'Link ID',
367
+ description: 'Link ID (from create_link or open_diagram response).',
364
368
  },
365
369
  structural_fields: {
366
370
  type: 'object',
@@ -371,25 +375,21 @@ export class PlanformMCPServer {
371
375
  description: 'Whether differences were detected',
372
376
  },
373
377
  },
374
- required: ['diagram_uuid', 'link_id', 'structural_fields'],
378
+ required: ['link_id', 'structural_fields'],
375
379
  },
376
380
  },
377
381
  {
378
- name: 'link_delete',
379
- description: 'Delete link with status-based logic',
382
+ name: 'delete_link',
383
+ description: 'Delete a link from the current diagram. link_id comes from create_link or open_diagram response.',
380
384
  inputSchema: {
381
385
  type: 'object',
382
386
  properties: {
383
- diagram_uuid: {
384
- type: 'string',
385
- description: 'Diagram UUID',
386
- },
387
387
  link_id: {
388
388
  type: 'string',
389
- description: 'Link ID',
389
+ description: 'Link ID (from create_link or open_diagram response).',
390
390
  },
391
391
  },
392
- required: ['diagram_uuid', 'link_id'],
392
+ required: ['link_id'],
393
393
  },
394
394
  },
395
395
  ];
@@ -397,40 +397,41 @@ export class PlanformMCPServer {
397
397
  });
398
398
  // Handle tool calls
399
399
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
400
+ this.restoreSessionFromDisk();
400
401
  this.logger.info(`Tool called: ${request.params.name}`);
401
402
  try {
402
403
  switch (request.params.name) {
403
404
  case 'health_check':
404
405
  return await this.handleHealthCheck();
405
- case 'get_usage_examples':
406
+ case 'get_usage_guide':
406
407
  return await this.handleGetUsageExamples();
407
- case 'device_start':
408
+ case 'sign_in':
408
409
  return await this.handleDeviceStart();
409
- case 'device_token_poll':
410
+ case 'poll_auth_token':
410
411
  return await this.handleDeviceTokenPoll(request.params.arguments);
411
- case 'backend_health_check':
412
+ case 'check_backend_health':
412
413
  return await this.handleBackendHealthCheck();
413
- case 'diagram_create':
414
+ case 'create_diagram':
414
415
  return await this.handleDiagramCreate(request.params.arguments);
415
- case 'diagram_get':
416
+ case 'open_diagram':
416
417
  return await this.handleDiagramGet(request.params.arguments);
417
- case 'diagrams_list':
418
+ case 'list_diagrams':
418
419
  return await this.handleDiagramsList(request.params.arguments);
419
- case 'nodes_create':
420
+ case 'create_node':
420
421
  return await this.handleNodesCreate(request.params.arguments);
421
- case 'node_update':
422
+ case 'update_node':
422
423
  return await this.handleNodeUpdate(request.params.arguments);
423
- case 'node_verify':
424
+ case 'verify_node':
424
425
  return await this.handleNodeVerify(request.params.arguments);
425
- case 'node_delete':
426
+ case 'delete_node':
426
427
  return await this.handleNodeDelete(request.params.arguments);
427
- case 'links_create':
428
+ case 'create_link':
428
429
  return await this.handleLinksCreate(request.params.arguments);
429
- case 'link_update':
430
+ case 'update_link':
430
431
  return await this.handleLinkUpdate(request.params.arguments);
431
- case 'link_verify':
432
+ case 'verify_link':
432
433
  return await this.handleLinkVerify(request.params.arguments);
433
- case 'link_delete':
434
+ case 'delete_link':
434
435
  return await this.handleLinkDelete(request.params.arguments);
435
436
  default:
436
437
  throw new Error(`Unknown tool: ${request.params.name}`);
@@ -542,6 +543,13 @@ export class PlanformMCPServer {
542
543
  if (pollResult.success) {
543
544
  deviceInfo.message = '✅ Authentication successful! Token received and stored.';
544
545
  deviceInfo.access_token_received = true;
546
+ if (response.session.approvedUserUuid) {
547
+ this.sessionUserUuid = response.session.approvedUserUuid;
548
+ }
549
+ const token = this.apiClient.getAccessToken();
550
+ if (token) {
551
+ saveToken(token, this.sessionUserUuid);
552
+ }
545
553
  }
546
554
  else {
547
555
  deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
@@ -585,7 +593,7 @@ export class PlanformMCPServer {
585
593
  this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
586
594
  return {
587
595
  success: false,
588
- error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use device_token_poll manually (optional), or try device_start again.`
596
+ error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
589
597
  };
590
598
  }
591
599
  else {
@@ -636,6 +644,56 @@ export class PlanformMCPServer {
636
644
  }
637
645
  }
638
646
  }
647
+ /**
648
+ * Returns the current diagram data (nodes, links, etc.). Throws if no diagram is selected.
649
+ * Used to resolve node names to uuids/labels under the hood.
650
+ */
651
+ async getCurrentDiagramData() {
652
+ const diagramUuid = this.currentDiagramUuid;
653
+ if (!diagramUuid) {
654
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
655
+ }
656
+ const diagram = await this.apiClient.getDiagram(diagramUuid);
657
+ return {
658
+ uuid: diagram.uuid,
659
+ nodes: diagram.nodes.map((n) => ({ uuid: n.uuid, label: n.label, name: n.name })),
660
+ links: diagram.links.map((l) => ({ uuid: l.uuid })),
661
+ };
662
+ }
663
+ /**
664
+ * Resolve node_identifier (name or UUID) to node UUID using the current diagram.
665
+ */
666
+ async resolveNodeToUuid(diagramUuid, nodeIdentifier) {
667
+ if (UUID_REGEX.test(nodeIdentifier)) {
668
+ return nodeIdentifier;
669
+ }
670
+ const data = await this.getCurrentDiagramData();
671
+ if (data.uuid !== diagramUuid) {
672
+ throw new Error('Current diagram changed; resolve again.');
673
+ }
674
+ const byName = data.nodes.find((n) => n.name === nodeIdentifier || n.name?.toLowerCase() === String(nodeIdentifier).toLowerCase());
675
+ if (!byName) {
676
+ throw new Error(`No node named "${nodeIdentifier}" in the current diagram. Use node names or UUIDs.`);
677
+ }
678
+ return byName.uuid;
679
+ }
680
+ /**
681
+ * Resolve node reference (name or label like n-1) to node label for create_link.
682
+ */
683
+ async resolveNodeToLabel(diagramUuid, nodeRef) {
684
+ if (NODE_LABEL_REGEX.test(nodeRef)) {
685
+ return nodeRef;
686
+ }
687
+ const data = await this.getCurrentDiagramData();
688
+ if (data.uuid !== diagramUuid) {
689
+ throw new Error('Current diagram changed; resolve again.');
690
+ }
691
+ const byName = data.nodes.find((n) => n.name === nodeRef || n.name?.toLowerCase() === String(nodeRef).toLowerCase());
692
+ if (!byName) {
693
+ throw new Error(`No node named "${nodeRef}" in the current diagram. Use node names (e.g. "User") or labels (e.g. n-1).`);
694
+ }
695
+ return byName.label;
696
+ }
639
697
  /**
640
698
  * Start automatic background polling for device token (kept for backward compatibility)
641
699
  * @deprecated Use pollUntilApproved for synchronous polling
@@ -748,17 +806,22 @@ export class PlanformMCPServer {
748
806
  }
749
807
  async handleDiagramCreate(args) {
750
808
  this.logger.info('Diagram create requested');
751
- if (!args.user_uuid || !args.title) {
752
- throw new Error('user_uuid and title are required');
809
+ if (!args.title) {
810
+ throw new Error('title is required');
811
+ }
812
+ if (!args.type) {
813
+ throw new Error('type is required');
753
814
  }
754
815
  try {
755
- if (!args.type) {
756
- throw new Error('type is required');
816
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
817
+ if (!userUuid) {
818
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
757
819
  }
758
- const response = await this.apiClient.createDiagram(args.user_uuid, {
820
+ const response = await this.apiClient.createDiagram(userUuid, {
759
821
  title: args.title,
760
822
  type: args.type,
761
823
  });
824
+ this.currentDiagramUuid = response.uuid;
762
825
  // Return the response in the format specified by the design document
763
826
  const result = {
764
827
  diagram_uuid: response.uuid,
@@ -784,11 +847,32 @@ export class PlanformMCPServer {
784
847
  }
785
848
  async handleDiagramGet(args) {
786
849
  this.logger.info('Diagram get requested');
787
- if (!args.diagram_uuid) {
788
- throw new Error('diagram_uuid is required');
789
- }
790
850
  try {
791
- const response = await this.apiClient.getDiagram(args.diagram_uuid);
851
+ let diagramUuid;
852
+ const identifier = args.diagram_identifier ?? args.diagram_uuid;
853
+ if (!identifier) {
854
+ diagramUuid = this.currentDiagramUuid ?? '';
855
+ if (!diagramUuid) {
856
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram(diagram_identifier) first.');
857
+ }
858
+ }
859
+ else if (UUID_REGEX.test(identifier)) {
860
+ diagramUuid = identifier;
861
+ }
862
+ else {
863
+ const userUuid = this.sessionUserUuid;
864
+ if (!userUuid) {
865
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
866
+ }
867
+ const listRes = await this.apiClient.getUserDiagrams(userUuid, 1, 100);
868
+ const byTitle = listRes.diagrams.find((d) => d.title === identifier || d.title?.toLowerCase() === String(identifier).toLowerCase());
869
+ if (!byTitle) {
870
+ throw new Error(`No diagram found with title "${identifier}". Use list_diagrams to see available diagrams.`);
871
+ }
872
+ diagramUuid = byTitle.uuid;
873
+ }
874
+ const response = await this.apiClient.getDiagram(diagramUuid);
875
+ this.currentDiagramUuid = response.uuid;
792
876
  // Return the response in the format specified by the design document
793
877
  const result = {
794
878
  diagram: {
@@ -821,19 +905,20 @@ export class PlanformMCPServer {
821
905
  }
822
906
  async handleDiagramsList(args) {
823
907
  this.logger.info('Diagrams list requested');
824
- if (!args.user_uuid) {
825
- throw new Error('user_uuid is required');
908
+ const userUuid = args.user_uuid ?? this.sessionUserUuid;
909
+ if (!userUuid) {
910
+ throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
826
911
  }
827
912
  try {
828
- const response = await this.apiClient.getUserDiagrams(args.user_uuid, args.page || 1, args.page_size || 10);
913
+ const response = await this.apiClient.getUserDiagrams(userUuid, args.page || 1, args.page_size || 10);
829
914
  // Return the response in the format specified by the design document
830
915
  const result = {
831
- items: response.diagrams.map(diagram => ({
916
+ items: response.diagrams.map((diagram) => ({
832
917
  diagram_uuid: diagram.uuid,
833
- external_id: diagram.label, // Using label as external_id for now
918
+ external_id: diagram.label,
834
919
  title: diagram.title,
835
920
  type: diagram.type,
836
- version: 1, // Default version
921
+ version: 1,
837
922
  updated_at: diagram.updatedAt,
838
923
  })),
839
924
  next_page: response.pagination.page < response.pagination.totalPages
@@ -857,8 +942,12 @@ export class PlanformMCPServer {
857
942
  }
858
943
  async handleNodesCreate(args) {
859
944
  this.logger.info('Nodes create requested');
860
- if (!args.diagram_uuid || !args.kind) {
861
- throw new Error('diagram_uuid and kind are required');
945
+ if (!args.kind) {
946
+ throw new Error('kind is required');
947
+ }
948
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
949
+ if (!diagramUuid) {
950
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
862
951
  }
863
952
  try {
864
953
  const requestPayload = {
@@ -871,11 +960,10 @@ export class PlanformMCPServer {
871
960
  group: args.group || null,
872
961
  meta: args.meta || null,
873
962
  };
874
- // Log the request payload for debugging enum values
875
963
  if (args.kind === 'enum') {
876
964
  this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
877
965
  }
878
- const response = await this.apiClient.createNode(args.diagram_uuid, requestPayload);
966
+ const response = await this.apiClient.createNode(diagramUuid, requestPayload);
879
967
  return {
880
968
  content: [
881
969
  {
@@ -896,11 +984,20 @@ export class PlanformMCPServer {
896
984
  }
897
985
  async handleNodeUpdate(args) {
898
986
  this.logger.info('Node update requested');
899
- if (!args.diagram_uuid || !args.node_id || !args.patch) {
900
- throw new Error('diagram_uuid, node_id, and patch are required');
987
+ if (!args.patch) {
988
+ throw new Error('patch is required');
989
+ }
990
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
991
+ if (!nodeIdentifier) {
992
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
993
+ }
994
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
995
+ if (!diagramUuid) {
996
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
901
997
  }
902
998
  try {
903
- const response = await this.apiClient.updateNode(args.diagram_uuid, args.node_id, args.patch);
999
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1000
+ const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
904
1001
  return {
905
1002
  content: [
906
1003
  {
@@ -921,11 +1018,20 @@ export class PlanformMCPServer {
921
1018
  }
922
1019
  async handleNodeVerify(args) {
923
1020
  this.logger.info('Node verify requested');
924
- if (!args.diagram_uuid || !args.node_id || !args.structural_fields) {
925
- throw new Error('diagram_uuid, node_id, and structural_fields are required');
1021
+ if (!args.structural_fields) {
1022
+ throw new Error('structural_fields is required');
1023
+ }
1024
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1025
+ if (!nodeIdentifier) {
1026
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1027
+ }
1028
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1029
+ if (!diagramUuid) {
1030
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
926
1031
  }
927
1032
  try {
928
- const response = await this.apiClient.verifyNode(args.diagram_uuid, args.node_id, args.structural_fields);
1033
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1034
+ const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
929
1035
  return {
930
1036
  content: [
931
1037
  {
@@ -943,11 +1049,17 @@ export class PlanformMCPServer {
943
1049
  }
944
1050
  async handleNodeDelete(args) {
945
1051
  this.logger.info('Node delete requested');
946
- if (!args.diagram_uuid || !args.node_id) {
947
- throw new Error('diagram_uuid and node_id are required');
1052
+ const nodeIdentifier = args.node_identifier ?? args.node_id;
1053
+ if (!nodeIdentifier) {
1054
+ throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
1055
+ }
1056
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1057
+ if (!diagramUuid) {
1058
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
948
1059
  }
949
1060
  try {
950
- const response = await this.apiClient.deleteNode(args.diagram_uuid, args.node_id);
1061
+ const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
1062
+ const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
951
1063
  return {
952
1064
  content: [
953
1065
  {
@@ -965,19 +1077,24 @@ export class PlanformMCPServer {
965
1077
  }
966
1078
  async handleLinksCreate(args) {
967
1079
  this.logger.info('Links create requested');
968
- if (!args.diagram_uuid || !args.kind || !args.from || !args.to) {
969
- throw new Error('diagram_uuid, kind, from, and to are required');
1080
+ if (!args.kind || !args.from || !args.to) {
1081
+ throw new Error('kind, from, and to are required (from/to can be node names, e.g. "User" and "Account").');
1082
+ }
1083
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1084
+ if (!diagramUuid) {
1085
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
970
1086
  }
971
- // Validate that "none" is not used for inheritance/implements/dependency links
972
1087
  const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
973
1088
  if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
974
1089
  throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
975
1090
  }
976
1091
  try {
1092
+ const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
1093
+ const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
977
1094
  const requestBody = {
978
1095
  kind: args.kind,
979
- fromNode: args.from,
980
- toNode: args.to,
1096
+ fromNode: fromLabel,
1097
+ toNode: toLabel,
981
1098
  name: args.label || null,
982
1099
  meta: args.meta || null,
983
1100
  fromMultiplicity: args.fromMultiplicity || null,
@@ -987,7 +1104,7 @@ export class PlanformMCPServer {
987
1104
  if (args.directional !== undefined) {
988
1105
  requestBody.directional = args.directional;
989
1106
  }
990
- const response = await this.apiClient.createLink(args.diagram_uuid, requestBody);
1107
+ const response = await this.apiClient.createLink(diagramUuid, requestBody);
991
1108
  return {
992
1109
  content: [
993
1110
  {
@@ -1008,11 +1125,15 @@ export class PlanformMCPServer {
1008
1125
  }
1009
1126
  async handleLinkUpdate(args) {
1010
1127
  this.logger.info('Link update requested');
1011
- if (!args.diagram_uuid || !args.link_id || !args.patch) {
1012
- throw new Error('diagram_uuid, link_id, and patch are required');
1128
+ if (!args.link_id || !args.patch) {
1129
+ throw new Error('link_id and patch are required (link_id comes from create_link or open_diagram response).');
1130
+ }
1131
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1132
+ if (!diagramUuid) {
1133
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1013
1134
  }
1014
1135
  try {
1015
- const response = await this.apiClient.updateLink(args.diagram_uuid, args.link_id, args.patch);
1136
+ const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
1016
1137
  return {
1017
1138
  content: [
1018
1139
  {
@@ -1033,11 +1154,15 @@ export class PlanformMCPServer {
1033
1154
  }
1034
1155
  async handleLinkVerify(args) {
1035
1156
  this.logger.info('Link verify requested');
1036
- if (!args.diagram_uuid || !args.link_id || !args.structural_fields) {
1037
- throw new Error('diagram_uuid, link_id, and structural_fields are required');
1157
+ if (!args.link_id || !args.structural_fields) {
1158
+ throw new Error('link_id and structural_fields are required.');
1159
+ }
1160
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1161
+ if (!diagramUuid) {
1162
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1038
1163
  }
1039
1164
  try {
1040
- const response = await this.apiClient.verifyLink(args.diagram_uuid, args.link_id, args.structural_fields);
1165
+ const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
1041
1166
  return {
1042
1167
  content: [
1043
1168
  {
@@ -1055,11 +1180,15 @@ export class PlanformMCPServer {
1055
1180
  }
1056
1181
  async handleLinkDelete(args) {
1057
1182
  this.logger.info('Link delete requested');
1058
- if (!args.diagram_uuid || !args.link_id) {
1059
- throw new Error('diagram_uuid and link_id are required');
1183
+ if (!args.link_id) {
1184
+ throw new Error('link_id is required (from create_link or open_diagram response).');
1185
+ }
1186
+ const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
1187
+ if (!diagramUuid) {
1188
+ throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
1060
1189
  }
1061
1190
  try {
1062
- const response = await this.apiClient.deleteLink(args.diagram_uuid, args.link_id);
1191
+ const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
1063
1192
  return {
1064
1193
  content: [
1065
1194
  {