@enfyra/mcp-server 0.0.98 → 0.0.99
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/README.md +1 -1
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +10 -15
- package/src/lib/mcp-instructions.js +1 -1
- package/src/lib/platform-operation-tools.js +478 -180
- package/src/mcp-server-entry.mjs +0 -63
package/README.md
CHANGED
|
@@ -189,7 +189,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
189
189
|
- Relation tools reject physical FK/junction names.
|
|
190
190
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
191
191
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
|
192
|
-
- Platform operation tools such as `create_api_endpoint`, `
|
|
192
|
+
- Platform operation tools such as `create_api_endpoint`, `publish_route_methods`, `add_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `ensure_script_flow_step`, `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
193
193
|
- Schema changes are serialized.
|
|
194
194
|
- Destructive deletes return a preview before requiring `confirm=true`.
|
|
195
195
|
|
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -996,13 +996,12 @@ ensure_route_access({
|
|
|
996
996
|
},
|
|
997
997
|
{
|
|
998
998
|
name: 'Publish read-only route',
|
|
999
|
-
code: `
|
|
999
|
+
code: `publish_route_methods({
|
|
1000
1000
|
path: "/articles",
|
|
1001
|
-
methods: ["GET"]
|
|
1002
|
-
mode: "merge"
|
|
1001
|
+
methods: ["GET"]
|
|
1003
1002
|
})`,
|
|
1004
1003
|
notes: [
|
|
1005
|
-
'Use
|
|
1004
|
+
'Use publish_route_methods instead of raw enfyra_route updates; the tool resolves method ids and validates that GET is available.',
|
|
1006
1005
|
'publicMethods controls anonymous route access. Route permissions are not for public access.',
|
|
1007
1006
|
'Route permissions apply when the method is not public.',
|
|
1008
1007
|
],
|
|
@@ -1417,7 +1416,7 @@ update_method({
|
|
|
1417
1416
|
},
|
|
1418
1417
|
{
|
|
1419
1418
|
name: 'Create menu then extension',
|
|
1420
|
-
code: `
|
|
1419
|
+
code: `ensure_menu({
|
|
1421
1420
|
label: "Reports",
|
|
1422
1421
|
type: "Menu",
|
|
1423
1422
|
path: "/reports",
|
|
@@ -1433,8 +1432,7 @@ update_method({
|
|
|
1433
1432
|
})
|
|
1434
1433
|
|
|
1435
1434
|
// Read the created menu id from the tool response, then:
|
|
1436
|
-
|
|
1437
|
-
type: "page",
|
|
1435
|
+
ensure_page_extension({
|
|
1438
1436
|
name: "ReportsPage",
|
|
1439
1437
|
description: "Reports dashboard",
|
|
1440
1438
|
menuId: "<created-menu-id>",
|
|
@@ -1445,7 +1443,7 @@ create_extension({
|
|
|
1445
1443
|
'Menu provides navigation; extension provides content.',
|
|
1446
1444
|
'Use enfyra_menu.label, not title.',
|
|
1447
1445
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1448
|
-
'For page extensions, create the menu first and pass
|
|
1446
|
+
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1449
1447
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1450
1448
|
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1451
1449
|
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
@@ -1488,8 +1486,7 @@ function openLatest() {
|
|
|
1488
1486
|
</script>
|
|
1489
1487
|
\`
|
|
1490
1488
|
|
|
1491
|
-
|
|
1492
|
-
type: "widget",
|
|
1489
|
+
ensure_widget_extension({
|
|
1493
1490
|
name: "ReportStatusWidget",
|
|
1494
1491
|
description: "Report status summary cards",
|
|
1495
1492
|
code: reportStatusWidgetCode,
|
|
@@ -1497,8 +1494,7 @@ create_extension({
|
|
|
1497
1494
|
})
|
|
1498
1495
|
|
|
1499
1496
|
// Read the created widget record id, then embed it from the page extension.
|
|
1500
|
-
|
|
1501
|
-
type: "page",
|
|
1497
|
+
ensure_page_extension({
|
|
1502
1498
|
name: "ReportsPage",
|
|
1503
1499
|
menuId: "<reports-menu-id>",
|
|
1504
1500
|
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
|
|
@@ -1574,8 +1570,7 @@ onUnmounted(() => {
|
|
|
1574
1570
|
</script>
|
|
1575
1571
|
\`
|
|
1576
1572
|
|
|
1577
|
-
|
|
1578
|
-
type: "global",
|
|
1573
|
+
ensure_global_extension({
|
|
1579
1574
|
name: "NotificationBellGlobal",
|
|
1580
1575
|
description: "Registers the app-wide notification bell in the account panel",
|
|
1581
1576
|
code: notificationBellCode,
|
|
@@ -1719,7 +1714,7 @@ registerHeaderActions([
|
|
|
1719
1714
|
{
|
|
1720
1715
|
name: 'Plan an admin dashboard as multiple pages',
|
|
1721
1716
|
code: `// Recommended menu shape for an operations surface:
|
|
1722
|
-
|
|
1717
|
+
ensure_menu({
|
|
1723
1718
|
type: "Dropdown Menu",
|
|
1724
1719
|
label: "Operations",
|
|
1725
1720
|
path: "/operations",
|
|
@@ -29,7 +29,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
29
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
30
|
'- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category.',
|
|
31
31
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
|
-
'- Prefer business operation
|
|
32
|
+
'- Prefer the most specific business operation tool over raw metadata CRUD: `create_api_endpoint`; route tools such as `add_route_methods`, `publish_route_methods`, and `unpublish_route_methods`; `set_table_graphql`; `ensure_guard`; permission/rule tools; websocket tools; flow tools such as `ensure_manual_flow`, `ensure_scheduled_flow`, and fixed-type flow step tools; and extension tools such as `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension`.',
|
|
33
33
|
'- Before saving standalone dynamic script or extension code, call `validate_dynamic_script` or `validate_extension_code` unless the chosen ensure/update tool already validates the code.',
|
|
34
34
|
'- For existing script-backed records, use `trace_metadata_usage` then `get_script_source`; edit with `patch_script_source` or `update_script_source` so source is hash-checked and validated.',
|
|
35
35
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
@@ -104,6 +104,66 @@ async function resolveRoute(apiUrl, { path, routeId }) {
|
|
|
104
104
|
return { route, routes, path: route.path };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
async function updateRouteMethods(apiUrl, { path, routeId, methods, mode, isEnabled }) {
|
|
108
|
+
const [{ route }, { methodMap, methodIdNameMap }] = await Promise.all([
|
|
109
|
+
resolveRoute(apiUrl, { path, routeId }),
|
|
110
|
+
getMethodContext(apiUrl),
|
|
111
|
+
]);
|
|
112
|
+
const existingAvailable = methodNamesFromRecords(route.availableMethods, methodIdNameMap);
|
|
113
|
+
const existingPublic = methodNamesFromRecords(route.publicMethods, methodIdNameMap);
|
|
114
|
+
const finalAvailable = mergeMethods(existingAvailable, methods, mode);
|
|
115
|
+
const finalPublic = existingPublic.filter((method) => finalAvailable.includes(method));
|
|
116
|
+
const body = {
|
|
117
|
+
availableMethods: resolveMethodRefs(methodMap, finalAvailable),
|
|
118
|
+
publicMethods: resolveMethodRefs(methodMap, finalPublic),
|
|
119
|
+
};
|
|
120
|
+
if (isEnabled !== undefined) body.isEnabled = isEnabled;
|
|
121
|
+
|
|
122
|
+
const result = await fetchAPI(apiUrl, `/enfyra_route/${encodeURIComponent(String(getId(route)))}`, {
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
body: JSON.stringify(body),
|
|
125
|
+
});
|
|
126
|
+
const routeReload = await reloadRoutes(apiUrl);
|
|
127
|
+
return {
|
|
128
|
+
action: 'route_methods_updated',
|
|
129
|
+
route: { id: getId(route), path: route.path },
|
|
130
|
+
before: { availableMethods: existingAvailable, publicMethods: existingPublic },
|
|
131
|
+
after: { availableMethods: finalAvailable, publicMethods: finalPublic },
|
|
132
|
+
result,
|
|
133
|
+
routeReload,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function updateRoutePublicMethods(apiUrl, { path, routeId, methods, mode }) {
|
|
138
|
+
const [{ route }, { methodMap, methodIdNameMap }] = await Promise.all([
|
|
139
|
+
resolveRoute(apiUrl, { path, routeId }),
|
|
140
|
+
getMethodContext(apiUrl),
|
|
141
|
+
]);
|
|
142
|
+
const availableMethods = methodNamesFromRecords(route.availableMethods, methodIdNameMap);
|
|
143
|
+
const existingPublic = methodNamesFromRecords(route.publicMethods, methodIdNameMap);
|
|
144
|
+
const requestedMethods = uniqueMethodNames(methods);
|
|
145
|
+
const unavailable = requestedMethods.filter((method) => !availableMethods.includes(method));
|
|
146
|
+
if (unavailable.length > 0) {
|
|
147
|
+
throw new Error(`Cannot make unavailable route method(s) public: ${unavailable.join(', ')}. First call add_route_methods to add them to availableMethods.`);
|
|
148
|
+
}
|
|
149
|
+
const finalPublic = mergeMethods(existingPublic, requestedMethods, mode);
|
|
150
|
+
const result = await fetchAPI(apiUrl, `/enfyra_route/${encodeURIComponent(String(getId(route)))}`, {
|
|
151
|
+
method: 'PATCH',
|
|
152
|
+
body: JSON.stringify({ publicMethods: resolveMethodRefs(methodMap, finalPublic) }),
|
|
153
|
+
});
|
|
154
|
+
const routeReload = await reloadRoutes(apiUrl);
|
|
155
|
+
return {
|
|
156
|
+
action: 'route_public_methods_updated',
|
|
157
|
+
route: { id: getId(route), path: route.path },
|
|
158
|
+
availableMethods,
|
|
159
|
+
publicMethodsBefore: existingPublic,
|
|
160
|
+
publicMethodsAfter: finalPublic,
|
|
161
|
+
publicAccess: finalPublic.length > 0 ? 'Methods listed in publicMethods bypass auth/RoleGuard.' : 'No public methods remain on this route.',
|
|
162
|
+
result,
|
|
163
|
+
routeReload,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
107
167
|
async function findHandler(apiUrl, routeId, methodId) {
|
|
108
168
|
const filter = encodeURIComponent(JSON.stringify({
|
|
109
169
|
route: { id: { _eq: routeId } },
|
|
@@ -249,11 +309,10 @@ function normalizeFlowStepBody(step, flowId) {
|
|
|
249
309
|
const body = {
|
|
250
310
|
key: step.key,
|
|
251
311
|
type: step.type,
|
|
252
|
-
|
|
312
|
+
stepOrder: step.order ?? 0,
|
|
253
313
|
config: step.config ?? {},
|
|
254
314
|
timeout: step.timeout,
|
|
255
315
|
isEnabled: step.isEnabled ?? true,
|
|
256
|
-
description: step.description,
|
|
257
316
|
flow: { id: flowId },
|
|
258
317
|
};
|
|
259
318
|
if (step.sourceCode !== undefined) body.sourceCode = step.sourceCode;
|
|
@@ -261,6 +320,138 @@ function normalizeFlowStepBody(step, flowId) {
|
|
|
261
320
|
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
|
|
262
321
|
}
|
|
263
322
|
|
|
323
|
+
async function ensureMenu(apiUrl, {
|
|
324
|
+
label,
|
|
325
|
+
path,
|
|
326
|
+
icon,
|
|
327
|
+
type = 'Menu',
|
|
328
|
+
order = 0,
|
|
329
|
+
permission,
|
|
330
|
+
description,
|
|
331
|
+
isEnabled = true,
|
|
332
|
+
}) {
|
|
333
|
+
const normalizedPath = path ? normalizeRestPath(path) : undefined;
|
|
334
|
+
const existing = normalizedPath
|
|
335
|
+
? await findRecord(apiUrl, 'enfyra_menu', { path: { _eq: normalizedPath } }, 'id,_id,path,label')
|
|
336
|
+
: await findRecord(apiUrl, 'enfyra_menu', { label: { _eq: label } }, 'id,_id,path,label');
|
|
337
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_menu', existing, {
|
|
338
|
+
label,
|
|
339
|
+
...(normalizedPath ? { path: normalizedPath } : {}),
|
|
340
|
+
icon,
|
|
341
|
+
type,
|
|
342
|
+
order,
|
|
343
|
+
permission: parseJsonObjectArg('permission', permission, undefined),
|
|
344
|
+
description,
|
|
345
|
+
isEnabled,
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
id: operation.id || getId(existing),
|
|
349
|
+
path: normalizedPath || existing?.path || null,
|
|
350
|
+
label,
|
|
351
|
+
action: operation.action,
|
|
352
|
+
operation,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function ensureExtension(apiUrl, {
|
|
357
|
+
name,
|
|
358
|
+
type,
|
|
359
|
+
code,
|
|
360
|
+
menuId,
|
|
361
|
+
description,
|
|
362
|
+
isEnabled = true,
|
|
363
|
+
version = '1.0.0',
|
|
364
|
+
}) {
|
|
365
|
+
if (type === 'page' && !menuId) {
|
|
366
|
+
throw new Error('menuId is required for page extensions. Use ensure_menu first, then ensure_page_extension.');
|
|
367
|
+
}
|
|
368
|
+
if (type !== 'page' && menuId) {
|
|
369
|
+
throw new Error('menuId is only valid for page extensions.');
|
|
370
|
+
}
|
|
371
|
+
const validation = await validateExtensionCode(apiUrl, code, name);
|
|
372
|
+
const existing = await findRecord(apiUrl, 'enfyra_extension', { name: { _eq: name } }, 'id,_id,name,menu.id,type');
|
|
373
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_extension', existing, {
|
|
374
|
+
name,
|
|
375
|
+
type,
|
|
376
|
+
code,
|
|
377
|
+
...(menuId ? { menu: { id: menuId } } : {}),
|
|
378
|
+
description,
|
|
379
|
+
isEnabled,
|
|
380
|
+
version,
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
id: operation.id || getId(existing),
|
|
384
|
+
name,
|
|
385
|
+
type,
|
|
386
|
+
action: operation.action,
|
|
387
|
+
operation,
|
|
388
|
+
validation,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function ensureFlow(apiUrl, {
|
|
393
|
+
name,
|
|
394
|
+
triggerType = 'manual',
|
|
395
|
+
triggerConfig,
|
|
396
|
+
timeout,
|
|
397
|
+
maxExecutions = 100,
|
|
398
|
+
isEnabled = true,
|
|
399
|
+
description,
|
|
400
|
+
}) {
|
|
401
|
+
const existing = await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: name } }, 'id,_id,name');
|
|
402
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_flow', existing, {
|
|
403
|
+
name,
|
|
404
|
+
triggerType,
|
|
405
|
+
triggerConfig: parseJsonObjectArg('triggerConfig', triggerConfig, {}),
|
|
406
|
+
timeout,
|
|
407
|
+
maxExecutions,
|
|
408
|
+
isEnabled,
|
|
409
|
+
description,
|
|
410
|
+
});
|
|
411
|
+
const reload = await reloadBestEffort(apiUrl, '/admin/reload/flows');
|
|
412
|
+
return { action: 'flow_ensured', flow: { id: operation.id, name }, operation, reload };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function ensureFlowStep(apiUrl, {
|
|
416
|
+
flowName,
|
|
417
|
+
flowId,
|
|
418
|
+
key,
|
|
419
|
+
type,
|
|
420
|
+
order,
|
|
421
|
+
config,
|
|
422
|
+
sourceCode,
|
|
423
|
+
scriptLanguage,
|
|
424
|
+
timeout,
|
|
425
|
+
isEnabled,
|
|
426
|
+
}) {
|
|
427
|
+
if (!flowName && !flowId) throw new Error('Provide flowName or flowId.');
|
|
428
|
+
if (flowName && flowId) throw new Error('Provide flowName or flowId, not both.');
|
|
429
|
+
const flow = flowId
|
|
430
|
+
? await findRecord(apiUrl, 'enfyra_flow', { id: { _eq: flowId } }, 'id,_id,name')
|
|
431
|
+
: await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: flowName } }, 'id,_id,name');
|
|
432
|
+
if (!flow) throw new Error(`Flow not found: ${flowId || flowName}`);
|
|
433
|
+
const parsedConfig = parseJsonObjectArg('config', config, {});
|
|
434
|
+
const validation = sourceCode && ['script', 'condition'].includes(type)
|
|
435
|
+
? await validateDynamicScript(apiUrl, sourceCode, scriptLanguage)
|
|
436
|
+
: { validated: false, reason: 'no script validation required' };
|
|
437
|
+
const existing = await findRecord(apiUrl, 'enfyra_flow_step', {
|
|
438
|
+
flow: { id: { _eq: getId(flow) } },
|
|
439
|
+
key: { _eq: key },
|
|
440
|
+
}, 'id,_id,key,flow.id');
|
|
441
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_flow_step', existing, normalizeFlowStepBody({
|
|
442
|
+
key,
|
|
443
|
+
type,
|
|
444
|
+
order,
|
|
445
|
+
config: parsedConfig,
|
|
446
|
+
sourceCode,
|
|
447
|
+
scriptLanguage,
|
|
448
|
+
timeout,
|
|
449
|
+
isEnabled,
|
|
450
|
+
}, getId(flow)));
|
|
451
|
+
const reload = await reloadBestEffort(apiUrl, '/admin/reload/flows');
|
|
452
|
+
return { action: 'flow_step_ensured', flow: { id: getId(flow), name: flow.name }, step: { id: operation.id, key, type }, validation, operation, reload };
|
|
453
|
+
}
|
|
454
|
+
|
|
264
455
|
export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
265
456
|
server.tool(
|
|
266
457
|
'validate_dynamic_script',
|
|
@@ -321,102 +512,105 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
321
512
|
);
|
|
322
513
|
|
|
323
514
|
server.tool(
|
|
324
|
-
'
|
|
325
|
-
|
|
326
|
-
'Business operation: set which HTTP methods a route supports.',
|
|
327
|
-
'Use this instead of raw enfyra_route CRUD when adding/removing availableMethods on an existing route.',
|
|
328
|
-
'It resolves method ids, preserves route metadata, patches availableMethods, and reloads routes.',
|
|
329
|
-
].join(' '),
|
|
515
|
+
'add_route_methods',
|
|
516
|
+
'Business operation: add HTTP methods to an existing route.',
|
|
330
517
|
{
|
|
331
518
|
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
332
519
|
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
333
|
-
methods: z.array(z.string()).min(1).describe('HTTP method names to
|
|
334
|
-
mode: z.enum(['merge', 'replace', 'remove']).optional().default('merge').describe('merge adds methods; replace sets exactly these methods; remove deletes these methods.'),
|
|
520
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to add.'),
|
|
335
521
|
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
336
522
|
},
|
|
337
|
-
async ({ path, routeId, methods,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const finalPublic = existingPublic.filter((method) => finalAvailable.includes(method));
|
|
346
|
-
const body = {
|
|
347
|
-
availableMethods: resolveMethodRefs(methodMap, finalAvailable),
|
|
348
|
-
publicMethods: resolveMethodRefs(methodMap, finalPublic),
|
|
349
|
-
};
|
|
350
|
-
if (isEnabled !== undefined) body.isEnabled = isEnabled;
|
|
523
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
524
|
+
path,
|
|
525
|
+
routeId,
|
|
526
|
+
methods,
|
|
527
|
+
mode: 'merge',
|
|
528
|
+
isEnabled,
|
|
529
|
+
})),
|
|
530
|
+
);
|
|
351
531
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
text: JSON.stringify({
|
|
361
|
-
action: 'route_methods_updated',
|
|
362
|
-
route: { id: getId(route), path: route.path },
|
|
363
|
-
before: { availableMethods: existingAvailable, publicMethods: existingPublic },
|
|
364
|
-
after: { availableMethods: finalAvailable, publicMethods: finalPublic },
|
|
365
|
-
result,
|
|
366
|
-
routeReload,
|
|
367
|
-
}, null, 2),
|
|
368
|
-
}],
|
|
369
|
-
};
|
|
532
|
+
server.tool(
|
|
533
|
+
'replace_route_methods',
|
|
534
|
+
'Business operation: replace an existing route availableMethods list exactly.',
|
|
535
|
+
{
|
|
536
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
537
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
538
|
+
methods: z.array(z.string()).min(1).describe('Exact HTTP method names for availableMethods.'),
|
|
539
|
+
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
370
540
|
},
|
|
541
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
542
|
+
path,
|
|
543
|
+
routeId,
|
|
544
|
+
methods,
|
|
545
|
+
mode: 'replace',
|
|
546
|
+
isEnabled,
|
|
547
|
+
})),
|
|
371
548
|
);
|
|
372
549
|
|
|
373
550
|
server.tool(
|
|
374
|
-
'
|
|
375
|
-
|
|
376
|
-
'Business operation: publish or unpublish REST methods on a route.',
|
|
377
|
-
'Use this instead of raw enfyra_route CRUD when the user says a route/method should be public, anonymous, private, or not require login.',
|
|
378
|
-
'The tool only touches publicMethods, validates that requested methods are already available on the route, and reloads routes.',
|
|
379
|
-
].join(' '),
|
|
551
|
+
'remove_route_methods',
|
|
552
|
+
'Business operation: remove HTTP methods from an existing route.',
|
|
380
553
|
{
|
|
381
554
|
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
382
555
|
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
383
|
-
methods: z.array(z.string()).min(1).describe('HTTP method names to
|
|
384
|
-
|
|
556
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to remove.'),
|
|
557
|
+
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
385
558
|
},
|
|
386
|
-
async ({ path, routeId, methods,
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
const routeReload = await reloadRoutes(ENFYRA_API_URL);
|
|
404
|
-
return {
|
|
405
|
-
content: [{
|
|
406
|
-
type: 'text',
|
|
407
|
-
text: JSON.stringify({
|
|
408
|
-
action: 'route_public_methods_updated',
|
|
409
|
-
route: { id: getId(route), path: route.path },
|
|
410
|
-
availableMethods,
|
|
411
|
-
publicMethodsBefore: existingPublic,
|
|
412
|
-
publicMethodsAfter: finalPublic,
|
|
413
|
-
publicAccess: finalPublic.length > 0 ? 'Methods listed in publicMethods bypass auth/RoleGuard.' : 'No public methods remain on this route.',
|
|
414
|
-
result,
|
|
415
|
-
routeReload,
|
|
416
|
-
}, null, 2),
|
|
417
|
-
}],
|
|
418
|
-
};
|
|
559
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
560
|
+
path,
|
|
561
|
+
routeId,
|
|
562
|
+
methods,
|
|
563
|
+
mode: 'remove',
|
|
564
|
+
isEnabled,
|
|
565
|
+
})),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
server.tool(
|
|
569
|
+
'publish_route_methods',
|
|
570
|
+
'Business operation: make existing route methods public/anonymous.',
|
|
571
|
+
{
|
|
572
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
573
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
574
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to publish. They must already be available on the route.'),
|
|
419
575
|
},
|
|
576
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
577
|
+
path,
|
|
578
|
+
routeId,
|
|
579
|
+
methods,
|
|
580
|
+
mode: 'merge',
|
|
581
|
+
})),
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
server.tool(
|
|
585
|
+
'replace_public_route_methods',
|
|
586
|
+
'Business operation: replace a route publicMethods list exactly.',
|
|
587
|
+
{
|
|
588
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
589
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
590
|
+
methods: z.array(z.string()).describe('Exact HTTP method names that should be public. Use an empty array to make all methods private.'),
|
|
591
|
+
},
|
|
592
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
593
|
+
path,
|
|
594
|
+
routeId,
|
|
595
|
+
methods,
|
|
596
|
+
mode: 'replace',
|
|
597
|
+
})),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
server.tool(
|
|
601
|
+
'unpublish_route_methods',
|
|
602
|
+
'Business operation: make specific public route methods private again.',
|
|
603
|
+
{
|
|
604
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
605
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
606
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to remove from publicMethods.'),
|
|
607
|
+
},
|
|
608
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
609
|
+
path,
|
|
610
|
+
routeId,
|
|
611
|
+
methods,
|
|
612
|
+
mode: 'remove',
|
|
613
|
+
})),
|
|
420
614
|
);
|
|
421
615
|
|
|
422
616
|
server.tool(
|
|
@@ -739,25 +933,25 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
739
933
|
|
|
740
934
|
server.tool(
|
|
741
935
|
'ensure_websocket_gateway',
|
|
742
|
-
'Business operation: create or update an Enfyra Socket.IO gateway. Connection handler
|
|
936
|
+
'Business operation: create or update an Enfyra Socket.IO gateway. Connection handler sourceCode is validated before save.',
|
|
743
937
|
{
|
|
744
938
|
path: z.string().describe('Gateway namespace/path, e.g. /chat.'),
|
|
745
|
-
|
|
939
|
+
sourceCode: z.string().optional().describe('Optional connection handler dynamic script sourceCode.'),
|
|
746
940
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for connection handler.'),
|
|
747
941
|
isEnabled: z.boolean().optional().default(true).describe('Enable gateway.'),
|
|
748
942
|
description: z.string().optional().describe('Admin note.'),
|
|
749
943
|
},
|
|
750
|
-
async ({ path,
|
|
944
|
+
async ({ path, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
751
945
|
const normalizedPath = normalizeRestPath(path);
|
|
752
|
-
const validation =
|
|
753
|
-
? { validated: false, reason: 'no
|
|
754
|
-
: await validateDynamicScript(ENFYRA_API_URL,
|
|
946
|
+
const validation = sourceCode === undefined
|
|
947
|
+
? { validated: false, reason: 'no sourceCode' }
|
|
948
|
+
: await validateDynamicScript(ENFYRA_API_URL, sourceCode, scriptLanguage);
|
|
755
949
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { path: { _eq: normalizedPath } }, 'id,_id,path');
|
|
756
950
|
const body = {
|
|
757
951
|
path: normalizedPath,
|
|
758
952
|
isEnabled,
|
|
759
953
|
description,
|
|
760
|
-
...(
|
|
954
|
+
...(sourceCode !== undefined ? { sourceCode, scriptLanguage } : {}),
|
|
761
955
|
};
|
|
762
956
|
const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_websocket', existing, body);
|
|
763
957
|
const reload = await reloadBestEffort(ENFYRA_API_URL, '/admin/reload/websockets');
|
|
@@ -767,24 +961,24 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
767
961
|
|
|
768
962
|
server.tool(
|
|
769
963
|
'ensure_websocket_event',
|
|
770
|
-
'Business operation: create or update one websocket event handler. It resolves gateway path/id and validates
|
|
964
|
+
'Business operation: create or update one websocket event handler. It resolves gateway path/id and validates sourceCode before save.',
|
|
771
965
|
{
|
|
772
966
|
gatewayPath: z.string().optional().describe('Gateway path, e.g. /chat. Use gatewayPath or gatewayId.'),
|
|
773
967
|
gatewayId: z.union([z.string(), z.number()]).optional().describe('Gateway id. Use gatewayPath or gatewayId.'),
|
|
774
968
|
eventName: z.string().describe('Socket event name.'),
|
|
775
|
-
|
|
969
|
+
sourceCode: z.string().describe('Event handler dynamic script sourceCode.'),
|
|
776
970
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
777
971
|
isEnabled: z.boolean().optional().default(true).describe('Enable event.'),
|
|
778
972
|
description: z.string().optional().describe('Admin note.'),
|
|
779
973
|
},
|
|
780
|
-
async ({ gatewayPath, gatewayId, eventName,
|
|
974
|
+
async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
781
975
|
if (!gatewayPath && !gatewayId) throw new Error('Provide gatewayPath or gatewayId.');
|
|
782
976
|
if (gatewayPath && gatewayId) throw new Error('Provide gatewayPath or gatewayId, not both.');
|
|
783
977
|
const gateway = gatewayId
|
|
784
978
|
? await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { id: { _eq: gatewayId } }, 'id,_id,path')
|
|
785
979
|
: await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { path: { _eq: normalizeRestPath(gatewayPath) } }, 'id,_id,path');
|
|
786
980
|
if (!gateway) throw new Error(`Websocket gateway not found: ${gatewayId || gatewayPath}`);
|
|
787
|
-
const validation = await validateDynamicScript(ENFYRA_API_URL,
|
|
981
|
+
const validation = await validateDynamicScript(ENFYRA_API_URL, sourceCode, scriptLanguage);
|
|
788
982
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_websocket_event', {
|
|
789
983
|
gateway: { id: { _eq: getId(gateway) } },
|
|
790
984
|
eventName: { _eq: eventName },
|
|
@@ -792,7 +986,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
792
986
|
const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_websocket_event', existing, {
|
|
793
987
|
gateway: { id: getId(gateway) },
|
|
794
988
|
eventName,
|
|
795
|
-
|
|
989
|
+
sourceCode,
|
|
796
990
|
scriptLanguage,
|
|
797
991
|
isEnabled,
|
|
798
992
|
description,
|
|
@@ -803,121 +997,225 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
803
997
|
);
|
|
804
998
|
|
|
805
999
|
server.tool(
|
|
806
|
-
'
|
|
807
|
-
'Business operation: create or update
|
|
1000
|
+
'ensure_manual_flow',
|
|
1001
|
+
'Business operation: create or update a manually triggered Enfyra flow. Use this when the flow is run by API, admin action, another flow, or hook.',
|
|
808
1002
|
{
|
|
809
1003
|
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
810
|
-
trigger: z.string().optional().describe('Flow trigger type/key.'),
|
|
811
1004
|
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|
|
812
1005
|
maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
|
|
813
1006
|
isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
|
|
814
1007
|
description: z.string().optional().describe('Admin note.'),
|
|
815
1008
|
},
|
|
816
|
-
async ({ name,
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1009
|
+
async ({ name, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
|
|
1010
|
+
name,
|
|
1011
|
+
triggerType: 'manual',
|
|
1012
|
+
timeout,
|
|
1013
|
+
maxExecutions,
|
|
1014
|
+
isEnabled,
|
|
1015
|
+
description,
|
|
1016
|
+
})),
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
server.tool(
|
|
1020
|
+
'ensure_scheduled_flow',
|
|
1021
|
+
'Business operation: create or update a scheduled Enfyra flow. Use this only for cron/time-based flows.',
|
|
1022
|
+
{
|
|
1023
|
+
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
1024
|
+
triggerConfig: z.string().describe('Schedule config JSON object.'),
|
|
1025
|
+
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|
|
1026
|
+
maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
|
|
1027
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
|
|
1028
|
+
description: z.string().optional().describe('Admin note.'),
|
|
828
1029
|
},
|
|
1030
|
+
async ({ name, triggerConfig, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
|
|
1031
|
+
name,
|
|
1032
|
+
triggerType: 'schedule',
|
|
1033
|
+
triggerConfig,
|
|
1034
|
+
timeout,
|
|
1035
|
+
maxExecutions,
|
|
1036
|
+
isEnabled,
|
|
1037
|
+
description,
|
|
1038
|
+
})),
|
|
829
1039
|
);
|
|
830
1040
|
|
|
831
1041
|
server.tool(
|
|
832
|
-
'
|
|
833
|
-
'Business operation: create or update one flow step
|
|
1042
|
+
'ensure_script_flow_step',
|
|
1043
|
+
'Business operation: create or update one script flow step. Use this for JavaScript/TypeScript flow logic instead of choosing type=script manually.',
|
|
834
1044
|
{
|
|
835
1045
|
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
836
1046
|
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
837
1047
|
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
838
|
-
|
|
839
|
-
order: z.number().optional().default(0).describe('Step order.'),
|
|
1048
|
+
sourceCode: z.string().describe('Script sourceCode.'),
|
|
1049
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
840
1050
|
config: z.string().optional().describe('Step config JSON object.'),
|
|
841
|
-
sourceCode: z.string().optional().describe('Script/condition sourceCode.'),
|
|
842
1051
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
843
1052
|
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
844
1053
|
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1054
|
+
},
|
|
1055
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1056
|
+
...input,
|
|
1057
|
+
type: 'script',
|
|
1058
|
+
})),
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
server.tool(
|
|
1062
|
+
'ensure_condition_flow_step',
|
|
1063
|
+
'Business operation: create or update one condition flow step. Use this for dynamic conditional branching instead of choosing type=condition manually.',
|
|
1064
|
+
{
|
|
1065
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1066
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1067
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1068
|
+
sourceCode: z.string().describe('Condition sourceCode.'),
|
|
1069
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1070
|
+
config: z.string().optional().describe('Step config JSON object.'),
|
|
1071
|
+
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1072
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1073
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1074
|
+
},
|
|
1075
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1076
|
+
...input,
|
|
1077
|
+
type: 'condition',
|
|
1078
|
+
})),
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
server.tool(
|
|
1082
|
+
'ensure_query_flow_step',
|
|
1083
|
+
'Business operation: create or update one query flow step. Use this for repository/query-style flow steps instead of choosing type=query manually.',
|
|
1084
|
+
{
|
|
1085
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1086
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1087
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1088
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1089
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1090
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1091
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1092
|
+
},
|
|
1093
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1094
|
+
...input,
|
|
1095
|
+
type: 'query',
|
|
1096
|
+
})),
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
server.tool(
|
|
1100
|
+
'ensure_http_flow_step',
|
|
1101
|
+
'Business operation: create or update one HTTP flow step. Use this for outbound HTTP calls instead of choosing type=http manually.',
|
|
1102
|
+
{
|
|
1103
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1104
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1105
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1106
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1107
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1108
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1109
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1110
|
+
},
|
|
1111
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1112
|
+
...input,
|
|
1113
|
+
type: 'http',
|
|
1114
|
+
})),
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
server.tool(
|
|
1118
|
+
'ensure_sleep_flow_step',
|
|
1119
|
+
'Business operation: create or update one sleep/wait flow step. Use this for delays instead of choosing type=sleep manually.',
|
|
1120
|
+
{
|
|
1121
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1122
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1123
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1124
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1125
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1126
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1127
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1128
|
+
},
|
|
1129
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1130
|
+
...input,
|
|
1131
|
+
type: 'sleep',
|
|
1132
|
+
})),
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
server.tool(
|
|
1136
|
+
'ensure_trigger_flow_step',
|
|
1137
|
+
'Business operation: create or update one child-flow trigger step. Use this for flow-to-flow orchestration instead of choosing type=trigger_flow manually.',
|
|
1138
|
+
{
|
|
1139
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1140
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1141
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1142
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1143
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1144
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1145
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1146
|
+
},
|
|
1147
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1148
|
+
...input,
|
|
1149
|
+
type: 'trigger_flow',
|
|
1150
|
+
})),
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
server.tool(
|
|
1154
|
+
'ensure_menu',
|
|
1155
|
+
'Business operation: create or update one admin menu item. Use this instead of raw enfyra_menu CRUD.',
|
|
1156
|
+
{
|
|
1157
|
+
label: z.string().describe('Menu label.'),
|
|
1158
|
+
path: z.string().optional().describe('Admin app route path for leaf menu items, e.g. /reports.'),
|
|
1159
|
+
icon: z.string().optional().describe('Menu icon name.'),
|
|
1160
|
+
type: z.enum(['Menu', 'Dropdown Menu']).optional().default('Menu').describe('Menu type.'),
|
|
1161
|
+
order: z.number().optional().default(0).describe('Display order.'),
|
|
1162
|
+
permission: z.string().optional().describe('Menu permission JSON object.'),
|
|
845
1163
|
description: z.string().optional().describe('Admin note.'),
|
|
1164
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable menu.'),
|
|
846
1165
|
},
|
|
847
|
-
async (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
key,
|
|
864
|
-
type,
|
|
865
|
-
order,
|
|
866
|
-
config: parsedConfig,
|
|
867
|
-
sourceCode,
|
|
868
|
-
scriptLanguage,
|
|
869
|
-
timeout,
|
|
870
|
-
isEnabled,
|
|
871
|
-
description,
|
|
872
|
-
}, getId(flow)));
|
|
873
|
-
const reload = await reloadBestEffort(ENFYRA_API_URL, '/admin/reload/flows');
|
|
874
|
-
return jsonText({ action: 'flow_step_ensured', flow: { id: getId(flow), name: flow.name }, step: { id: operation.id, key, type }, validation, operation, reload });
|
|
1166
|
+
async (input) => jsonText({
|
|
1167
|
+
action: 'menu_ensured',
|
|
1168
|
+
menu: await ensureMenu(ENFYRA_API_URL, input),
|
|
1169
|
+
}),
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
server.tool(
|
|
1173
|
+
'ensure_page_extension',
|
|
1174
|
+
'Business operation: create or update one page extension attached to an existing menu. Validates extension code before save.',
|
|
1175
|
+
{
|
|
1176
|
+
name: z.string().describe('Extension unique name.'),
|
|
1177
|
+
code: z.string().describe('Vue SFC extension code.'),
|
|
1178
|
+
menuId: z.union([z.string(), z.number()]).describe('Existing menu id for this page extension.'),
|
|
1179
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1180
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
1181
|
+
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
875
1182
|
},
|
|
1183
|
+
async (input) => jsonText({
|
|
1184
|
+
action: 'page_extension_ensured',
|
|
1185
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'page' }),
|
|
1186
|
+
}),
|
|
876
1187
|
);
|
|
877
1188
|
|
|
878
1189
|
server.tool(
|
|
879
|
-
'
|
|
880
|
-
'Business operation: create or update
|
|
1190
|
+
'ensure_global_extension',
|
|
1191
|
+
'Business operation: create or update one global shell extension. Validates extension code before save and rejects menu coupling.',
|
|
881
1192
|
{
|
|
882
1193
|
name: z.string().describe('Extension unique name.'),
|
|
883
1194
|
code: z.string().describe('Vue SFC extension code.'),
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
icon: z.string().optional().describe('Menu icon name.'),
|
|
887
|
-
permission: z.string().optional().describe('Menu permission JSON object.'),
|
|
888
|
-
description: z.string().optional().describe('Description for menu/extension.'),
|
|
889
|
-
isEnabled: z.boolean().optional().default(true).describe('Enable menu and extension.'),
|
|
1195
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1196
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
890
1197
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
891
1198
|
},
|
|
892
|
-
async (
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
name,
|
|
908
|
-
type: 'page',
|
|
909
|
-
code,
|
|
910
|
-
menu: { id: menuOperation.id || getId(existingMenu) },
|
|
911
|
-
description,
|
|
912
|
-
isEnabled,
|
|
913
|
-
version,
|
|
914
|
-
});
|
|
915
|
-
return jsonText({
|
|
916
|
-
action: 'menu_extension_page_ensured',
|
|
917
|
-
menu: { id: menuOperation.id || getId(existingMenu), path: normalizedPath, action: menuOperation.action },
|
|
918
|
-
extension: { id: extensionOperation.id || getId(existingExtension), name, action: extensionOperation.action },
|
|
919
|
-
validation,
|
|
920
|
-
});
|
|
1199
|
+
async (input) => jsonText({
|
|
1200
|
+
action: 'global_extension_ensured',
|
|
1201
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'global' }),
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
server.tool(
|
|
1206
|
+
'ensure_widget_extension',
|
|
1207
|
+
'Business operation: create or update one widget extension. Validates extension code before save and rejects menu coupling.',
|
|
1208
|
+
{
|
|
1209
|
+
name: z.string().describe('Extension unique name.'),
|
|
1210
|
+
code: z.string().describe('Vue SFC extension code.'),
|
|
1211
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1212
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
1213
|
+
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
921
1214
|
},
|
|
1215
|
+
async (input) => jsonText({
|
|
1216
|
+
action: 'widget_extension_ensured',
|
|
1217
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'widget' }),
|
|
1218
|
+
}),
|
|
922
1219
|
);
|
|
1220
|
+
|
|
923
1221
|
}
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -3054,69 +3054,6 @@ server.tool(
|
|
|
3054
3054
|
},
|
|
3055
3055
|
);
|
|
3056
3056
|
|
|
3057
|
-
// ============================================================================
|
|
3058
|
-
// MENU & EXTENSION TOOLS
|
|
3059
|
-
// ============================================================================
|
|
3060
|
-
|
|
3061
|
-
server.tool('create_menu', 'Create a menu item in the navigation. Use permission JSON for sensitive menu visibility; successful writes should trigger the app menu reload contract.', {
|
|
3062
|
-
label: z.string().describe('Menu label'),
|
|
3063
|
-
type: z.enum(['Menu', 'Dropdown Menu']).default('Menu').describe('Menu type: "Menu" for leaf items, "Dropdown Menu" for items with children'),
|
|
3064
|
-
icon: z.string().optional().describe('Lucide icon name'),
|
|
3065
|
-
path: z.string().optional().describe('App route path for a clickable menu item, e.g. "/reports".'),
|
|
3066
|
-
externalUrl: z.string().optional().describe('External URL for a menu item when the backend supports external links.'),
|
|
3067
|
-
order: z.number().optional().default(0).describe('Display order'),
|
|
3068
|
-
isEnabled: z.boolean().optional().default(true).describe('Enable menu'),
|
|
3069
|
-
description: z.string().optional().describe('Menu description'),
|
|
3070
|
-
permission: z.string().optional().describe('Optional menu visibility permission JSON object string, e.g. {"or":[{"route":"/reports","methods":["GET"]}]}'),
|
|
3071
|
-
}, async (data) => {
|
|
3072
|
-
const body = { ...data };
|
|
3073
|
-
if (body.permission !== undefined) {
|
|
3074
|
-
body.permission = parseJsonArg(body.permission);
|
|
3075
|
-
if (!body.permission || typeof body.permission !== 'object' || Array.isArray(body.permission)) {
|
|
3076
|
-
throw new Error('permission must be a JSON object string.');
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
if (body.path && !body.path.startsWith('/')) {
|
|
3080
|
-
body.path = '/' + body.path;
|
|
3081
|
-
}
|
|
3082
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_menu', { method: 'POST', body: JSON.stringify(body) });
|
|
3083
|
-
const created = firstDataRecord(result);
|
|
3084
|
-
return { content: [{ type: 'text', text: `Menu created (ID: ${getId(created)}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
3085
|
-
});
|
|
3086
|
-
|
|
3087
|
-
server.tool(
|
|
3088
|
-
'create_extension',
|
|
3089
|
-
[
|
|
3090
|
-
'Create an extension (Vue SFC page, widget, or global shell extension). Code must be Vue SFC: <template>...</template> + <script setup>...</script> — NO imports, use globals (ref, useToast, useApi, UButton, etc).',
|
|
3091
|
-
'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget: no menu, embed via <Widget>. For type=global: no menu, the Enfyra admin UI mounts it invisibly at shell level for registries/realtime. Server auto-compiles and should emit realtime reload to open Enfyra admin tabs. See extension rules in MCP instructions.',
|
|
3092
|
-
].join(' '),
|
|
3093
|
-
{
|
|
3094
|
-
name: z.string().describe('Extension name (unique)'),
|
|
3095
|
-
type: z.enum(['page', 'widget', 'global']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component; global = shell-level lifecycle component'),
|
|
3096
|
-
code: z.string().describe('Vue SFC string — <template> + <script setup>, NO import statements'),
|
|
3097
|
-
menuId: z.string().optional().describe('Required for type=page — enfyra_menu id from create_menu. Omit for widget/global'),
|
|
3098
|
-
isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
|
|
3099
|
-
description: z.string().optional().describe('Extension description'),
|
|
3100
|
-
version: z.string().optional().default('1.0.0').describe('Extension version'),
|
|
3101
|
-
},
|
|
3102
|
-
async (data) => {
|
|
3103
|
-
const body = { ...data };
|
|
3104
|
-
if (body.type === 'page' && !body.menuId) {
|
|
3105
|
-
throw new Error('menuId is required for type=page. Create or find a enfyra_menu record first.');
|
|
3106
|
-
}
|
|
3107
|
-
if (body.type !== 'page' && body.menuId) {
|
|
3108
|
-
throw new Error('menuId is only valid for type=page. Omit menuId for widget/global extensions.');
|
|
3109
|
-
}
|
|
3110
|
-
if (body.menuId) {
|
|
3111
|
-
body.menu = { id: body.menuId };
|
|
3112
|
-
delete body.menuId;
|
|
3113
|
-
}
|
|
3114
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_extension', { method: 'POST', body: JSON.stringify(body) });
|
|
3115
|
-
const created = firstDataRecord(result);
|
|
3116
|
-
return { content: [{ type: 'text', text: `Extension created (ID: ${getId(created)}). Open Enfyra admin tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
|
|
3117
|
-
},
|
|
3118
|
-
);
|
|
3119
|
-
|
|
3120
3057
|
// ============================================================================
|
|
3121
3058
|
// MAIN
|
|
3122
3059
|
// ============================================================================
|