@enfyra/mcp-server 0.0.98 → 0.0.100
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 +484 -182
- 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 } },
|
|
@@ -148,6 +208,10 @@ async function reloadBestEffort(apiUrl, path) {
|
|
|
148
208
|
}
|
|
149
209
|
}
|
|
150
210
|
|
|
211
|
+
function naturalPartialReload(reason) {
|
|
212
|
+
return { attempted: false, succeeded: true, reason };
|
|
213
|
+
}
|
|
214
|
+
|
|
151
215
|
async function validateDynamicScript(apiUrl, sourceCode, scriptLanguage = 'javascript') {
|
|
152
216
|
const result = await fetchAPI(apiUrl, '/admin/script/validate', {
|
|
153
217
|
method: 'POST',
|
|
@@ -249,11 +313,10 @@ function normalizeFlowStepBody(step, flowId) {
|
|
|
249
313
|
const body = {
|
|
250
314
|
key: step.key,
|
|
251
315
|
type: step.type,
|
|
252
|
-
|
|
316
|
+
stepOrder: step.order ?? 0,
|
|
253
317
|
config: step.config ?? {},
|
|
254
318
|
timeout: step.timeout,
|
|
255
319
|
isEnabled: step.isEnabled ?? true,
|
|
256
|
-
description: step.description,
|
|
257
320
|
flow: { id: flowId },
|
|
258
321
|
};
|
|
259
322
|
if (step.sourceCode !== undefined) body.sourceCode = step.sourceCode;
|
|
@@ -261,6 +324,138 @@ function normalizeFlowStepBody(step, flowId) {
|
|
|
261
324
|
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
|
|
262
325
|
}
|
|
263
326
|
|
|
327
|
+
async function ensureMenu(apiUrl, {
|
|
328
|
+
label,
|
|
329
|
+
path,
|
|
330
|
+
icon,
|
|
331
|
+
type = 'Menu',
|
|
332
|
+
order = 0,
|
|
333
|
+
permission,
|
|
334
|
+
description,
|
|
335
|
+
isEnabled = true,
|
|
336
|
+
}) {
|
|
337
|
+
const normalizedPath = path ? normalizeRestPath(path) : undefined;
|
|
338
|
+
const existing = normalizedPath
|
|
339
|
+
? await findRecord(apiUrl, 'enfyra_menu', { path: { _eq: normalizedPath } }, 'id,_id,path,label')
|
|
340
|
+
: await findRecord(apiUrl, 'enfyra_menu', { label: { _eq: label } }, 'id,_id,path,label');
|
|
341
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_menu', existing, {
|
|
342
|
+
label,
|
|
343
|
+
...(normalizedPath ? { path: normalizedPath } : {}),
|
|
344
|
+
icon,
|
|
345
|
+
type,
|
|
346
|
+
order,
|
|
347
|
+
permission: parseJsonObjectArg('permission', permission, undefined),
|
|
348
|
+
description,
|
|
349
|
+
isEnabled,
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
id: operation.id || getId(existing),
|
|
353
|
+
path: normalizedPath || existing?.path || null,
|
|
354
|
+
label,
|
|
355
|
+
action: operation.action,
|
|
356
|
+
operation,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function ensureExtension(apiUrl, {
|
|
361
|
+
name,
|
|
362
|
+
type,
|
|
363
|
+
code,
|
|
364
|
+
menuId,
|
|
365
|
+
description,
|
|
366
|
+
isEnabled = true,
|
|
367
|
+
version = '1.0.0',
|
|
368
|
+
}) {
|
|
369
|
+
if (type === 'page' && !menuId) {
|
|
370
|
+
throw new Error('menuId is required for page extensions. Use ensure_menu first, then ensure_page_extension.');
|
|
371
|
+
}
|
|
372
|
+
if (type !== 'page' && menuId) {
|
|
373
|
+
throw new Error('menuId is only valid for page extensions.');
|
|
374
|
+
}
|
|
375
|
+
const validation = await validateExtensionCode(apiUrl, code, name);
|
|
376
|
+
const existing = await findRecord(apiUrl, 'enfyra_extension', { name: { _eq: name } }, 'id,_id,name,menu.id,type');
|
|
377
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_extension', existing, {
|
|
378
|
+
name,
|
|
379
|
+
type,
|
|
380
|
+
code,
|
|
381
|
+
...(menuId ? { menu: { id: menuId } } : {}),
|
|
382
|
+
description,
|
|
383
|
+
isEnabled,
|
|
384
|
+
version,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
id: operation.id || getId(existing),
|
|
388
|
+
name,
|
|
389
|
+
type,
|
|
390
|
+
action: operation.action,
|
|
391
|
+
operation,
|
|
392
|
+
validation,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function ensureFlow(apiUrl, {
|
|
397
|
+
name,
|
|
398
|
+
triggerType = 'manual',
|
|
399
|
+
triggerConfig,
|
|
400
|
+
timeout,
|
|
401
|
+
maxExecutions = 100,
|
|
402
|
+
isEnabled = true,
|
|
403
|
+
description,
|
|
404
|
+
}) {
|
|
405
|
+
const existing = await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: name } }, 'id,_id,name');
|
|
406
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_flow', existing, {
|
|
407
|
+
name,
|
|
408
|
+
triggerType,
|
|
409
|
+
triggerConfig: parseJsonObjectArg('triggerConfig', triggerConfig, {}),
|
|
410
|
+
timeout,
|
|
411
|
+
maxExecutions,
|
|
412
|
+
isEnabled,
|
|
413
|
+
description,
|
|
414
|
+
});
|
|
415
|
+
const reload = naturalPartialReload('Flow metadata writes trigger the server partial reload contract; there is no dedicated flow reload endpoint.');
|
|
416
|
+
return { action: 'flow_ensured', flow: { id: operation.id, name }, operation, reload };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function ensureFlowStep(apiUrl, {
|
|
420
|
+
flowName,
|
|
421
|
+
flowId,
|
|
422
|
+
key,
|
|
423
|
+
type,
|
|
424
|
+
order,
|
|
425
|
+
config,
|
|
426
|
+
sourceCode,
|
|
427
|
+
scriptLanguage,
|
|
428
|
+
timeout,
|
|
429
|
+
isEnabled,
|
|
430
|
+
}) {
|
|
431
|
+
if (!flowName && !flowId) throw new Error('Provide flowName or flowId.');
|
|
432
|
+
if (flowName && flowId) throw new Error('Provide flowName or flowId, not both.');
|
|
433
|
+
const flow = flowId
|
|
434
|
+
? await findRecord(apiUrl, 'enfyra_flow', { id: { _eq: flowId } }, 'id,_id,name')
|
|
435
|
+
: await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: flowName } }, 'id,_id,name');
|
|
436
|
+
if (!flow) throw new Error(`Flow not found: ${flowId || flowName}`);
|
|
437
|
+
const parsedConfig = parseJsonObjectArg('config', config, {});
|
|
438
|
+
const validation = sourceCode && ['script', 'condition'].includes(type)
|
|
439
|
+
? await validateDynamicScript(apiUrl, sourceCode, scriptLanguage)
|
|
440
|
+
: { validated: false, reason: 'no script validation required' };
|
|
441
|
+
const existing = await findRecord(apiUrl, 'enfyra_flow_step', {
|
|
442
|
+
flow: { id: { _eq: getId(flow) } },
|
|
443
|
+
key: { _eq: key },
|
|
444
|
+
}, 'id,_id,key,flow.id');
|
|
445
|
+
const operation = await createOrPatch(apiUrl, 'enfyra_flow_step', existing, normalizeFlowStepBody({
|
|
446
|
+
key,
|
|
447
|
+
type,
|
|
448
|
+
order,
|
|
449
|
+
config: parsedConfig,
|
|
450
|
+
sourceCode,
|
|
451
|
+
scriptLanguage,
|
|
452
|
+
timeout,
|
|
453
|
+
isEnabled,
|
|
454
|
+
}, getId(flow)));
|
|
455
|
+
const reload = naturalPartialReload('Flow step writes trigger the server partial reload contract; there is no dedicated flow reload endpoint.');
|
|
456
|
+
return { action: 'flow_step_ensured', flow: { id: getId(flow), name: flow.name }, step: { id: operation.id, key, type }, validation, operation, reload };
|
|
457
|
+
}
|
|
458
|
+
|
|
264
459
|
export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
265
460
|
server.tool(
|
|
266
461
|
'validate_dynamic_script',
|
|
@@ -321,102 +516,105 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
321
516
|
);
|
|
322
517
|
|
|
323
518
|
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(' '),
|
|
519
|
+
'add_route_methods',
|
|
520
|
+
'Business operation: add HTTP methods to an existing route.',
|
|
330
521
|
{
|
|
331
522
|
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
332
523
|
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.'),
|
|
524
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to add.'),
|
|
335
525
|
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
336
526
|
},
|
|
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;
|
|
527
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
528
|
+
path,
|
|
529
|
+
routeId,
|
|
530
|
+
methods,
|
|
531
|
+
mode: 'merge',
|
|
532
|
+
isEnabled,
|
|
533
|
+
})),
|
|
534
|
+
);
|
|
351
535
|
|
|
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
|
-
};
|
|
536
|
+
server.tool(
|
|
537
|
+
'replace_route_methods',
|
|
538
|
+
'Business operation: replace an existing route availableMethods list exactly.',
|
|
539
|
+
{
|
|
540
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
541
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
542
|
+
methods: z.array(z.string()).min(1).describe('Exact HTTP method names for availableMethods.'),
|
|
543
|
+
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
370
544
|
},
|
|
545
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
546
|
+
path,
|
|
547
|
+
routeId,
|
|
548
|
+
methods,
|
|
549
|
+
mode: 'replace',
|
|
550
|
+
isEnabled,
|
|
551
|
+
})),
|
|
371
552
|
);
|
|
372
553
|
|
|
373
554
|
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(' '),
|
|
555
|
+
'remove_route_methods',
|
|
556
|
+
'Business operation: remove HTTP methods from an existing route.',
|
|
380
557
|
{
|
|
381
558
|
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
382
559
|
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
|
-
|
|
560
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to remove.'),
|
|
561
|
+
isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
|
|
385
562
|
},
|
|
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
|
-
};
|
|
563
|
+
async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
|
|
564
|
+
path,
|
|
565
|
+
routeId,
|
|
566
|
+
methods,
|
|
567
|
+
mode: 'remove',
|
|
568
|
+
isEnabled,
|
|
569
|
+
})),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
server.tool(
|
|
573
|
+
'publish_route_methods',
|
|
574
|
+
'Business operation: make existing route methods public/anonymous.',
|
|
575
|
+
{
|
|
576
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
577
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
578
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to publish. They must already be available on the route.'),
|
|
419
579
|
},
|
|
580
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
581
|
+
path,
|
|
582
|
+
routeId,
|
|
583
|
+
methods,
|
|
584
|
+
mode: 'merge',
|
|
585
|
+
})),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
server.tool(
|
|
589
|
+
'replace_public_route_methods',
|
|
590
|
+
'Business operation: replace a route publicMethods list exactly.',
|
|
591
|
+
{
|
|
592
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
593
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
594
|
+
methods: z.array(z.string()).describe('Exact HTTP method names that should be public. Use an empty array to make all methods private.'),
|
|
595
|
+
},
|
|
596
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
597
|
+
path,
|
|
598
|
+
routeId,
|
|
599
|
+
methods,
|
|
600
|
+
mode: 'replace',
|
|
601
|
+
})),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
server.tool(
|
|
605
|
+
'unpublish_route_methods',
|
|
606
|
+
'Business operation: make specific public route methods private again.',
|
|
607
|
+
{
|
|
608
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
609
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
610
|
+
methods: z.array(z.string()).min(1).describe('HTTP method names to remove from publicMethods.'),
|
|
611
|
+
},
|
|
612
|
+
async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
|
|
613
|
+
path,
|
|
614
|
+
routeId,
|
|
615
|
+
methods,
|
|
616
|
+
mode: 'remove',
|
|
617
|
+
})),
|
|
420
618
|
);
|
|
421
619
|
|
|
422
620
|
server.tool(
|
|
@@ -739,52 +937,52 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
739
937
|
|
|
740
938
|
server.tool(
|
|
741
939
|
'ensure_websocket_gateway',
|
|
742
|
-
'Business operation: create or update an Enfyra Socket.IO gateway. Connection handler
|
|
940
|
+
'Business operation: create or update an Enfyra Socket.IO gateway. Connection handler sourceCode is validated before save.',
|
|
743
941
|
{
|
|
744
942
|
path: z.string().describe('Gateway namespace/path, e.g. /chat.'),
|
|
745
|
-
|
|
943
|
+
sourceCode: z.string().optional().describe('Optional connection handler dynamic script sourceCode.'),
|
|
746
944
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for connection handler.'),
|
|
747
945
|
isEnabled: z.boolean().optional().default(true).describe('Enable gateway.'),
|
|
748
946
|
description: z.string().optional().describe('Admin note.'),
|
|
749
947
|
},
|
|
750
|
-
async ({ path,
|
|
948
|
+
async ({ path, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
751
949
|
const normalizedPath = normalizeRestPath(path);
|
|
752
|
-
const validation =
|
|
753
|
-
? { validated: false, reason: 'no
|
|
754
|
-
: await validateDynamicScript(ENFYRA_API_URL,
|
|
950
|
+
const validation = sourceCode === undefined
|
|
951
|
+
? { validated: false, reason: 'no sourceCode' }
|
|
952
|
+
: await validateDynamicScript(ENFYRA_API_URL, sourceCode, scriptLanguage);
|
|
755
953
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { path: { _eq: normalizedPath } }, 'id,_id,path');
|
|
756
954
|
const body = {
|
|
757
955
|
path: normalizedPath,
|
|
758
956
|
isEnabled,
|
|
759
957
|
description,
|
|
760
|
-
...(
|
|
958
|
+
...(sourceCode !== undefined ? { sourceCode, scriptLanguage } : {}),
|
|
761
959
|
};
|
|
762
960
|
const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_websocket', existing, body);
|
|
763
|
-
const reload =
|
|
961
|
+
const reload = naturalPartialReload('Websocket metadata writes trigger the server partial reload contract; there is no dedicated websocket reload endpoint.');
|
|
764
962
|
return jsonText({ action: 'websocket_gateway_ensured', gateway: { id: operation.id, path: normalizedPath }, validation, operation, reload });
|
|
765
963
|
},
|
|
766
964
|
);
|
|
767
965
|
|
|
768
966
|
server.tool(
|
|
769
967
|
'ensure_websocket_event',
|
|
770
|
-
'Business operation: create or update one websocket event handler. It resolves gateway path/id and validates
|
|
968
|
+
'Business operation: create or update one websocket event handler. It resolves gateway path/id and validates sourceCode before save.',
|
|
771
969
|
{
|
|
772
970
|
gatewayPath: z.string().optional().describe('Gateway path, e.g. /chat. Use gatewayPath or gatewayId.'),
|
|
773
971
|
gatewayId: z.union([z.string(), z.number()]).optional().describe('Gateway id. Use gatewayPath or gatewayId.'),
|
|
774
972
|
eventName: z.string().describe('Socket event name.'),
|
|
775
|
-
|
|
973
|
+
sourceCode: z.string().describe('Event handler dynamic script sourceCode.'),
|
|
776
974
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
777
975
|
isEnabled: z.boolean().optional().default(true).describe('Enable event.'),
|
|
778
976
|
description: z.string().optional().describe('Admin note.'),
|
|
779
977
|
},
|
|
780
|
-
async ({ gatewayPath, gatewayId, eventName,
|
|
978
|
+
async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
781
979
|
if (!gatewayPath && !gatewayId) throw new Error('Provide gatewayPath or gatewayId.');
|
|
782
980
|
if (gatewayPath && gatewayId) throw new Error('Provide gatewayPath or gatewayId, not both.');
|
|
783
981
|
const gateway = gatewayId
|
|
784
982
|
? await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { id: { _eq: gatewayId } }, 'id,_id,path')
|
|
785
983
|
: await findRecord(ENFYRA_API_URL, 'enfyra_websocket', { path: { _eq: normalizeRestPath(gatewayPath) } }, 'id,_id,path');
|
|
786
984
|
if (!gateway) throw new Error(`Websocket gateway not found: ${gatewayId || gatewayPath}`);
|
|
787
|
-
const validation = await validateDynamicScript(ENFYRA_API_URL,
|
|
985
|
+
const validation = await validateDynamicScript(ENFYRA_API_URL, sourceCode, scriptLanguage);
|
|
788
986
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_websocket_event', {
|
|
789
987
|
gateway: { id: { _eq: getId(gateway) } },
|
|
790
988
|
eventName: { _eq: eventName },
|
|
@@ -792,132 +990,236 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
792
990
|
const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_websocket_event', existing, {
|
|
793
991
|
gateway: { id: getId(gateway) },
|
|
794
992
|
eventName,
|
|
795
|
-
|
|
993
|
+
sourceCode,
|
|
796
994
|
scriptLanguage,
|
|
797
995
|
isEnabled,
|
|
798
996
|
description,
|
|
799
997
|
});
|
|
800
|
-
const reload =
|
|
998
|
+
const reload = naturalPartialReload('Websocket event writes trigger the server partial reload contract; there is no dedicated websocket reload endpoint.');
|
|
801
999
|
return jsonText({ action: 'websocket_event_ensured', gateway: { id: getId(gateway), path: gateway.path }, eventName, validation, operation, reload });
|
|
802
1000
|
},
|
|
803
1001
|
);
|
|
804
1002
|
|
|
805
1003
|
server.tool(
|
|
806
|
-
'
|
|
807
|
-
'Business operation: create or update
|
|
1004
|
+
'ensure_manual_flow',
|
|
1005
|
+
'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
1006
|
{
|
|
809
1007
|
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
810
|
-
trigger: z.string().optional().describe('Flow trigger type/key.'),
|
|
811
1008
|
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|
|
812
1009
|
maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
|
|
813
1010
|
isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
|
|
814
1011
|
description: z.string().optional().describe('Admin note.'),
|
|
815
1012
|
},
|
|
816
|
-
async ({ name,
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1013
|
+
async ({ name, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
|
|
1014
|
+
name,
|
|
1015
|
+
triggerType: 'manual',
|
|
1016
|
+
timeout,
|
|
1017
|
+
maxExecutions,
|
|
1018
|
+
isEnabled,
|
|
1019
|
+
description,
|
|
1020
|
+
})),
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
server.tool(
|
|
1024
|
+
'ensure_scheduled_flow',
|
|
1025
|
+
'Business operation: create or update a scheduled Enfyra flow. Use this only for cron/time-based flows.',
|
|
1026
|
+
{
|
|
1027
|
+
name: z.string().describe('Flow name. Existing flow with this name is updated.'),
|
|
1028
|
+
triggerConfig: z.string().describe('Schedule config JSON object.'),
|
|
1029
|
+
timeout: z.number().int().positive().optional().describe('Flow timeout in ms.'),
|
|
1030
|
+
maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
|
|
1031
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
|
|
1032
|
+
description: z.string().optional().describe('Admin note.'),
|
|
828
1033
|
},
|
|
1034
|
+
async ({ name, triggerConfig, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
|
|
1035
|
+
name,
|
|
1036
|
+
triggerType: 'schedule',
|
|
1037
|
+
triggerConfig,
|
|
1038
|
+
timeout,
|
|
1039
|
+
maxExecutions,
|
|
1040
|
+
isEnabled,
|
|
1041
|
+
description,
|
|
1042
|
+
})),
|
|
829
1043
|
);
|
|
830
1044
|
|
|
831
1045
|
server.tool(
|
|
832
|
-
'
|
|
833
|
-
'Business operation: create or update one flow step
|
|
1046
|
+
'ensure_script_flow_step',
|
|
1047
|
+
'Business operation: create or update one script flow step. Use this for JavaScript/TypeScript flow logic instead of choosing type=script manually.',
|
|
834
1048
|
{
|
|
835
1049
|
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
836
1050
|
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
837
1051
|
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.'),
|
|
1052
|
+
sourceCode: z.string().describe('Script sourceCode.'),
|
|
1053
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
840
1054
|
config: z.string().optional().describe('Step config JSON object.'),
|
|
841
|
-
sourceCode: z.string().optional().describe('Script/condition sourceCode.'),
|
|
842
1055
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
843
1056
|
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
844
1057
|
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1058
|
+
},
|
|
1059
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1060
|
+
...input,
|
|
1061
|
+
type: 'script',
|
|
1062
|
+
})),
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
server.tool(
|
|
1066
|
+
'ensure_condition_flow_step',
|
|
1067
|
+
'Business operation: create or update one condition flow step. Use this for dynamic conditional branching instead of choosing type=condition manually.',
|
|
1068
|
+
{
|
|
1069
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1070
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1071
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1072
|
+
sourceCode: z.string().describe('Condition sourceCode.'),
|
|
1073
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1074
|
+
config: z.string().optional().describe('Step config JSON object.'),
|
|
1075
|
+
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1076
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1077
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1078
|
+
},
|
|
1079
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1080
|
+
...input,
|
|
1081
|
+
type: 'condition',
|
|
1082
|
+
})),
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
server.tool(
|
|
1086
|
+
'ensure_query_flow_step',
|
|
1087
|
+
'Business operation: create or update one query flow step. Use this for repository/query-style flow steps instead of choosing type=query manually.',
|
|
1088
|
+
{
|
|
1089
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1090
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1091
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1092
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1093
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1094
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1095
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1096
|
+
},
|
|
1097
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1098
|
+
...input,
|
|
1099
|
+
type: 'query',
|
|
1100
|
+
})),
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
server.tool(
|
|
1104
|
+
'ensure_http_flow_step',
|
|
1105
|
+
'Business operation: create or update one HTTP flow step. Use this for outbound HTTP calls instead of choosing type=http manually.',
|
|
1106
|
+
{
|
|
1107
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1108
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1109
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1110
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1111
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1112
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1113
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1114
|
+
},
|
|
1115
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1116
|
+
...input,
|
|
1117
|
+
type: 'http',
|
|
1118
|
+
})),
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
server.tool(
|
|
1122
|
+
'ensure_sleep_flow_step',
|
|
1123
|
+
'Business operation: create or update one sleep/wait flow step. Use this for delays instead of choosing type=sleep manually.',
|
|
1124
|
+
{
|
|
1125
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1126
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1127
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1128
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1129
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1130
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1131
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1132
|
+
},
|
|
1133
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1134
|
+
...input,
|
|
1135
|
+
type: 'sleep',
|
|
1136
|
+
})),
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
server.tool(
|
|
1140
|
+
'ensure_trigger_flow_step',
|
|
1141
|
+
'Business operation: create or update one child-flow trigger step. Use this for flow-to-flow orchestration instead of choosing type=trigger_flow manually.',
|
|
1142
|
+
{
|
|
1143
|
+
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
1144
|
+
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|
|
1145
|
+
key: z.string().describe('Stable step key. Existing step with flow+key is updated.'),
|
|
1146
|
+
config: z.string().describe('Step config JSON object.'),
|
|
1147
|
+
order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
|
|
1148
|
+
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1149
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1150
|
+
},
|
|
1151
|
+
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1152
|
+
...input,
|
|
1153
|
+
type: 'trigger_flow',
|
|
1154
|
+
})),
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
server.tool(
|
|
1158
|
+
'ensure_menu',
|
|
1159
|
+
'Business operation: create or update one admin menu item. Use this instead of raw enfyra_menu CRUD.',
|
|
1160
|
+
{
|
|
1161
|
+
label: z.string().describe('Menu label.'),
|
|
1162
|
+
path: z.string().optional().describe('Admin app route path for leaf menu items, e.g. /reports.'),
|
|
1163
|
+
icon: z.string().optional().describe('Menu icon name.'),
|
|
1164
|
+
type: z.enum(['Menu', 'Dropdown Menu']).optional().default('Menu').describe('Menu type.'),
|
|
1165
|
+
order: z.number().optional().default(0).describe('Display order.'),
|
|
1166
|
+
permission: z.string().optional().describe('Menu permission JSON object.'),
|
|
845
1167
|
description: z.string().optional().describe('Admin note.'),
|
|
1168
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable menu.'),
|
|
846
1169
|
},
|
|
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 });
|
|
1170
|
+
async (input) => jsonText({
|
|
1171
|
+
action: 'menu_ensured',
|
|
1172
|
+
menu: await ensureMenu(ENFYRA_API_URL, input),
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
server.tool(
|
|
1177
|
+
'ensure_page_extension',
|
|
1178
|
+
'Business operation: create or update one page extension attached to an existing menu. Validates extension code before save.',
|
|
1179
|
+
{
|
|
1180
|
+
name: z.string().describe('Extension unique name.'),
|
|
1181
|
+
code: z.string().describe('Vue SFC extension code.'),
|
|
1182
|
+
menuId: z.union([z.string(), z.number()]).describe('Existing menu id for this page extension.'),
|
|
1183
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1184
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
1185
|
+
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
875
1186
|
},
|
|
1187
|
+
async (input) => jsonText({
|
|
1188
|
+
action: 'page_extension_ensured',
|
|
1189
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'page' }),
|
|
1190
|
+
}),
|
|
876
1191
|
);
|
|
877
1192
|
|
|
878
1193
|
server.tool(
|
|
879
|
-
'
|
|
880
|
-
'Business operation: create or update
|
|
1194
|
+
'ensure_global_extension',
|
|
1195
|
+
'Business operation: create or update one global shell extension. Validates extension code before save and rejects menu coupling.',
|
|
881
1196
|
{
|
|
882
1197
|
name: z.string().describe('Extension unique name.'),
|
|
883
1198
|
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.'),
|
|
1199
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1200
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
890
1201
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
891
1202
|
},
|
|
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
|
-
});
|
|
1203
|
+
async (input) => jsonText({
|
|
1204
|
+
action: 'global_extension_ensured',
|
|
1205
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'global' }),
|
|
1206
|
+
}),
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
server.tool(
|
|
1210
|
+
'ensure_widget_extension',
|
|
1211
|
+
'Business operation: create or update one widget extension. Validates extension code before save and rejects menu coupling.',
|
|
1212
|
+
{
|
|
1213
|
+
name: z.string().describe('Extension unique name.'),
|
|
1214
|
+
code: z.string().describe('Vue SFC extension code.'),
|
|
1215
|
+
description: z.string().optional().describe('Extension description.'),
|
|
1216
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
1217
|
+
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
921
1218
|
},
|
|
1219
|
+
async (input) => jsonText({
|
|
1220
|
+
action: 'widget_extension_ensured',
|
|
1221
|
+
extension: await ensureExtension(ENFYRA_API_URL, { ...input, type: 'widget' }),
|
|
1222
|
+
}),
|
|
922
1223
|
);
|
|
1224
|
+
|
|
923
1225
|
}
|
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
|
// ============================================================================
|