@shirbarzur/planform-mcp-server 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +262 -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/server.ts +275 -191
- package/src/usage-examples.ts +30 -22
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: '
|
|
41
|
-
description: 'Get
|
|
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: '
|
|
49
|
-
description: '
|
|
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: '
|
|
57
|
-
description: '[Optional/Advanced] Manually poll for
|
|
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
|
|
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: '
|
|
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: '
|
|
79
|
-
description: '
|
|
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: '
|
|
130
|
-
description: '
|
|
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
|
-
|
|
115
|
+
title: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'Diagram title (e.g. "My class diagram")',
|
|
118
|
+
},
|
|
119
|
+
type: {
|
|
135
120
|
type: 'string',
|
|
136
|
-
description: 'Diagram
|
|
121
|
+
description: 'Diagram type (e.g. "uml.class")',
|
|
137
122
|
},
|
|
138
123
|
},
|
|
139
|
-
required: ['
|
|
124
|
+
required: ['title', 'type'],
|
|
140
125
|
},
|
|
141
126
|
},
|
|
142
127
|
{
|
|
143
|
-
name: '
|
|
144
|
-
description: '
|
|
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
|
-
|
|
133
|
+
diagram_identifier: {
|
|
149
134
|
type: 'string',
|
|
150
|
-
description: 'Diagram
|
|
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: ['
|
|
210
|
+
required: ['kind'],
|
|
217
211
|
},
|
|
218
212
|
},
|
|
219
213
|
{
|
|
220
|
-
name: '
|
|
221
|
-
description:
|
|
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
|
-
|
|
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: '
|
|
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: ['
|
|
232
|
+
required: ['node_identifier', 'patch'],
|
|
243
233
|
},
|
|
244
234
|
},
|
|
245
235
|
{
|
|
246
|
-
name: '
|
|
247
|
-
description: 'Mark a node as verified
|
|
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
|
-
|
|
241
|
+
node_identifier: {
|
|
252
242
|
type: 'string',
|
|
253
|
-
description: '
|
|
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: ['
|
|
254
|
+
required: ['node_identifier', 'structural_fields'],
|
|
269
255
|
},
|
|
270
256
|
},
|
|
271
257
|
{
|
|
272
|
-
name: '
|
|
273
|
-
description: 'Delete node
|
|
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
|
-
|
|
263
|
+
node_identifier: {
|
|
278
264
|
type: 'string',
|
|
279
|
-
description: '
|
|
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: ['
|
|
268
|
+
required: ['node_identifier'],
|
|
287
269
|
},
|
|
288
270
|
},
|
|
289
271
|
{
|
|
290
|
-
name: '
|
|
291
|
-
description: 'Create link between
|
|
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
|
|
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-
|
|
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
|
|
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: ['
|
|
300
|
+
required: ['kind', 'from', 'to'],
|
|
323
301
|
},
|
|
324
302
|
},
|
|
325
303
|
{
|
|
326
|
-
name: '
|
|
327
|
-
description:
|
|
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: ['
|
|
322
|
+
required: ['link_id', 'patch'],
|
|
349
323
|
},
|
|
350
324
|
},
|
|
351
325
|
{
|
|
352
|
-
name: '
|
|
353
|
-
description: 'Mark a link as verified
|
|
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: ['
|
|
344
|
+
required: ['link_id', 'structural_fields'],
|
|
375
345
|
},
|
|
376
346
|
},
|
|
377
347
|
{
|
|
378
|
-
name: '
|
|
379
|
-
description: 'Delete link
|
|
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: ['
|
|
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 '
|
|
371
|
+
case 'get_usage_guide':
|
|
406
372
|
return await this.handleGetUsageExamples();
|
|
407
|
-
case '
|
|
373
|
+
case 'sign_in':
|
|
408
374
|
return await this.handleDeviceStart();
|
|
409
|
-
case '
|
|
375
|
+
case 'poll_auth_token':
|
|
410
376
|
return await this.handleDeviceTokenPoll(request.params.arguments);
|
|
411
|
-
case '
|
|
377
|
+
case 'check_backend_health':
|
|
412
378
|
return await this.handleBackendHealthCheck();
|
|
413
|
-
case '
|
|
379
|
+
case 'create_diagram':
|
|
414
380
|
return await this.handleDiagramCreate(request.params.arguments);
|
|
415
|
-
case '
|
|
381
|
+
case 'open_diagram':
|
|
416
382
|
return await this.handleDiagramGet(request.params.arguments);
|
|
417
|
-
case '
|
|
383
|
+
case 'list_diagrams':
|
|
418
384
|
return await this.handleDiagramsList(request.params.arguments);
|
|
419
|
-
case '
|
|
385
|
+
case 'create_node':
|
|
420
386
|
return await this.handleNodesCreate(request.params.arguments);
|
|
421
|
-
case '
|
|
387
|
+
case 'update_node':
|
|
422
388
|
return await this.handleNodeUpdate(request.params.arguments);
|
|
423
|
-
case '
|
|
389
|
+
case 'verify_node':
|
|
424
390
|
return await this.handleNodeVerify(request.params.arguments);
|
|
425
|
-
case '
|
|
391
|
+
case 'delete_node':
|
|
426
392
|
return await this.handleNodeDelete(request.params.arguments);
|
|
427
|
-
case '
|
|
393
|
+
case 'create_link':
|
|
428
394
|
return await this.handleLinksCreate(request.params.arguments);
|
|
429
|
-
case '
|
|
395
|
+
case 'update_link':
|
|
430
396
|
return await this.handleLinkUpdate(request.params.arguments);
|
|
431
|
-
case '
|
|
397
|
+
case 'verify_link':
|
|
432
398
|
return await this.handleLinkVerify(request.params.arguments);
|
|
433
|
-
case '
|
|
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}`);
|
|
@@ -542,6 +508,9 @@ export class PlanformMCPServer {
|
|
|
542
508
|
if (pollResult.success) {
|
|
543
509
|
deviceInfo.message = '✅ Authentication successful! Token received and stored.';
|
|
544
510
|
deviceInfo.access_token_received = true;
|
|
511
|
+
if (response.session.approvedUserUuid) {
|
|
512
|
+
this.sessionUserUuid = response.session.approvedUserUuid;
|
|
513
|
+
}
|
|
545
514
|
}
|
|
546
515
|
else {
|
|
547
516
|
deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
|
|
@@ -585,7 +554,7 @@ export class PlanformMCPServer {
|
|
|
585
554
|
this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
|
|
586
555
|
return {
|
|
587
556
|
success: false,
|
|
588
|
-
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use
|
|
557
|
+
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
|
|
589
558
|
};
|
|
590
559
|
}
|
|
591
560
|
else {
|
|
@@ -636,6 +605,56 @@ export class PlanformMCPServer {
|
|
|
636
605
|
}
|
|
637
606
|
}
|
|
638
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
|
+
}
|
|
639
658
|
/**
|
|
640
659
|
* Start automatic background polling for device token (kept for backward compatibility)
|
|
641
660
|
* @deprecated Use pollUntilApproved for synchronous polling
|
|
@@ -748,17 +767,22 @@ export class PlanformMCPServer {
|
|
|
748
767
|
}
|
|
749
768
|
async handleDiagramCreate(args) {
|
|
750
769
|
this.logger.info('Diagram create requested');
|
|
751
|
-
if (!args.
|
|
752
|
-
throw new Error('
|
|
770
|
+
if (!args.title) {
|
|
771
|
+
throw new Error('title is required');
|
|
772
|
+
}
|
|
773
|
+
if (!args.type) {
|
|
774
|
+
throw new Error('type is required');
|
|
753
775
|
}
|
|
754
776
|
try {
|
|
755
|
-
|
|
756
|
-
|
|
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.');
|
|
757
780
|
}
|
|
758
|
-
const response = await this.apiClient.createDiagram(
|
|
781
|
+
const response = await this.apiClient.createDiagram(userUuid, {
|
|
759
782
|
title: args.title,
|
|
760
783
|
type: args.type,
|
|
761
784
|
});
|
|
785
|
+
this.currentDiagramUuid = response.uuid;
|
|
762
786
|
// Return the response in the format specified by the design document
|
|
763
787
|
const result = {
|
|
764
788
|
diagram_uuid: response.uuid,
|
|
@@ -784,11 +808,32 @@ export class PlanformMCPServer {
|
|
|
784
808
|
}
|
|
785
809
|
async handleDiagramGet(args) {
|
|
786
810
|
this.logger.info('Diagram get requested');
|
|
787
|
-
if (!args.diagram_uuid) {
|
|
788
|
-
throw new Error('diagram_uuid is required');
|
|
789
|
-
}
|
|
790
811
|
try {
|
|
791
|
-
|
|
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;
|
|
792
837
|
// Return the response in the format specified by the design document
|
|
793
838
|
const result = {
|
|
794
839
|
diagram: {
|
|
@@ -821,19 +866,20 @@ export class PlanformMCPServer {
|
|
|
821
866
|
}
|
|
822
867
|
async handleDiagramsList(args) {
|
|
823
868
|
this.logger.info('Diagrams list requested');
|
|
824
|
-
|
|
825
|
-
|
|
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.');
|
|
826
872
|
}
|
|
827
873
|
try {
|
|
828
|
-
const response = await this.apiClient.getUserDiagrams(
|
|
874
|
+
const response = await this.apiClient.getUserDiagrams(userUuid, args.page || 1, args.page_size || 10);
|
|
829
875
|
// Return the response in the format specified by the design document
|
|
830
876
|
const result = {
|
|
831
|
-
items: response.diagrams.map(diagram => ({
|
|
877
|
+
items: response.diagrams.map((diagram) => ({
|
|
832
878
|
diagram_uuid: diagram.uuid,
|
|
833
|
-
external_id: diagram.label,
|
|
879
|
+
external_id: diagram.label,
|
|
834
880
|
title: diagram.title,
|
|
835
881
|
type: diagram.type,
|
|
836
|
-
version: 1,
|
|
882
|
+
version: 1,
|
|
837
883
|
updated_at: diagram.updatedAt,
|
|
838
884
|
})),
|
|
839
885
|
next_page: response.pagination.page < response.pagination.totalPages
|
|
@@ -857,8 +903,12 @@ export class PlanformMCPServer {
|
|
|
857
903
|
}
|
|
858
904
|
async handleNodesCreate(args) {
|
|
859
905
|
this.logger.info('Nodes create requested');
|
|
860
|
-
if (!args.
|
|
861
|
-
throw new Error('
|
|
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.');
|
|
862
912
|
}
|
|
863
913
|
try {
|
|
864
914
|
const requestPayload = {
|
|
@@ -871,11 +921,10 @@ export class PlanformMCPServer {
|
|
|
871
921
|
group: args.group || null,
|
|
872
922
|
meta: args.meta || null,
|
|
873
923
|
};
|
|
874
|
-
// Log the request payload for debugging enum values
|
|
875
924
|
if (args.kind === 'enum') {
|
|
876
925
|
this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
|
|
877
926
|
}
|
|
878
|
-
const response = await this.apiClient.createNode(
|
|
927
|
+
const response = await this.apiClient.createNode(diagramUuid, requestPayload);
|
|
879
928
|
return {
|
|
880
929
|
content: [
|
|
881
930
|
{
|
|
@@ -896,11 +945,20 @@ export class PlanformMCPServer {
|
|
|
896
945
|
}
|
|
897
946
|
async handleNodeUpdate(args) {
|
|
898
947
|
this.logger.info('Node update requested');
|
|
899
|
-
if (!args.
|
|
900
|
-
throw new Error('
|
|
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.');
|
|
901
958
|
}
|
|
902
959
|
try {
|
|
903
|
-
const
|
|
960
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
961
|
+
const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
|
|
904
962
|
return {
|
|
905
963
|
content: [
|
|
906
964
|
{
|
|
@@ -921,11 +979,20 @@ export class PlanformMCPServer {
|
|
|
921
979
|
}
|
|
922
980
|
async handleNodeVerify(args) {
|
|
923
981
|
this.logger.info('Node verify requested');
|
|
924
|
-
if (!args.
|
|
925
|
-
throw new Error('
|
|
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.');
|
|
926
992
|
}
|
|
927
993
|
try {
|
|
928
|
-
const
|
|
994
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
995
|
+
const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
|
|
929
996
|
return {
|
|
930
997
|
content: [
|
|
931
998
|
{
|
|
@@ -943,11 +1010,17 @@ export class PlanformMCPServer {
|
|
|
943
1010
|
}
|
|
944
1011
|
async handleNodeDelete(args) {
|
|
945
1012
|
this.logger.info('Node delete requested');
|
|
946
|
-
|
|
947
|
-
|
|
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.');
|
|
948
1020
|
}
|
|
949
1021
|
try {
|
|
950
|
-
const
|
|
1022
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
1023
|
+
const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
|
|
951
1024
|
return {
|
|
952
1025
|
content: [
|
|
953
1026
|
{
|
|
@@ -965,19 +1038,24 @@ export class PlanformMCPServer {
|
|
|
965
1038
|
}
|
|
966
1039
|
async handleLinksCreate(args) {
|
|
967
1040
|
this.logger.info('Links create requested');
|
|
968
|
-
if (!args.
|
|
969
|
-
throw new Error('
|
|
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.');
|
|
970
1047
|
}
|
|
971
|
-
// Validate that "none" is not used for inheritance/implements/dependency links
|
|
972
1048
|
const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
|
|
973
1049
|
if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
|
|
974
1050
|
throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
|
|
975
1051
|
}
|
|
976
1052
|
try {
|
|
1053
|
+
const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
|
|
1054
|
+
const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
|
|
977
1055
|
const requestBody = {
|
|
978
1056
|
kind: args.kind,
|
|
979
|
-
fromNode:
|
|
980
|
-
toNode:
|
|
1057
|
+
fromNode: fromLabel,
|
|
1058
|
+
toNode: toLabel,
|
|
981
1059
|
name: args.label || null,
|
|
982
1060
|
meta: args.meta || null,
|
|
983
1061
|
fromMultiplicity: args.fromMultiplicity || null,
|
|
@@ -987,7 +1065,7 @@ export class PlanformMCPServer {
|
|
|
987
1065
|
if (args.directional !== undefined) {
|
|
988
1066
|
requestBody.directional = args.directional;
|
|
989
1067
|
}
|
|
990
|
-
const response = await this.apiClient.createLink(
|
|
1068
|
+
const response = await this.apiClient.createLink(diagramUuid, requestBody);
|
|
991
1069
|
return {
|
|
992
1070
|
content: [
|
|
993
1071
|
{
|
|
@@ -1008,11 +1086,15 @@ export class PlanformMCPServer {
|
|
|
1008
1086
|
}
|
|
1009
1087
|
async handleLinkUpdate(args) {
|
|
1010
1088
|
this.logger.info('Link update requested');
|
|
1011
|
-
if (!args.
|
|
1012
|
-
throw new Error('
|
|
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.');
|
|
1013
1095
|
}
|
|
1014
1096
|
try {
|
|
1015
|
-
const response = await this.apiClient.updateLink(
|
|
1097
|
+
const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
|
|
1016
1098
|
return {
|
|
1017
1099
|
content: [
|
|
1018
1100
|
{
|
|
@@ -1033,11 +1115,15 @@ export class PlanformMCPServer {
|
|
|
1033
1115
|
}
|
|
1034
1116
|
async handleLinkVerify(args) {
|
|
1035
1117
|
this.logger.info('Link verify requested');
|
|
1036
|
-
if (!args.
|
|
1037
|
-
throw new Error('
|
|
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.');
|
|
1038
1124
|
}
|
|
1039
1125
|
try {
|
|
1040
|
-
const response = await this.apiClient.verifyLink(
|
|
1126
|
+
const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
|
|
1041
1127
|
return {
|
|
1042
1128
|
content: [
|
|
1043
1129
|
{
|
|
@@ -1055,11 +1141,15 @@ export class PlanformMCPServer {
|
|
|
1055
1141
|
}
|
|
1056
1142
|
async handleLinkDelete(args) {
|
|
1057
1143
|
this.logger.info('Link delete requested');
|
|
1058
|
-
if (!args.
|
|
1059
|
-
throw new Error('
|
|
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.');
|
|
1060
1150
|
}
|
|
1061
1151
|
try {
|
|
1062
|
-
const response = await this.apiClient.deleteLink(
|
|
1152
|
+
const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
|
|
1063
1153
|
return {
|
|
1064
1154
|
content: [
|
|
1065
1155
|
{
|