@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/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}`);
|
|
@@ -461,6 +427,7 @@ export class PlanformMCPServer {
|
|
|
461
427
|
server: 'planform-mcp',
|
|
462
428
|
version: process.env.MCP_SERVER_VERSION || '1.0.0',
|
|
463
429
|
timestamp: new Date().toISOString(),
|
|
430
|
+
backend_base_url: process.env.BACKEND_BASE_URL || 'https://www.planform.io/api',
|
|
464
431
|
}, null, 2),
|
|
465
432
|
},
|
|
466
433
|
],
|
|
@@ -541,6 +508,9 @@ export class PlanformMCPServer {
|
|
|
541
508
|
if (pollResult.success) {
|
|
542
509
|
deviceInfo.message = '✅ Authentication successful! Token received and stored.';
|
|
543
510
|
deviceInfo.access_token_received = true;
|
|
511
|
+
if (response.session.approvedUserUuid) {
|
|
512
|
+
this.sessionUserUuid = response.session.approvedUserUuid;
|
|
513
|
+
}
|
|
544
514
|
}
|
|
545
515
|
else {
|
|
546
516
|
deviceInfo.message = `❌ Authentication failed: ${pollResult.error || 'Unknown error'}`;
|
|
@@ -584,7 +554,7 @@ export class PlanformMCPServer {
|
|
|
584
554
|
this.logger.warn(`Max wait time (${maxWaitSeconds}s) reached. Device code still valid for ${expiresIn - elapsed}s.`);
|
|
585
555
|
return {
|
|
586
556
|
success: false,
|
|
587
|
-
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use
|
|
557
|
+
error: `Max wait time (${maxWaitSeconds}s) reached. Please approve in browser and use poll_auth_token manually (optional), or try sign_in again.`
|
|
588
558
|
};
|
|
589
559
|
}
|
|
590
560
|
else {
|
|
@@ -635,6 +605,56 @@ export class PlanformMCPServer {
|
|
|
635
605
|
}
|
|
636
606
|
}
|
|
637
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Returns the current diagram data (nodes, links, etc.). Throws if no diagram is selected.
|
|
610
|
+
* Used to resolve node names to uuids/labels under the hood.
|
|
611
|
+
*/
|
|
612
|
+
async getCurrentDiagramData() {
|
|
613
|
+
const diagramUuid = this.currentDiagramUuid;
|
|
614
|
+
if (!diagramUuid) {
|
|
615
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
616
|
+
}
|
|
617
|
+
const diagram = await this.apiClient.getDiagram(diagramUuid);
|
|
618
|
+
return {
|
|
619
|
+
uuid: diagram.uuid,
|
|
620
|
+
nodes: diagram.nodes.map((n) => ({ uuid: n.uuid, label: n.label, name: n.name })),
|
|
621
|
+
links: diagram.links.map((l) => ({ uuid: l.uuid })),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Resolve node_identifier (name or UUID) to node UUID using the current diagram.
|
|
626
|
+
*/
|
|
627
|
+
async resolveNodeToUuid(diagramUuid, nodeIdentifier) {
|
|
628
|
+
if (UUID_REGEX.test(nodeIdentifier)) {
|
|
629
|
+
return nodeIdentifier;
|
|
630
|
+
}
|
|
631
|
+
const data = await this.getCurrentDiagramData();
|
|
632
|
+
if (data.uuid !== diagramUuid) {
|
|
633
|
+
throw new Error('Current diagram changed; resolve again.');
|
|
634
|
+
}
|
|
635
|
+
const byName = data.nodes.find((n) => n.name === nodeIdentifier || n.name?.toLowerCase() === String(nodeIdentifier).toLowerCase());
|
|
636
|
+
if (!byName) {
|
|
637
|
+
throw new Error(`No node named "${nodeIdentifier}" in the current diagram. Use node names or UUIDs.`);
|
|
638
|
+
}
|
|
639
|
+
return byName.uuid;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Resolve node reference (name or label like n-1) to node label for create_link.
|
|
643
|
+
*/
|
|
644
|
+
async resolveNodeToLabel(diagramUuid, nodeRef) {
|
|
645
|
+
if (NODE_LABEL_REGEX.test(nodeRef)) {
|
|
646
|
+
return nodeRef;
|
|
647
|
+
}
|
|
648
|
+
const data = await this.getCurrentDiagramData();
|
|
649
|
+
if (data.uuid !== diagramUuid) {
|
|
650
|
+
throw new Error('Current diagram changed; resolve again.');
|
|
651
|
+
}
|
|
652
|
+
const byName = data.nodes.find((n) => n.name === nodeRef || n.name?.toLowerCase() === String(nodeRef).toLowerCase());
|
|
653
|
+
if (!byName) {
|
|
654
|
+
throw new Error(`No node named "${nodeRef}" in the current diagram. Use node names (e.g. "User") or labels (e.g. n-1).`);
|
|
655
|
+
}
|
|
656
|
+
return byName.label;
|
|
657
|
+
}
|
|
638
658
|
/**
|
|
639
659
|
* Start automatic background polling for device token (kept for backward compatibility)
|
|
640
660
|
* @deprecated Use pollUntilApproved for synchronous polling
|
|
@@ -747,17 +767,22 @@ export class PlanformMCPServer {
|
|
|
747
767
|
}
|
|
748
768
|
async handleDiagramCreate(args) {
|
|
749
769
|
this.logger.info('Diagram create requested');
|
|
750
|
-
if (!args.
|
|
751
|
-
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');
|
|
752
775
|
}
|
|
753
776
|
try {
|
|
754
|
-
|
|
755
|
-
|
|
777
|
+
const userUuid = args.user_uuid ?? this.sessionUserUuid;
|
|
778
|
+
if (!userUuid) {
|
|
779
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
756
780
|
}
|
|
757
|
-
const response = await this.apiClient.createDiagram(
|
|
781
|
+
const response = await this.apiClient.createDiagram(userUuid, {
|
|
758
782
|
title: args.title,
|
|
759
783
|
type: args.type,
|
|
760
784
|
});
|
|
785
|
+
this.currentDiagramUuid = response.uuid;
|
|
761
786
|
// Return the response in the format specified by the design document
|
|
762
787
|
const result = {
|
|
763
788
|
diagram_uuid: response.uuid,
|
|
@@ -783,11 +808,32 @@ export class PlanformMCPServer {
|
|
|
783
808
|
}
|
|
784
809
|
async handleDiagramGet(args) {
|
|
785
810
|
this.logger.info('Diagram get requested');
|
|
786
|
-
if (!args.diagram_uuid) {
|
|
787
|
-
throw new Error('diagram_uuid is required');
|
|
788
|
-
}
|
|
789
811
|
try {
|
|
790
|
-
|
|
812
|
+
let diagramUuid;
|
|
813
|
+
const identifier = args.diagram_identifier ?? args.diagram_uuid;
|
|
814
|
+
if (!identifier) {
|
|
815
|
+
diagramUuid = this.currentDiagramUuid ?? '';
|
|
816
|
+
if (!diagramUuid) {
|
|
817
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram(diagram_identifier) first.');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else if (UUID_REGEX.test(identifier)) {
|
|
821
|
+
diagramUuid = identifier;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const userUuid = this.sessionUserUuid;
|
|
825
|
+
if (!userUuid) {
|
|
826
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
827
|
+
}
|
|
828
|
+
const listRes = await this.apiClient.getUserDiagrams(userUuid, 1, 100);
|
|
829
|
+
const byTitle = listRes.diagrams.find((d) => d.title === identifier || d.title?.toLowerCase() === String(identifier).toLowerCase());
|
|
830
|
+
if (!byTitle) {
|
|
831
|
+
throw new Error(`No diagram found with title "${identifier}". Use list_diagrams to see available diagrams.`);
|
|
832
|
+
}
|
|
833
|
+
diagramUuid = byTitle.uuid;
|
|
834
|
+
}
|
|
835
|
+
const response = await this.apiClient.getDiagram(diagramUuid);
|
|
836
|
+
this.currentDiagramUuid = response.uuid;
|
|
791
837
|
// Return the response in the format specified by the design document
|
|
792
838
|
const result = {
|
|
793
839
|
diagram: {
|
|
@@ -820,19 +866,20 @@ export class PlanformMCPServer {
|
|
|
820
866
|
}
|
|
821
867
|
async handleDiagramsList(args) {
|
|
822
868
|
this.logger.info('Diagrams list requested');
|
|
823
|
-
|
|
824
|
-
|
|
869
|
+
const userUuid = args.user_uuid ?? this.sessionUserUuid;
|
|
870
|
+
if (!userUuid) {
|
|
871
|
+
throw new Error('Not authenticated. Call sign_in first, then approve in the browser.');
|
|
825
872
|
}
|
|
826
873
|
try {
|
|
827
|
-
const response = await this.apiClient.getUserDiagrams(
|
|
874
|
+
const response = await this.apiClient.getUserDiagrams(userUuid, args.page || 1, args.page_size || 10);
|
|
828
875
|
// Return the response in the format specified by the design document
|
|
829
876
|
const result = {
|
|
830
|
-
items: response.diagrams.map(diagram => ({
|
|
877
|
+
items: response.diagrams.map((diagram) => ({
|
|
831
878
|
diagram_uuid: diagram.uuid,
|
|
832
|
-
external_id: diagram.label,
|
|
879
|
+
external_id: diagram.label,
|
|
833
880
|
title: diagram.title,
|
|
834
881
|
type: diagram.type,
|
|
835
|
-
version: 1,
|
|
882
|
+
version: 1,
|
|
836
883
|
updated_at: diagram.updatedAt,
|
|
837
884
|
})),
|
|
838
885
|
next_page: response.pagination.page < response.pagination.totalPages
|
|
@@ -856,8 +903,12 @@ export class PlanformMCPServer {
|
|
|
856
903
|
}
|
|
857
904
|
async handleNodesCreate(args) {
|
|
858
905
|
this.logger.info('Nodes create requested');
|
|
859
|
-
if (!args.
|
|
860
|
-
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.');
|
|
861
912
|
}
|
|
862
913
|
try {
|
|
863
914
|
const requestPayload = {
|
|
@@ -870,11 +921,10 @@ export class PlanformMCPServer {
|
|
|
870
921
|
group: args.group || null,
|
|
871
922
|
meta: args.meta || null,
|
|
872
923
|
};
|
|
873
|
-
// Log the request payload for debugging enum values
|
|
874
924
|
if (args.kind === 'enum') {
|
|
875
925
|
this.logger.info(`Creating enum node with enum_values: ${JSON.stringify(requestPayload.enum_values)}`);
|
|
876
926
|
}
|
|
877
|
-
const response = await this.apiClient.createNode(
|
|
927
|
+
const response = await this.apiClient.createNode(diagramUuid, requestPayload);
|
|
878
928
|
return {
|
|
879
929
|
content: [
|
|
880
930
|
{
|
|
@@ -895,11 +945,20 @@ export class PlanformMCPServer {
|
|
|
895
945
|
}
|
|
896
946
|
async handleNodeUpdate(args) {
|
|
897
947
|
this.logger.info('Node update requested');
|
|
898
|
-
if (!args.
|
|
899
|
-
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.');
|
|
900
958
|
}
|
|
901
959
|
try {
|
|
902
|
-
const
|
|
960
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
961
|
+
const response = await this.apiClient.updateNode(diagramUuid, nodeUuid, args.patch);
|
|
903
962
|
return {
|
|
904
963
|
content: [
|
|
905
964
|
{
|
|
@@ -920,11 +979,20 @@ export class PlanformMCPServer {
|
|
|
920
979
|
}
|
|
921
980
|
async handleNodeVerify(args) {
|
|
922
981
|
this.logger.info('Node verify requested');
|
|
923
|
-
if (!args.
|
|
924
|
-
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.');
|
|
925
992
|
}
|
|
926
993
|
try {
|
|
927
|
-
const
|
|
994
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
995
|
+
const response = await this.apiClient.verifyNode(diagramUuid, nodeUuid, args.structural_fields);
|
|
928
996
|
return {
|
|
929
997
|
content: [
|
|
930
998
|
{
|
|
@@ -942,11 +1010,17 @@ export class PlanformMCPServer {
|
|
|
942
1010
|
}
|
|
943
1011
|
async handleNodeDelete(args) {
|
|
944
1012
|
this.logger.info('Node delete requested');
|
|
945
|
-
|
|
946
|
-
|
|
1013
|
+
const nodeIdentifier = args.node_identifier ?? args.node_id;
|
|
1014
|
+
if (!nodeIdentifier) {
|
|
1015
|
+
throw new Error('node_identifier is required (use the node name, e.g. "User", or its UUID).');
|
|
1016
|
+
}
|
|
1017
|
+
const diagramUuid = args.diagram_uuid ?? this.currentDiagramUuid;
|
|
1018
|
+
if (!diagramUuid) {
|
|
1019
|
+
throw new Error('No diagram selected. Create one with create_diagram or open one with open_diagram first.');
|
|
947
1020
|
}
|
|
948
1021
|
try {
|
|
949
|
-
const
|
|
1022
|
+
const nodeUuid = await this.resolveNodeToUuid(diagramUuid, nodeIdentifier);
|
|
1023
|
+
const response = await this.apiClient.deleteNode(diagramUuid, nodeUuid);
|
|
950
1024
|
return {
|
|
951
1025
|
content: [
|
|
952
1026
|
{
|
|
@@ -964,19 +1038,24 @@ export class PlanformMCPServer {
|
|
|
964
1038
|
}
|
|
965
1039
|
async handleLinksCreate(args) {
|
|
966
1040
|
this.logger.info('Links create requested');
|
|
967
|
-
if (!args.
|
|
968
|
-
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.');
|
|
969
1047
|
}
|
|
970
|
-
// Validate that "none" is not used for inheritance/implements/dependency links
|
|
971
1048
|
const directionalRequiredKinds = ['inheritance', 'implements', 'dependency'];
|
|
972
1049
|
if (args.directional === 'none' && directionalRequiredKinds.includes(args.kind)) {
|
|
973
1050
|
throw new Error(`Link kind "${args.kind}" requires directional to be "unidirectional" or "bidirectional". "none" is not allowed.`);
|
|
974
1051
|
}
|
|
975
1052
|
try {
|
|
1053
|
+
const fromLabel = await this.resolveNodeToLabel(diagramUuid, args.from);
|
|
1054
|
+
const toLabel = await this.resolveNodeToLabel(diagramUuid, args.to);
|
|
976
1055
|
const requestBody = {
|
|
977
1056
|
kind: args.kind,
|
|
978
|
-
fromNode:
|
|
979
|
-
toNode:
|
|
1057
|
+
fromNode: fromLabel,
|
|
1058
|
+
toNode: toLabel,
|
|
980
1059
|
name: args.label || null,
|
|
981
1060
|
meta: args.meta || null,
|
|
982
1061
|
fromMultiplicity: args.fromMultiplicity || null,
|
|
@@ -986,7 +1065,7 @@ export class PlanformMCPServer {
|
|
|
986
1065
|
if (args.directional !== undefined) {
|
|
987
1066
|
requestBody.directional = args.directional;
|
|
988
1067
|
}
|
|
989
|
-
const response = await this.apiClient.createLink(
|
|
1068
|
+
const response = await this.apiClient.createLink(diagramUuid, requestBody);
|
|
990
1069
|
return {
|
|
991
1070
|
content: [
|
|
992
1071
|
{
|
|
@@ -1007,11 +1086,15 @@ export class PlanformMCPServer {
|
|
|
1007
1086
|
}
|
|
1008
1087
|
async handleLinkUpdate(args) {
|
|
1009
1088
|
this.logger.info('Link update requested');
|
|
1010
|
-
if (!args.
|
|
1011
|
-
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.');
|
|
1012
1095
|
}
|
|
1013
1096
|
try {
|
|
1014
|
-
const response = await this.apiClient.updateLink(
|
|
1097
|
+
const response = await this.apiClient.updateLink(diagramUuid, args.link_id, args.patch);
|
|
1015
1098
|
return {
|
|
1016
1099
|
content: [
|
|
1017
1100
|
{
|
|
@@ -1032,11 +1115,15 @@ export class PlanformMCPServer {
|
|
|
1032
1115
|
}
|
|
1033
1116
|
async handleLinkVerify(args) {
|
|
1034
1117
|
this.logger.info('Link verify requested');
|
|
1035
|
-
if (!args.
|
|
1036
|
-
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.');
|
|
1037
1124
|
}
|
|
1038
1125
|
try {
|
|
1039
|
-
const response = await this.apiClient.verifyLink(
|
|
1126
|
+
const response = await this.apiClient.verifyLink(diagramUuid, args.link_id, args.structural_fields);
|
|
1040
1127
|
return {
|
|
1041
1128
|
content: [
|
|
1042
1129
|
{
|
|
@@ -1054,11 +1141,15 @@ export class PlanformMCPServer {
|
|
|
1054
1141
|
}
|
|
1055
1142
|
async handleLinkDelete(args) {
|
|
1056
1143
|
this.logger.info('Link delete requested');
|
|
1057
|
-
if (!args.
|
|
1058
|
-
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.');
|
|
1059
1150
|
}
|
|
1060
1151
|
try {
|
|
1061
|
-
const response = await this.apiClient.deleteLink(
|
|
1152
|
+
const response = await this.apiClient.deleteLink(diagramUuid, args.link_id);
|
|
1062
1153
|
return {
|
|
1063
1154
|
content: [
|
|
1064
1155
|
{
|