@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/.mcpregistry_github_token +1 -1
- package/.mcpregistry_registry_token +1 -1
- package/MCP_CONNECTION_GUIDE.md +20 -10
- package/README.md +27 -39
- package/dist/api-client.js +14 -14
- package/dist/api-client.js.map +1 -1
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +263 -172
- package/dist/server.js.map +1 -1
- package/dist/usage-examples.d.ts +2 -2
- package/dist/usage-examples.d.ts.map +1 -1
- package/dist/usage-examples.js +30 -22
- package/dist/usage-examples.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/api-client.ts +14 -14
- package/src/server.ts +276 -191
- package/src/usage-examples.ts +30 -22
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: '
|
|
57
|
-
description: 'Get
|
|
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: '
|
|
65
|
-
description: '
|
|
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: '
|
|
73
|
-
description: '[Optional/Advanced] Manually poll for
|
|
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
|
|
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: '
|
|
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: '
|
|
95
|
-
description: '
|
|
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: '
|
|
146
|
-
description: '
|
|
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
|
-
|
|
132
|
+
title: {
|
|
151
133
|
type: 'string',
|
|
152
|
-
description: 'Diagram
|
|
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: ['
|
|
141
|
+
required: ['title', 'type'],
|
|
156
142
|
},
|
|
157
143
|
},
|
|
158
144
|
{
|
|
159
|
-
name: '
|
|
160
|
-
description: '
|
|
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
|
-
|
|
150
|
+
diagram_identifier: {
|
|
165
151
|
type: 'string',
|
|
166
|
-
description: 'Diagram
|
|
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: ['
|
|
227
|
+
required: ['kind'],
|
|
233
228
|
},
|
|
234
229
|
},
|
|
235
230
|
{
|
|
236
|
-
name: '
|
|
237
|
-
description:
|
|
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
|
-
|
|
236
|
+
node_identifier: {
|
|
242
237
|
type: 'string',
|
|
243
|
-
description: '
|
|
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: '
|
|
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: ['
|
|
249
|
+
required: ['node_identifier', 'patch'],
|
|
259
250
|
},
|
|
260
251
|
},
|
|
261
252
|
{
|
|
262
|
-
name: '
|
|
263
|
-
description: 'Mark a node as verified
|
|
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
|
-
|
|
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: ['
|
|
271
|
+
required: ['node_identifier', 'structural_fields'],
|
|
285
272
|
},
|
|
286
273
|
},
|
|
287
274
|
{
|
|
288
|
-
name: '
|
|
289
|
-
description: 'Delete node
|
|
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
|
-
|
|
280
|
+
node_identifier: {
|
|
294
281
|
type: 'string',
|
|
295
|
-
description: '
|
|
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: ['
|
|
285
|
+
required: ['node_identifier'],
|
|
303
286
|
},
|
|
304
287
|
},
|
|
305
288
|
{
|
|
306
|
-
name: '
|
|
307
|
-
description: 'Create link between
|
|
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
|
|
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-
|
|
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
|
|
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: ['
|
|
317
|
+
required: ['kind', 'from', 'to'],
|
|
339
318
|
},
|
|
340
319
|
},
|
|
341
320
|
{
|
|
342
|
-
name: '
|
|
343
|
-
description:
|
|
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: ['
|
|
339
|
+
required: ['link_id', 'patch'],
|
|
365
340
|
},
|
|
366
341
|
},
|
|
367
342
|
{
|
|
368
|
-
name: '
|
|
369
|
-
description: 'Mark a link as verified
|
|
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: ['
|
|
361
|
+
required: ['link_id', 'structural_fields'],
|
|
391
362
|
},
|
|
392
363
|
},
|
|
393
364
|
{
|
|
394
|
-
name: '
|
|
395
|
-
description: 'Delete link
|
|
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: ['
|
|
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 '
|
|
392
|
+
case 'get_usage_guide':
|
|
426
393
|
return await this.handleGetUsageExamples();
|
|
427
394
|
|
|
428
|
-
case '
|
|
395
|
+
case 'sign_in':
|
|
429
396
|
return await this.handleDeviceStart();
|
|
430
397
|
|
|
431
|
-
case '
|
|
398
|
+
case 'poll_auth_token':
|
|
432
399
|
return await this.handleDeviceTokenPoll(request.params.arguments);
|
|
433
400
|
|
|
434
|
-
case '
|
|
401
|
+
case 'check_backend_health':
|
|
435
402
|
return await this.handleBackendHealthCheck();
|
|
436
403
|
|
|
437
|
-
case '
|
|
404
|
+
case 'create_diagram':
|
|
438
405
|
return await this.handleDiagramCreate(request.params.arguments);
|
|
439
406
|
|
|
440
|
-
case '
|
|
407
|
+
case 'open_diagram':
|
|
441
408
|
return await this.handleDiagramGet(request.params.arguments);
|
|
442
409
|
|
|
443
|
-
case '
|
|
410
|
+
case 'list_diagrams':
|
|
444
411
|
return await this.handleDiagramsList(request.params.arguments);
|
|
445
412
|
|
|
446
|
-
case '
|
|
413
|
+
case 'create_node':
|
|
447
414
|
return await this.handleNodesCreate(request.params.arguments);
|
|
448
415
|
|
|
449
|
-
case '
|
|
416
|
+
case 'update_node':
|
|
450
417
|
return await this.handleNodeUpdate(request.params.arguments);
|
|
451
418
|
|
|
452
|
-
case '
|
|
419
|
+
case 'verify_node':
|
|
453
420
|
return await this.handleNodeVerify(request.params.arguments);
|
|
454
421
|
|
|
455
|
-
case '
|
|
422
|
+
case 'delete_node':
|
|
456
423
|
return await this.handleNodeDelete(request.params.arguments);
|
|
457
424
|
|
|
458
|
-
case '
|
|
425
|
+
case 'create_link':
|
|
459
426
|
return await this.handleLinksCreate(request.params.arguments);
|
|
460
427
|
|
|
461
|
-
case '
|
|
428
|
+
case 'update_link':
|
|
462
429
|
return await this.handleLinkUpdate(request.params.arguments);
|
|
463
430
|
|
|
464
|
-
case '
|
|
431
|
+
case 'verify_link':
|
|
465
432
|
return await this.handleLinkVerify(request.params.arguments);
|
|
466
433
|
|
|
467
|
-
case '
|
|
434
|
+
case 'delete_link':
|
|
468
435
|
return await this.handleLinkDelete(request.params.arguments);
|
|
469
436
|
|
|
470
437
|
default:
|
|
@@ -497,6 +464,7 @@ export class PlanformMCPServer {
|
|
|
497
464
|
server: 'planform-mcp',
|
|
498
465
|
version: process.env.MCP_SERVER_VERSION || '1.0.0',
|
|
499
466
|
timestamp: new Date().toISOString(),
|
|
467
|
+
backend_base_url: process.env.BACKEND_BASE_URL || 'https://www.planform.io/api',
|
|
500
468
|
}, null, 2),
|
|
501
469
|
},
|
|
502
470
|
],
|
|
@@ -591,6 +559,9 @@ export class PlanformMCPServer {
|
|
|
591
559
|
if (pollResult.success) {
|
|
592
560
|
deviceInfo.message = '✅ Authentication successful! Token received and stored.';
|
|
593
561
|
deviceInfo.access_token_received = true;
|
|
562
|
+
if (response.session.approvedUserUuid) {
|
|
563
|
+
this.sessionUserUuid = response.session.approvedUserUuid;
|
|
564
|
+
}
|
|
594
565
|
} else {
|
|
595
566
|
deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
|
|
596
567
|
deviceInfo.access_token_received = false;
|
|
@@ -643,7 +614,7 @@ export class PlanformMCPServer {
|
|
|
643
614
|
this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
|
|
644
615
|
return {
|
|
645
616
|
success: false,
|
|
646
|
-
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use
|
|
617
|
+
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
|
|
647
618
|
};
|
|
648
619
|
} else {
|
|
649
620
|
this.logger.warn('Device code expired while waiting for approval');
|
|
@@ -690,6 +661,63 @@ export class PlanformMCPServer {
|
|
|
690
661
|
}
|
|
691
662
|
}
|
|
692
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Returns the current diagram data (nodes, links, etc.). Throws if no diagram is selected.
|
|
666
|
+
* Used to resolve node names to uuids/labels under the hood.
|
|
667
|
+
*/
|
|
668
|
+
private async getCurrentDiagramData(): Promise<{ uuid: string; nodes: Array<{ uuid: string; label: string; name: string | null }>; links: Array<{ uuid: string }> }> {
|
|
669
|
+
const diagramUuid = this.currentDiagramUuid;
|
|
670
|
+
if (!diagramUuid) {
|
|
671
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
672
|
+
}
|
|
673
|
+
const diagram = await this.apiClient.getDiagram(diagramUuid);
|
|
674
|
+
return {
|
|
675
|
+
uuid: diagram.uuid,
|
|
676
|
+
nodes: diagram.nodes.map((n) => ({ uuid: n.uuid, label: n.label, name: n.name })),
|
|
677
|
+
links: diagram.links.map((l) => ({ uuid: l.uuid })),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Resolve node_identifier (name or UUID) to node UUID using the current diagram.
|
|
683
|
+
*/
|
|
684
|
+
private async resolveNodeToUuid(diagramUuid: string, nodeIdentifier: string): Promise<string> {
|
|
685
|
+
if (UUID_REGEX.test(nodeIdentifier)) {
|
|
686
|
+
return nodeIdentifier;
|
|
687
|
+
}
|
|
688
|
+
const data = await this.getCurrentDiagramData();
|
|
689
|
+
if (data.uuid !== diagramUuid) {
|
|
690
|
+
throw new Error('Current diagram changed; resolve again.');
|
|
691
|
+
}
|
|
692
|
+
const byName = data.nodes.find(
|
|
693
|
+
(n) => n.name === nodeIdentifier || n.name?.toLowerCase() === String(nodeIdentifier).toLowerCase()
|
|
694
|
+
);
|
|
695
|
+
if (!byName) {
|
|
696
|
+
throw new Error(`No node named "${nodeIdentifier}" in the current diagram. Use node names or UUIDs.`);
|
|
697
|
+
}
|
|
698
|
+
return byName.uuid;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Resolve node reference (name or label like n-1) to node label for create_link.
|
|
703
|
+
*/
|
|
704
|
+
private async resolveNodeToLabel(diagramUuid: string, nodeRef: string): Promise<string> {
|
|
705
|
+
if (NODE_LABEL_REGEX.test(nodeRef)) {
|
|
706
|
+
return nodeRef;
|
|
707
|
+
}
|
|
708
|
+
const data = await this.getCurrentDiagramData();
|
|
709
|
+
if (data.uuid !== diagramUuid) {
|
|
710
|
+
throw new Error('Current diagram changed; resolve again.');
|
|
711
|
+
}
|
|
712
|
+
const byName = data.nodes.find(
|
|
713
|
+
(n) => n.name === nodeRef || n.name?.toLowerCase() === String(nodeRef).toLowerCase()
|
|
714
|
+
);
|
|
715
|
+
if (!byName) {
|
|
716
|
+
throw new Error(`No node named "${nodeRef}" in the current diagram. Use node names (e.g. "User") or labels (e.g. n-1).`);
|
|
717
|
+
}
|
|
718
|
+
return byName.label;
|
|
719
|
+
}
|
|
720
|
+
|
|
693
721
|
/**
|
|
694
722
|
* Start automatic background polling for device token (kept for backward compatibility)
|
|
695
723
|
* @deprecated Use pollUntilApproved for synchronous polling
|
|
@@ -810,20 +838,24 @@ export class PlanformMCPServer {
|
|
|
810
838
|
private async handleDiagramCreate(args: any): Promise<CallToolResult> {
|
|
811
839
|
this.logger.info('Diagram create requested');
|
|
812
840
|
|
|
813
|
-
if (!args.
|
|
814
|
-
throw new Error('
|
|
841
|
+
if (!args.title) {
|
|
842
|
+
throw new Error('title is required');
|
|
843
|
+
}
|
|
844
|
+
if (!args.type) {
|
|
845
|
+
throw new Error('type is required');
|
|
815
846
|
}
|
|
816
847
|
|
|
817
848
|
try {
|
|
818
|
-
if (!args.type) {
|
|
819
|
-
throw new Error('type is required');
|
|
820
|
-
}
|
|
821
849
|
|
|
822
|
-
const
|
|
850
|
+
const userUuid = args.user_uuid ?? this.sessionUserUuid;
|
|
851
|
+
if (!userUuid) {
|
|
852
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
853
|
+
}
|
|
854
|
+
const response = await this.apiClient.createDiagram(userUuid, {
|
|
823
855
|
title: args.title,
|
|
824
856
|
type: args.type,
|
|
825
857
|
});
|
|
826
|
-
|
|
858
|
+
this.currentDiagramUuid = response.uuid;
|
|
827
859
|
// Return the response in the format specified by the design document
|
|
828
860
|
const result = {
|
|
829
861
|
diagram_uuid: response.uuid,
|
|
@@ -850,14 +882,33 @@ export class PlanformMCPServer {
|
|
|
850
882
|
|
|
851
883
|
private async handleDiagramGet(args: any): Promise<CallToolResult> {
|
|
852
884
|
this.logger.info('Diagram get requested');
|
|
853
|
-
|
|
854
|
-
if (!args.diagram_uuid) {
|
|
855
|
-
throw new Error('diagram_uuid is required');
|
|
856
|
-
}
|
|
857
885
|
|
|
858
886
|
try {
|
|
859
|
-
|
|
860
|
-
|
|
887
|
+
let diagramUuid: string;
|
|
888
|
+
const identifier = args.diagram_identifier ?? args.diagram_uuid;
|
|
889
|
+
if (!identifier) {
|
|
890
|
+
diagramUuid = this.currentDiagramUuid ?? '';
|
|
891
|
+
if (!diagramUuid) {
|
|
892
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram(diagram_identifier) first.');
|
|
893
|
+
}
|
|
894
|
+
} else if (UUID_REGEX.test(identifier)) {
|
|
895
|
+
diagramUuid = identifier;
|
|
896
|
+
} else {
|
|
897
|
+
const userUuid = this.sessionUserUuid;
|
|
898
|
+
if (!userUuid) {
|
|
899
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
900
|
+
}
|
|
901
|
+
const listRes = await this.apiClient.getUserDiagrams(userUuid, 1, 100);
|
|
902
|
+
const byTitle = listRes.diagrams.find(
|
|
903
|
+
(d) => d.title === identifier || d.title?.toLowerCase() === String(identifier).toLowerCase()
|
|
904
|
+
);
|
|
905
|
+
if (!byTitle) {
|
|
906
|
+
throw new Error(`No diagram found with title "${identifier}". Use list_diagrams to see available diagrams.`);
|
|
907
|
+
}
|
|
908
|
+
diagramUuid = byTitle.uuid;
|
|
909
|
+
}
|
|
910
|
+
const response = await this.apiClient.getDiagram(diagramUuid);
|
|
911
|
+
this.currentDiagramUuid = response.uuid;
|
|
861
912
|
// Return the response in the format specified by the design document
|
|
862
913
|
const result = {
|
|
863
914
|
diagram: {
|
|
@@ -873,7 +924,6 @@ export class PlanformMCPServer {
|
|
|
873
924
|
links: response.links,
|
|
874
925
|
},
|
|
875
926
|
};
|
|
876
|
-
|
|
877
927
|
return {
|
|
878
928
|
content: [
|
|
879
929
|
{
|
|
@@ -891,31 +941,31 @@ export class PlanformMCPServer {
|
|
|
891
941
|
|
|
892
942
|
private async handleDiagramsList(args: any): Promise<CallToolResult> {
|
|
893
943
|
this.logger.info('Diagrams list requested');
|
|
894
|
-
|
|
895
|
-
if (!
|
|
896
|
-
throw new Error('
|
|
944
|
+
const userUuid = args.user_uuid ?? this.sessionUserUuid;
|
|
945
|
+
if (!userUuid) {
|
|
946
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
897
947
|
}
|
|
898
948
|
|
|
899
949
|
try {
|
|
900
950
|
const response = await this.apiClient.getUserDiagrams(
|
|
901
|
-
|
|
951
|
+
userUuid,
|
|
902
952
|
args.page || 1,
|
|
903
953
|
args.page_size || 10
|
|
904
954
|
);
|
|
905
|
-
|
|
906
955
|
// Return the response in the format specified by the design document
|
|
907
956
|
const result = {
|
|
908
|
-
items: response.diagrams.map(diagram => ({
|
|
957
|
+
items: response.diagrams.map((diagram) => ({
|
|
909
958
|
diagram_uuid: diagram.uuid,
|
|
910
|
-
external_id: diagram.label,
|
|
959
|
+
external_id: diagram.label,
|
|
911
960
|
title: diagram.title,
|
|
912
961
|
type: diagram.type,
|
|
913
|
-
version: 1,
|
|
962
|
+
version: 1,
|
|
914
963
|
updated_at: diagram.updatedAt,
|
|
915
964
|
})),
|
|
916
|
-
next_page:
|
|
917
|
-
|
|
918
|
-
|
|
965
|
+
next_page:
|
|
966
|
+
response.pagination.page < response.pagination.totalPages
|
|
967
|
+
? (response.pagination.page + 1).toString()
|
|
968
|
+
: undefined,
|
|
919
969
|
};
|
|
920
970
|
|
|
921
971
|
return {
|
|
@@ -935,9 +985,12 @@ export class PlanformMCPServer {
|
|
|
935
985
|
|
|
936
986
|
private async handleNodesCreate(args: any): Promise<CallToolResult> {
|
|
937
987
|
this.logger.info('Nodes create requested');
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
988
|
+
if (!args.kind) {
|
|
989
|
+
throw new Error('kind is required');
|
|
990
|
+
}
|
|
991
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
992
|
+
if (!diagramUuid) {
|
|
993
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
941
994
|
}
|
|
942
995
|
|
|
943
996
|
try {
|
|
@@ -952,12 +1005,11 @@ export class PlanformMCPServer {
|
|
|
952
1005
|
meta: args.meta || null,
|
|
953
1006
|
};
|
|
954
1007
|
|
|
955
|
-
// Log the request payload for debugging enum values
|
|
956
1008
|
if (args.kind === 'enum') {
|
|
957
1009
|
this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
|
|
958
1010
|
}
|
|
959
1011
|
|
|
960
|
-
const response = await this.apiClient.createNode(
|
|
1012
|
+
const response = await this.apiClient.createNode(diagramUuid, requestPayload);
|
|
961
1013
|
|
|
962
1014
|
return {
|
|
963
1015
|
content: [
|
|
@@ -979,13 +1031,21 @@ export class PlanformMCPServer {
|
|
|
979
1031
|
|
|
980
1032
|
private async handleNodeUpdate(args: any): Promise<CallToolResult> {
|
|
981
1033
|
this.logger.info('Node update requested');
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1034
|
+
if (!args.patch) {
|
|
1035
|
+
throw new Error('patch is required');
|
|
1036
|
+
}
|
|
1037
|
+
const nodeIdentifier = args.node_identifier ?? args.node_id;
|
|
1038
|
+
if (!nodeIdentifier) {
|
|
1039
|
+
throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
|
|
1040
|
+
}
|
|
1041
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1042
|
+
if (!diagramUuid) {
|
|
1043
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
985
1044
|
}
|
|
986
1045
|
|
|
987
1046
|
try {
|
|
988
|
-
const
|
|
1047
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
1048
|
+
const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
|
|
989
1049
|
|
|
990
1050
|
return {
|
|
991
1051
|
content: [
|
|
@@ -1007,13 +1067,21 @@ export class PlanformMCPServer {
|
|
|
1007
1067
|
|
|
1008
1068
|
private async handleNodeVerify(args: any): Promise<CallToolResult> {
|
|
1009
1069
|
this.logger.info('Node verify requested');
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1070
|
+
if (!args.structural_fields) {
|
|
1071
|
+
throw new Error('structural_fields is required');
|
|
1072
|
+
}
|
|
1073
|
+
const nodeIdentifier = args.node_identifier ?? args.node_id;
|
|
1074
|
+
if (!nodeIdentifier) {
|
|
1075
|
+
throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
|
|
1076
|
+
}
|
|
1077
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1078
|
+
if (!diagramUuid) {
|
|
1079
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1013
1080
|
}
|
|
1014
1081
|
|
|
1015
1082
|
try {
|
|
1016
|
-
const
|
|
1083
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
1084
|
+
const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
|
|
1017
1085
|
|
|
1018
1086
|
return {
|
|
1019
1087
|
content: [
|
|
@@ -1032,13 +1100,18 @@ export class PlanformMCPServer {
|
|
|
1032
1100
|
|
|
1033
1101
|
private async handleNodeDelete(args: any): Promise<CallToolResult> {
|
|
1034
1102
|
this.logger.info('Node delete requested');
|
|
1035
|
-
|
|
1036
|
-
if (!
|
|
1037
|
-
throw new Error('
|
|
1103
|
+
const nodeIdentifier = args.node_identifier ?? args.node_id;
|
|
1104
|
+
if (!nodeIdentifier) {
|
|
1105
|
+
throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
|
|
1106
|
+
}
|
|
1107
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1108
|
+
if (!diagramUuid) {
|
|
1109
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1038
1110
|
}
|
|
1039
1111
|
|
|
1040
1112
|
try {
|
|
1041
|
-
const
|
|
1113
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
1114
|
+
const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
|
|
1042
1115
|
|
|
1043
1116
|
return {
|
|
1044
1117
|
content: [
|
|
@@ -1057,22 +1130,26 @@ export class PlanformMCPServer {
|
|
|
1057
1130
|
|
|
1058
1131
|
private async handleLinksCreate(args: any): Promise<CallToolResult> {
|
|
1059
1132
|
this.logger.info('Links create requested');
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1133
|
+
if (!args.kind || !args.from || !args.to) {
|
|
1134
|
+
throw new Error('kind, from, and to are required (from/to can be node names, e.g. "User" and "Account").');
|
|
1135
|
+
}
|
|
1136
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1137
|
+
if (!diagramUuid) {
|
|
1138
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1063
1139
|
}
|
|
1064
1140
|
|
|
1065
|
-
// Validate that "none" is not used for inheritance/implements/dependency links
|
|
1066
1141
|
const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
|
|
1067
1142
|
if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
|
|
1068
1143
|
throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
|
|
1069
1144
|
}
|
|
1070
1145
|
|
|
1071
1146
|
try {
|
|
1147
|
+
const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
|
|
1148
|
+
const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
|
|
1072
1149
|
const requestBody: any = {
|
|
1073
1150
|
kind: args.kind,
|
|
1074
|
-
fromNode:
|
|
1075
|
-
toNode:
|
|
1151
|
+
fromNode: fromLabel,
|
|
1152
|
+
toNode: toLabel,
|
|
1076
1153
|
name: args.label || null,
|
|
1077
1154
|
meta: args.meta || null,
|
|
1078
1155
|
fromMultiplicity: args.fromMultiplicity || null,
|
|
@@ -1083,8 +1160,7 @@ export class PlanformMCPServer {
|
|
|
1083
1160
|
if (args.directional !== undefined) {
|
|
1084
1161
|
requestBody.directional = args.directional;
|
|
1085
1162
|
}
|
|
1086
|
-
|
|
1087
|
-
const response = await this.apiClient.createLink(args.diagram_uuid, requestBody);
|
|
1163
|
+
const response = await this.apiClient.createLink(diagramUuid, requestBody);
|
|
1088
1164
|
|
|
1089
1165
|
return {
|
|
1090
1166
|
content: [
|
|
@@ -1106,13 +1182,16 @@ export class PlanformMCPServer {
|
|
|
1106
1182
|
|
|
1107
1183
|
private async handleLinkUpdate(args: any): Promise<CallToolResult> {
|
|
1108
1184
|
this.logger.info('Link update requested');
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1185
|
+
if (!args.link_id || !args.patch) {
|
|
1186
|
+
throw new Error('link_id and patch are required (link_id comes from create_link or open_diagram response).');
|
|
1187
|
+
}
|
|
1188
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1189
|
+
if (!diagramUuid) {
|
|
1190
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1112
1191
|
}
|
|
1113
1192
|
|
|
1114
1193
|
try {
|
|
1115
|
-
const response = await this.apiClient.updateLink(
|
|
1194
|
+
const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
|
|
1116
1195
|
|
|
1117
1196
|
return {
|
|
1118
1197
|
content: [
|
|
@@ -1134,13 +1213,16 @@ export class PlanformMCPServer {
|
|
|
1134
1213
|
|
|
1135
1214
|
private async handleLinkVerify(args: any): Promise<CallToolResult> {
|
|
1136
1215
|
this.logger.info('Link verify requested');
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1216
|
+
if (!args.link_id || !args.structural_fields) {
|
|
1217
|
+
throw new Error('link_id and structural_fields are required.');
|
|
1218
|
+
}
|
|
1219
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1220
|
+
if (!diagramUuid) {
|
|
1221
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1140
1222
|
}
|
|
1141
1223
|
|
|
1142
1224
|
try {
|
|
1143
|
-
const response = await this.apiClient.verifyLink(
|
|
1225
|
+
const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
|
|
1144
1226
|
|
|
1145
1227
|
return {
|
|
1146
1228
|
content: [
|
|
@@ -1159,13 +1241,16 @@ export class PlanformMCPServer {
|
|
|
1159
1241
|
|
|
1160
1242
|
private async handleLinkDelete(args: any): Promise<CallToolResult> {
|
|
1161
1243
|
this.logger.info('Link delete requested');
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1244
|
+
if (!args.link_id) {
|
|
1245
|
+
throw new Error('link_id is required (from create_link or open_diagram response).');
|
|
1246
|
+
}
|
|
1247
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1248
|
+
if (!diagramUuid) {
|
|
1249
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
1165
1250
|
}
|
|
1166
1251
|
|
|
1167
1252
|
try {
|
|
1168
|
-
const response = await this.apiClient.deleteLink(
|
|
1253
|
+
const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
|
|
1169
1254
|
|
|
1170
1255
|
return {
|
|
1171
1256
|
content: [
|