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