@enfyra/mcp-server 0.1.6 → 0.1.8
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 +4 -1
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +5 -17
- package/src/lib/mcp-instructions.js +2 -1
- package/src/lib/platform-operation-tools.js +11 -4
- package/src/lib/required-knowledge.js +1 -1
- package/src/lib/table-tools.js +63 -29
- package/src/lib/tool-routing.js +581 -0
- package/src/mcp-server-entry.mjs +55 -30
package/README.md
CHANGED
|
@@ -244,6 +244,8 @@ Use `get_enfyra_examples` from the MCP tool list when asking an LLM to generate
|
|
|
244
244
|
- files and storage
|
|
245
245
|
- Enfyra admin extensions
|
|
246
246
|
|
|
247
|
+
Use `discover_enfyra_workflows` when an LLM knows the goal but may not know the right Enfyra tool path. It returns progressive-disclosure workflow matches with first tools, required acknowledgements, verification tools, relevant example categories, and `avoidTools` boundaries that prevent near-correct but unsafe tool choices.
|
|
248
|
+
|
|
247
249
|
Use `get_enfyra_required_knowledge` before asking an LLM to mutate metadata, schema, routes, permissions, menus, packages, cache state, dynamic server code, or Enfyra extension code. It returns global rules plus acknowledgement keys that write tools verify before saving. Dynamic server code also requires the dynamic-code acknowledgement key, and extension code also requires the extension acknowledgement key.
|
|
248
250
|
|
|
249
251
|
## Runtime Safety
|
|
@@ -262,6 +264,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
262
264
|
- Relation tools reject physical FK/junction names and resolve table ids from exact table names or aliases before schema mutation.
|
|
263
265
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
264
266
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
|
267
|
+
- `discover_enfyra_workflows` maps task intent to workflow surfaces before the agent loads detailed examples or guesses between similar tools.
|
|
265
268
|
- Platform operation tools such as `api_endpoint_workflow`, `create_api_endpoint`, `enable_route`, `disable_route`, `delete_route`, `public_route_methods`, `add_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `choose_flow_step_tool`, fixed-type flow step tools, `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
266
269
|
- Schema changes are serialized.
|
|
267
270
|
- Destructive deletes return a preview before requiring `confirm=true`.
|
|
@@ -300,7 +303,7 @@ Do not create custom login/logout/me routes that manually set Enfyra token cooki
|
|
|
300
303
|
|
|
301
304
|
## Tool Summary
|
|
302
305
|
|
|
303
|
-
The MCP server exposes tools for metadata discovery, required knowledge, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, `get_enfyra_api_context`, and `get_enfyra_required_knowledge`.
|
|
306
|
+
The MCP server exposes tools for workflow routing, metadata discovery, required knowledge, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, `get_enfyra_api_context`, and `get_enfyra_required_knowledge`.
|
|
304
307
|
|
|
305
308
|
Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods` and `private_route_methods` for that access boundary.
|
|
306
309
|
|
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -1597,26 +1597,14 @@ ensure_page_extension({
|
|
|
1597
1597
|
'Use enfyra_menu.label, not title.',
|
|
1598
1598
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1599
1599
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1600
|
-
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
|
|
1600
|
+
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI; that tool is the authority for theme, color, layout, modal, drawer, and shell registry details.',
|
|
1601
1601
|
'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
|
|
1602
1602
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1603
|
-
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1604
|
-
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
1605
1603
|
'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
|
|
1606
|
-
'Page extensions should be full-bleed
|
|
1607
|
-
'
|
|
1608
|
-
'Use
|
|
1609
|
-
'
|
|
1610
|
-
'Decision cases: normal decorative accents, feature icons, non-state tiles, active tabs, progress fills, selected segments, and primary actions use runtime primary/identity classes; true semantic states use their matching status colors such as error, warning, success, or info; large ordinary surfaces stay neutral and carry only small badges/icons for status; selected/current entity blocks may use eapp-primary-surface.',
|
|
1611
|
-
'Pattern examples: KPI/metric cards should be eapp-surface-card with a small icon tile using eapp-primary-soft eapp-icon-tile; selected/current entity cards may use eapp-primary-surface; progress bars use eapp-surface-muted tracks plus eapp-primary-solid fills; list rows use eapp-surface-card/eapp-divide-y/eapp-surface-hover and only small chips inside; primary scope actions use UButton color="primary" variant="solid"; secondary actions use neutral variants.',
|
|
1612
|
-
'Status colors belong only in UBadge/UAlert semantic colors or eapp-status-*-soft/text/border classes for badges, small icons, or short status text. Do not read --badge-* variables directly in extension templates. Do not color large panels, alert-like success blocks, KPI cards, list containers, or attention/reconciliation blocks green/yellow/red because of state; keep the block neutral and place the status badge/icon inside.',
|
|
1613
|
-
'Use PageHeader gradient: "none" for generated operational pages unless the user explicitly asks for a decorative page accent; do not hardcode cyan/violet/purple/blue/green gradients.',
|
|
1614
|
-
'For general card grids inside the shell, use md:grid-cols-2 xl:grid-cols-3 instead of lg:grid-cols-3 because the desktop sidebar leaves tablet-width content at 1024px.',
|
|
1615
|
-
'Do not use Nuxt UI neutral semantic classes such as bg-default, text-muted, text-dimmed, border-default, or divide-default inside extension code; use eApp class tokens instead. Do not write text-[var(...)], bg-[var(...)], or border-[var(...)] in generated extension templates unless no class token exists for that exact primitive.',
|
|
1616
|
-
'Do not pass ui.content: "eapp-surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller content classes should only append z-index or width.',
|
|
1617
|
-
'CommonModal and CommonDrawer own action-only footers through cancelAction, primaryAction, dangerAction, leadingActions, and footerHint. Pass action intent through props instead of styling footer buttons manually; cancelAction defaults to neutral outline, dangerAction is for irreversible destructive work, and Keep editing should use tone: "primary" in discard dialogs.',
|
|
1618
|
-
'Use UTabs for page sections instead of custom tab bars so the app-level active/inactive indicators, spacing, focus rings, and theme contrast stay consistent.',
|
|
1619
|
-
'Do not inject global CSS, create theme guards, redefine the app palette, or solve one extension by overriding the whole app shell.',
|
|
1604
|
+
'Page extensions should be full-bleed and responsive from the first version; the extension root is already inside the Enfyra admin page main.',
|
|
1605
|
+
'Render ordinary metrics and lists in the body, not PageHeader.stats, unless the user explicitly wants a compact overview header.',
|
|
1606
|
+
'Use app theme tokens and Nuxt UI semantic colors by intent; do not hard-code concrete palettes or redefine the app palette inside extension code.',
|
|
1607
|
+
'Use app-owned primitives such as UTabs, CommonModal, CommonDrawer, Widget, useMenuNotificationRegistry, and useAccountPanelRegistry when the workflow matches them.',
|
|
1620
1608
|
'Keep list selection local and fetch detail rows only; do not refetch the whole list after a row click unless the list data changed.',
|
|
1621
1609
|
'Page extension paths are admin app UI routes. Do not verify them with test_rest_endpoint against ENFYRA_API_URL unless inspect_route shows an API route with the same path.',
|
|
1622
1610
|
'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
|
|
@@ -25,13 +25,14 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
25
25
|
'',
|
|
26
26
|
'### Work Flow',
|
|
27
27
|
'- For a quick target/base sanity check, call `get_enfyra_api_context`; do not call broad discovery just to confirm which instance this MCP is connected to.',
|
|
28
|
+
'- When the task intent is clear but the right tool path is not, call `discover_enfyra_workflows` with the intent, risk, and optional surface. Use `detail: "plan"` before writes to get firstTools, required acknowledgements, verify tools, and avoidTools.',
|
|
28
29
|
'- Discover before deciding. For architecture/capability questions call `discover_enfyra_system`; for DB/pk/runtime/cache context call `discover_runtime_context`; for filters/deep/sort/relation query shape call `discover_query_capabilities`. Run broad discovery tools sequentially, not in parallel.',
|
|
29
30
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
31
|
'- Load examples only when needed. Use `get_enfyra_examples` by category. Before extension UI, call `get_extension_theme_contract`; call `get_theme_class_reference` for exact eapp/Nuxt UI theme classes.',
|
|
31
32
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
33
|
'- Before mutating metadata, schema, routes, permissions, menus, packages, cache state, dynamic code, or extension UI, call `get_enfyra_required_knowledge`, read the global rules, and pass `globalRulesAckKey` into write tools. Dynamic server code also requires `dynamicCodeAckKey`; extension code also requires `extensionAckKey`.',
|
|
33
34
|
'- With non-root API tokens, call `get_permission_profile` before relying on admin helper tools or when debugging 403s. MCP admin helpers require ordinary route permissions for static admin routes such as `/admin/script/validate`, `/admin/test/run`, `/admin/flow/trigger/:id`, and `/admin/reload/*`.',
|
|
34
|
-
'- Prefer the most specific business operation tool over raw metadata CRUD
|
|
35
|
+
'- Prefer the most specific business operation tool over raw metadata CRUD. `discover_enfyra_workflows` provides the current operation-tool map and negative-routing avoidTools.',
|
|
35
36
|
'- 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.',
|
|
36
37
|
'- 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.',
|
|
37
38
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
@@ -1441,7 +1441,8 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1441
1441
|
'create_api_endpoint',
|
|
1442
1442
|
[
|
|
1443
1443
|
'Business operation: create or update a custom REST endpoint with a handler in one safe operation.',
|
|
1444
|
-
'
|
|
1444
|
+
'Prefer api_endpoint_workflow when route access, role/user permissions, overwrite decisions, or multi-step planning matter.',
|
|
1445
|
+
'Use this one-shot helper only when the endpoint contract is already clear and no authenticated route-permission step is needed in the same operation, such as a simple public webhook or private admin-only utility that will be granted separately.',
|
|
1445
1446
|
'It creates the route without mainTableId, ensures the method is available, validates sourceCode, creates or overwrites the route handler, optionally makes the method public, reloads routes, and can smoke-test the endpoint.',
|
|
1446
1447
|
'Use table/schema tools separately when the user needs persisted data. This tool is for custom behavior endpoints.',
|
|
1447
1448
|
].join(' '),
|
|
@@ -1591,8 +1592,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1591
1592
|
message: z.string().optional().describe('Custom validation error message.'),
|
|
1592
1593
|
description: z.string().optional().describe('Admin note.'),
|
|
1593
1594
|
isEnabled: z.boolean().optional().default(true).describe('Enable the rule.'),
|
|
1595
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
1594
1596
|
},
|
|
1595
|
-
async ({ tableName, columnName, ruleType, value, message, description, isEnabled }) => {
|
|
1597
|
+
async ({ tableName, columnName, ruleType, value, message, description, isEnabled, globalRulesAckKey }) => {
|
|
1598
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
1596
1599
|
const table = resolveTable(await getMetadataTables(ENFYRA_API_URL), tableName);
|
|
1597
1600
|
const column = resolveColumn(table, columnName);
|
|
1598
1601
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_column_rule', {
|
|
@@ -1632,8 +1635,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1632
1635
|
condition: z.string().optional().describe('Condition JSON object using field permission DSL.'),
|
|
1633
1636
|
description: z.string().optional().describe('Admin note.'),
|
|
1634
1637
|
isEnabled: z.boolean().optional().default(true).describe('Enable the permission.'),
|
|
1638
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
1635
1639
|
},
|
|
1636
|
-
async ({ tableName, columnName, relationName, action, effect, roleId, roleName, allowedUserIds, condition, description, isEnabled }) => {
|
|
1640
|
+
async ({ tableName, columnName, relationName, action, effect, roleId, roleName, allowedUserIds, condition, description, isEnabled, globalRulesAckKey }) => {
|
|
1641
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
1637
1642
|
if (!!columnName === !!relationName) throw new Error('Provide exactly one of columnName or relationName.');
|
|
1638
1643
|
assertOneScope({ roleId, roleName, allowedUserIds });
|
|
1639
1644
|
const [tables, role] = await Promise.all([
|
|
@@ -1691,8 +1696,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1691
1696
|
description: z.string().optional().describe('Admin note.'),
|
|
1692
1697
|
rules: z.string().optional().describe('Rules JSON array: [{type, config, priority, isEnabled, description, userIds}].'),
|
|
1693
1698
|
rulesMode: z.enum(['append', 'replace', 'none']).optional().default('append').describe('append creates rules, replace disables existing rules first, none leaves rules unchanged.'),
|
|
1699
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
1694
1700
|
},
|
|
1695
|
-
async ({ name, guardId, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules, rulesMode }) => {
|
|
1701
|
+
async ({ name, guardId, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules, rulesMode, globalRulesAckKey }) => {
|
|
1702
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
1696
1703
|
if (path && routeId) throw new Error('Provide path or routeId, not both.');
|
|
1697
1704
|
const ruleInputs = parseJsonArrayArg('rules', rules, []);
|
|
1698
1705
|
if (position === 'pre_auth') {
|
|
@@ -49,7 +49,7 @@ export function assertExtensionKnowledgeAckIf(condition, key) {
|
|
|
49
49
|
export function buildRequiredKnowledgePayload() {
|
|
50
50
|
return {
|
|
51
51
|
version: REQUIRED_KNOWLEDGE_VERSION,
|
|
52
|
-
purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, dynamic server code, or extension UI through MCP.',
|
|
52
|
+
purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, menus, packages, cache state, dynamic server code, or extension UI through MCP.',
|
|
53
53
|
globalRulesAckKey: GLOBAL_RULES_ACK_KEY,
|
|
54
54
|
dynamicCodeAckKey: DYNAMIC_CODE_KNOWLEDGE_ACK_KEY,
|
|
55
55
|
extensionAckKey: EXTENSION_KNOWLEDGE_ACK_KEY,
|
package/src/lib/table-tools.js
CHANGED
|
@@ -313,7 +313,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
313
313
|
return withSchemaQueue(async () => {
|
|
314
314
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
|
|
315
315
|
if (!tableData) {
|
|
316
|
-
|
|
316
|
+
throw new Error(`Table with ID ${args.tableId} not found.`);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -325,9 +325,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
325
325
|
columnName: args.name,
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
-
return {
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
return jsonContent({
|
|
329
|
+
action: 'column_created',
|
|
330
|
+
tableId: args.tableId,
|
|
331
|
+
columnName: args.name,
|
|
332
|
+
result,
|
|
333
|
+
});
|
|
331
334
|
});
|
|
332
335
|
}
|
|
333
336
|
|
|
@@ -341,7 +344,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
341
344
|
const resolvedTargetTableId = resolveTableIdentifierFromMetadata(metadata, targetTableId, 'targetTableId');
|
|
342
345
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedSourceTableId);
|
|
343
346
|
if (!tableData) {
|
|
344
|
-
|
|
347
|
+
throw new Error(`Table ${sourceTableId} not found.`);
|
|
345
348
|
}
|
|
346
349
|
const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
|
|
347
350
|
const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
|
|
@@ -356,9 +359,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
356
359
|
action: 'create',
|
|
357
360
|
propertyName,
|
|
358
361
|
});
|
|
359
|
-
return {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
return jsonContent({
|
|
363
|
+
action: 'relation_created',
|
|
364
|
+
relation: { propertyName, type, sourceTableId: resolvedSourceTableId, targetTableId: resolvedTargetTableId },
|
|
365
|
+
result,
|
|
366
|
+
});
|
|
362
367
|
});
|
|
363
368
|
}
|
|
364
369
|
|
|
@@ -366,7 +371,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
366
371
|
return withSchemaQueue(async () => {
|
|
367
372
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
368
373
|
if (!tableData) {
|
|
369
|
-
|
|
374
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
370
375
|
}
|
|
371
376
|
|
|
372
377
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -399,9 +404,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
399
404
|
columnId,
|
|
400
405
|
});
|
|
401
406
|
|
|
402
|
-
return {
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
return jsonContent({
|
|
408
|
+
action: 'column_deleted',
|
|
409
|
+
tableId,
|
|
410
|
+
columnId,
|
|
411
|
+
result,
|
|
412
|
+
});
|
|
405
413
|
});
|
|
406
414
|
}
|
|
407
415
|
|
|
@@ -409,7 +417,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
409
417
|
return withSchemaQueue(async () => {
|
|
410
418
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
411
419
|
if (!tableData) {
|
|
412
|
-
|
|
420
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
413
421
|
}
|
|
414
422
|
|
|
415
423
|
const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
|
|
@@ -442,9 +450,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
442
450
|
relationId,
|
|
443
451
|
});
|
|
444
452
|
|
|
445
|
-
return {
|
|
446
|
-
|
|
447
|
-
|
|
453
|
+
return jsonContent({
|
|
454
|
+
action: 'relation_deleted',
|
|
455
|
+
tableId,
|
|
456
|
+
relationId,
|
|
457
|
+
result,
|
|
458
|
+
});
|
|
448
459
|
});
|
|
449
460
|
}
|
|
450
461
|
|
|
@@ -613,9 +624,25 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
613
624
|
indexes.length ? `Index group(s): ${indexes.length}.` : null,
|
|
614
625
|
uniques.length ? `Unique group(s): ${uniques.length}.` : null,
|
|
615
626
|
].filter(Boolean).join(' ');
|
|
616
|
-
return {
|
|
617
|
-
|
|
618
|
-
|
|
627
|
+
return jsonContent({
|
|
628
|
+
action: 'table_created',
|
|
629
|
+
table: { id: createdTableId, name, routePath },
|
|
630
|
+
summary: {
|
|
631
|
+
columnCount: userColumns.length + 1,
|
|
632
|
+
createdColumnCount: userColumns.length,
|
|
633
|
+
relationCount: userRelations.length,
|
|
634
|
+
indexGroupCount: indexes.length,
|
|
635
|
+
uniqueGroupCount: uniques.length,
|
|
636
|
+
},
|
|
637
|
+
rest: {
|
|
638
|
+
base,
|
|
639
|
+
routePath,
|
|
640
|
+
operations: ['GET /<table>', 'POST /<table>', 'PATCH /<table>/:id', 'DELETE /<table>/:id'],
|
|
641
|
+
noGetById: true,
|
|
642
|
+
},
|
|
643
|
+
message: [colHint, relHint, constraintHint, restHint].filter(Boolean).join('\n'),
|
|
644
|
+
result,
|
|
645
|
+
});
|
|
619
646
|
})
|
|
620
647
|
);
|
|
621
648
|
|
|
@@ -652,9 +679,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
652
679
|
if (uniquesJson !== undefined) body.uniques = normalizeConstraintGroups('uniques', parseJsonArrayParam('uniques', uniquesJson));
|
|
653
680
|
|
|
654
681
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, body);
|
|
655
|
-
return {
|
|
656
|
-
|
|
657
|
-
|
|
682
|
+
return jsonContent({
|
|
683
|
+
action: 'table_updated',
|
|
684
|
+
tableId,
|
|
685
|
+
result,
|
|
686
|
+
});
|
|
658
687
|
})
|
|
659
688
|
);
|
|
660
689
|
|
|
@@ -691,9 +720,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
691
720
|
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
|
|
692
721
|
method: 'DELETE',
|
|
693
722
|
});
|
|
694
|
-
return {
|
|
695
|
-
|
|
696
|
-
|
|
723
|
+
return jsonContent({
|
|
724
|
+
action: 'table_deleted',
|
|
725
|
+
tableId,
|
|
726
|
+
result,
|
|
727
|
+
});
|
|
697
728
|
})
|
|
698
729
|
);
|
|
699
730
|
|
|
@@ -741,7 +772,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
741
772
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
742
773
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
743
774
|
if (!tableData) {
|
|
744
|
-
|
|
775
|
+
throw new Error(`Table with ID ${tableId} not found.`);
|
|
745
776
|
}
|
|
746
777
|
|
|
747
778
|
const existingColumns = getPatchableColumns(tableData.columns);
|
|
@@ -771,9 +802,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
771
802
|
columnId,
|
|
772
803
|
});
|
|
773
804
|
|
|
774
|
-
return {
|
|
775
|
-
|
|
776
|
-
|
|
805
|
+
return jsonContent({
|
|
806
|
+
action: 'column_updated',
|
|
807
|
+
tableId,
|
|
808
|
+
columnId,
|
|
809
|
+
result,
|
|
810
|
+
});
|
|
777
811
|
})
|
|
778
812
|
);
|
|
779
813
|
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
export const WORKFLOW_SURFACES = [
|
|
2
|
+
'api-endpoint',
|
|
3
|
+
'extension',
|
|
4
|
+
'schema',
|
|
5
|
+
'record-data',
|
|
6
|
+
'dynamic-script',
|
|
7
|
+
'route-access',
|
|
8
|
+
'guards-permissions-rules',
|
|
9
|
+
'flow',
|
|
10
|
+
'websocket',
|
|
11
|
+
'graphql',
|
|
12
|
+
'package',
|
|
13
|
+
'cache',
|
|
14
|
+
'logs-debug',
|
|
15
|
+
'auth-context',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const ALL_DETAILS = ['summary', 'plan', 'full'];
|
|
19
|
+
|
|
20
|
+
export const TOOL_WORKFLOWS = [
|
|
21
|
+
{
|
|
22
|
+
key: 'api-endpoint',
|
|
23
|
+
title: 'Custom REST endpoint with handler',
|
|
24
|
+
useWhen: [
|
|
25
|
+
'Creating or changing a custom API path with handler behavior.',
|
|
26
|
+
'Changing public/private method access on a custom endpoint.',
|
|
27
|
+
'Adding route permissions for endpoint access.',
|
|
28
|
+
],
|
|
29
|
+
keywords: ['endpoint', 'api', 'route handler', 'handler', 'webhook', 'custom route', 'rest path'],
|
|
30
|
+
firstTools: ['get_enfyra_required_knowledge', 'discover_script_contexts', 'inspect_route'],
|
|
31
|
+
inspectTools: ['inspect_route', 'get_all_routes', 'trace_metadata_usage'],
|
|
32
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'discover_script_contexts'],
|
|
33
|
+
writeTools: ['api_endpoint_workflow', 'create_api_endpoint', 'enable_route', 'public_route_methods', 'private_route_methods', 'ensure_route_access'],
|
|
34
|
+
verifyTools: ['test_rest_endpoint', 'run_admin_test'],
|
|
35
|
+
avoidTools: [
|
|
36
|
+
{
|
|
37
|
+
tool: 'create_route',
|
|
38
|
+
when: 'a handler-backed endpoint or route permission plan is needed',
|
|
39
|
+
useInstead: 'api_endpoint_workflow',
|
|
40
|
+
reason: 'The workflow validates source, reads live route/handler/access state, applies one safe step at a time, and returns nextSteps.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
tool: 'create_table',
|
|
44
|
+
when: 'the user asked for custom behavior at an API path',
|
|
45
|
+
useInstead: 'api_endpoint_workflow',
|
|
46
|
+
reason: 'Tables create persisted data models; custom route handlers own behavior endpoints.',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
requiredAck: ['globalRulesAckKey', 'dynamicCodeAckKey when saving handler source'],
|
|
50
|
+
exampleCategories: ['handlers-hooks', 'permissions-rls'],
|
|
51
|
+
nextStepTemplate: [
|
|
52
|
+
'Inspect the route or run api_endpoint_workflow with apply=false.',
|
|
53
|
+
'Read required knowledge before apply/applyAll.',
|
|
54
|
+
'Apply one pending step or use applyAll only when the plan is fully understood.',
|
|
55
|
+
'Verify the endpoint with test_rest_endpoint or run_admin_test.',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'extension',
|
|
60
|
+
title: 'Admin extension, menu, shell notification, or account panel UI',
|
|
61
|
+
useWhen: [
|
|
62
|
+
'Creating or changing Enfyra admin page/widget/global extensions.',
|
|
63
|
+
'Adding menu entries, menu notification dots/counts, account panel rows, or shell actions.',
|
|
64
|
+
'Reviewing extension UI theme/layout/component usage.',
|
|
65
|
+
],
|
|
66
|
+
keywords: ['extension', 'menu', 'account panel', 'notification', 'chip', 'badge', 'sidebar', 'shell', 'page ui', 'widget'],
|
|
67
|
+
firstTools: ['get_enfyra_required_knowledge', 'get_extension_theme_contract', 'inspect_feature'],
|
|
68
|
+
inspectTools: ['inspect_feature', 'trace_metadata_usage', 'get_script_source'],
|
|
69
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'get_extension_theme_contract', 'get_theme_class_reference'],
|
|
70
|
+
writeTools: ['ensure_menu', 'ensure_page_extension', 'ensure_global_extension', 'ensure_widget_extension'],
|
|
71
|
+
verifyTools: ['validate_extension_code', 'inspect_feature'],
|
|
72
|
+
avoidTools: [
|
|
73
|
+
{
|
|
74
|
+
tool: 'create_record/update_record on enfyra_extension',
|
|
75
|
+
when: 'creating or changing extension code',
|
|
76
|
+
useInstead: 'ensure_page_extension, ensure_global_extension, or ensure_widget_extension',
|
|
77
|
+
reason: 'Ensure tools validate extension code and preserve extension/menu contracts before saving.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
tool: 'query_table on destination domain lists',
|
|
81
|
+
when: 'decorating menu/account panel notifications',
|
|
82
|
+
useInstead: 'notification summary/realtime shell signal plus destination-page fetch on click',
|
|
83
|
+
reason: 'Shell notifications should not fetch messages, tickets, orders, or jobs lists solely for a badge.',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
requiredAck: ['globalRulesAckKey', 'extensionAckKey when saving extension code'],
|
|
87
|
+
exampleCategories: ['extensions'],
|
|
88
|
+
nextStepTemplate: [
|
|
89
|
+
'Call get_extension_theme_contract before writing or reviewing UI.',
|
|
90
|
+
'Inspect the existing menu/extension/global shell registration.',
|
|
91
|
+
'Choose count only when the source already owns an exact count; choose dot/chip for new-attention signals.',
|
|
92
|
+
'Validate extension code or use an ensure_*_extension tool that validates before saving.',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'schema',
|
|
97
|
+
title: 'Table, column, relation, validation, or schema metadata change',
|
|
98
|
+
useWhen: [
|
|
99
|
+
'Creating or changing tables, columns, relations, indexes, or validation metadata.',
|
|
100
|
+
'Designing relation direction or inverse relation exposure.',
|
|
101
|
+
'Changing GraphQL/table schema flags alongside metadata.',
|
|
102
|
+
],
|
|
103
|
+
keywords: ['schema', 'table', 'column', 'relation', 'field', 'index', 'validation', 'inverse'],
|
|
104
|
+
firstTools: ['get_enfyra_required_knowledge', 'inspect_table', 'get_all_tables'],
|
|
105
|
+
inspectTools: ['inspect_table', 'get_table_metadata', 'get_all_tables'],
|
|
106
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'get_enfyra_examples'],
|
|
107
|
+
writeTools: ['create_table', 'update_table', 'delete_table', 'create_column', 'update_column', 'delete_column', 'create_relation', 'delete_relation', 'ensure_column_rule'],
|
|
108
|
+
verifyTools: ['inspect_table', 'get_table_metadata'],
|
|
109
|
+
avoidTools: [
|
|
110
|
+
{
|
|
111
|
+
tool: 'create_record/update_record on enfyra_column or enfyra_relation',
|
|
112
|
+
when: 'changing schema metadata',
|
|
113
|
+
useInstead: 'table/column/relation schema tools',
|
|
114
|
+
reason: 'Schema tools resolve table ids, preserve relation contracts, and reject physical FK names.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
tool: 'manual inversePropertyName',
|
|
118
|
+
when: 'there is no concrete response/UI/deep-query/aggregate need for reverse traversal',
|
|
119
|
+
useInstead: 'owning relation only',
|
|
120
|
+
reason: 'Relation design stays minimal unless the reverse field is actually used.',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
requiredAck: ['globalRulesAckKey'],
|
|
124
|
+
exampleCategories: ['schema-relations'],
|
|
125
|
+
nextStepTemplate: [
|
|
126
|
+
'Inspect the existing table and relations.',
|
|
127
|
+
'Decide owner relation and whether an inverse is actually needed.',
|
|
128
|
+
'Apply schema tool changes with globalRulesAckKey.',
|
|
129
|
+
'Re-inspect metadata instead of assuming the saved shape.',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'record-data',
|
|
134
|
+
title: 'Route-backed table data query or CRUD',
|
|
135
|
+
useWhen: [
|
|
136
|
+
'Reading or mutating normal route-backed records.',
|
|
137
|
+
'Counting records or finding one row by filter.',
|
|
138
|
+
'Testing filters, fields, deep relations, pagination, or aggregate query shape.',
|
|
139
|
+
],
|
|
140
|
+
keywords: ['record', 'crud', 'query', 'count', 'filter', 'aggregate', 'deep', 'sort', 'pagination'],
|
|
141
|
+
firstTools: ['inspect_table', 'discover_query_capabilities'],
|
|
142
|
+
inspectTools: ['inspect_table', 'get_table_metadata', 'discover_query_capabilities'],
|
|
143
|
+
knowledgeTools: ['get_enfyra_required_knowledge for writes'],
|
|
144
|
+
writeTools: ['create_record', 'update_record', 'delete_record'],
|
|
145
|
+
verifyTools: ['find_one_record', 'query_table', 'count_records'],
|
|
146
|
+
avoidTools: [
|
|
147
|
+
{
|
|
148
|
+
tool: 'query_table without limit or all=true',
|
|
149
|
+
when: 'listing records',
|
|
150
|
+
useInstead: 'query_table with a bounded limit or all=true for intentional complete reads',
|
|
151
|
+
reason: 'List/query tools require explicit paging intent.',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
tool: 'generic CRUD on internal/no-route system tables',
|
|
155
|
+
when: 'changing schema, sessions, columns, or other no-route internals',
|
|
156
|
+
useInstead: 'specific schema/platform tools',
|
|
157
|
+
reason: 'Generic CRUD is for route-backed tables only.',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
requiredAck: ['globalRulesAckKey for writes', 'dynamicCodeAckKey for script-backed sourceCode writes', 'extensionAckKey for extension code writes'],
|
|
161
|
+
exampleCategories: ['queries-deep'],
|
|
162
|
+
nextStepTemplate: [
|
|
163
|
+
'Inspect table metadata and choose fields explicitly.',
|
|
164
|
+
'Use bounded pagination or all=true deliberately.',
|
|
165
|
+
'For writes, read required knowledge and use metadata-backed field names only.',
|
|
166
|
+
'Re-read with explicit fields after mutation when saved shape matters.',
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'dynamic-script',
|
|
171
|
+
title: 'Dynamic server code: handlers, hooks, scripts, or source patches',
|
|
172
|
+
useWhen: [
|
|
173
|
+
'Writing or reviewing handler, hook, flow step, websocket, GraphQL, or bootstrap sourceCode.',
|
|
174
|
+
'Editing an existing script-backed metadata record.',
|
|
175
|
+
'Debugging macro, repository, or validation behavior.',
|
|
176
|
+
],
|
|
177
|
+
keywords: ['sourcecode', 'script', 'hook', 'pre hook', 'post hook', 'compiledcode', 'macro', 'repos', 'bootstrap'],
|
|
178
|
+
firstTools: ['get_enfyra_required_knowledge', 'discover_script_contexts', 'trace_metadata_usage'],
|
|
179
|
+
inspectTools: ['trace_metadata_usage', 'get_script_source', 'discover_script_contexts'],
|
|
180
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'discover_script_contexts'],
|
|
181
|
+
writeTools: ['patch_script_source', 'update_script_source', 'create_handler', 'create_pre_hook', 'create_post_hook', 'api_endpoint_workflow'],
|
|
182
|
+
verifyTools: ['validate_dynamic_script', 'run_admin_test', 'test_rest_endpoint', 'test_flow_step'],
|
|
183
|
+
avoidTools: [
|
|
184
|
+
{
|
|
185
|
+
tool: 'update_record with compiledCode',
|
|
186
|
+
when: 'editing dynamic scripts',
|
|
187
|
+
useInstead: 'patch_script_source or update_script_source with sourceCode',
|
|
188
|
+
reason: 'compiledCode is generated and may differ because macros expand.',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
tool: 'throw new Error for intentional user/domain failures',
|
|
192
|
+
when: 'writing generated dynamic server code',
|
|
193
|
+
useInstead: '@THROW400-style macros or native $ctx.$throw helpers',
|
|
194
|
+
reason: 'Intentional domain errors should use the platform error contract.',
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
requiredAck: ['globalRulesAckKey', 'dynamicCodeAckKey'],
|
|
198
|
+
exampleCategories: ['handlers-hooks'],
|
|
199
|
+
nextStepTemplate: [
|
|
200
|
+
'Discover script context macros for the surface.',
|
|
201
|
+
'Read existing source through trace_metadata_usage/get_script_source when patching.',
|
|
202
|
+
'Validate source before save unless the chosen write tool already validates.',
|
|
203
|
+
'Verify behavior with the route/test runner that matches the script surface.',
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: 'route-access',
|
|
208
|
+
title: 'Authenticated route access and public/private method state',
|
|
209
|
+
useWhen: [
|
|
210
|
+
'Debugging 403/401 route access.',
|
|
211
|
+
'Granting role/user access to a route method.',
|
|
212
|
+
'Changing publicMethods or availableMethods.',
|
|
213
|
+
],
|
|
214
|
+
keywords: ['permission', '403', '401', 'role', 'route access', 'public method', 'private method', 'available method'],
|
|
215
|
+
firstTools: ['get_permission_profile', 'inspect_route', 'audit_route_access'],
|
|
216
|
+
inspectTools: ['get_permission_profile', 'inspect_route', 'audit_route_access'],
|
|
217
|
+
knowledgeTools: ['get_enfyra_required_knowledge'],
|
|
218
|
+
writeTools: ['ensure_route_access', 'add_route_methods', 'replace_route_methods', 'remove_route_methods', 'public_route_methods', 'private_route_methods', 'enable_route', 'disable_route'],
|
|
219
|
+
verifyTools: ['audit_route_access', 'test_rest_endpoint'],
|
|
220
|
+
avoidTools: [
|
|
221
|
+
{
|
|
222
|
+
tool: 'raw enfyra_route_permission CRUD',
|
|
223
|
+
when: 'granting route access',
|
|
224
|
+
useInstead: 'ensure_route_access',
|
|
225
|
+
reason: 'The operation tool resolves roles/methods and merges existing method grants safely.',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
tool: 'public_route_methods',
|
|
229
|
+
when: 'the desired behavior is authenticated user access',
|
|
230
|
+
useInstead: 'ensure_route_access',
|
|
231
|
+
reason: 'publicMethods grants anonymous access; route permissions grant authenticated access.',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
requiredAck: ['globalRulesAckKey for writes'],
|
|
235
|
+
exampleCategories: ['permissions-rls'],
|
|
236
|
+
nextStepTemplate: [
|
|
237
|
+
'Inspect route and permission profile before changing access.',
|
|
238
|
+
'Decide anonymous publicMethods versus authenticated route permission.',
|
|
239
|
+
'Use route operation tools instead of raw permission CRUD.',
|
|
240
|
+
'Audit and test the route after the change.',
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: 'guards-permissions-rules',
|
|
245
|
+
title: 'Guards, field permissions, and column validation rules',
|
|
246
|
+
useWhen: [
|
|
247
|
+
'Adding route guards, guard rules, field permissions, or column rules.',
|
|
248
|
+
'Restricting field read/write behavior.',
|
|
249
|
+
'Adding body validation rules at metadata level.',
|
|
250
|
+
],
|
|
251
|
+
keywords: ['guard', 'field permission', 'column rule', 'validation rule', 'rule', 'rls'],
|
|
252
|
+
firstTools: ['get_enfyra_required_knowledge', 'inspect_table', 'inspect_route'],
|
|
253
|
+
inspectTools: ['inspect_table', 'inspect_route', 'discover_query_capabilities'],
|
|
254
|
+
knowledgeTools: ['get_enfyra_required_knowledge'],
|
|
255
|
+
writeTools: ['ensure_guard', 'ensure_field_permission', 'ensure_column_rule'],
|
|
256
|
+
verifyTools: ['test_rest_endpoint', 'query_table', 'run_admin_test'],
|
|
257
|
+
avoidTools: [
|
|
258
|
+
{
|
|
259
|
+
tool: 'raw create_record on guard/rule tables',
|
|
260
|
+
when: 'a dedicated ensure_* operation exists',
|
|
261
|
+
useInstead: 'ensure_guard, ensure_field_permission, or ensure_column_rule',
|
|
262
|
+
reason: 'Ensure tools resolve ids and preserve the current rule contract.',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
requiredAck: ['globalRulesAckKey'],
|
|
266
|
+
exampleCategories: ['permissions-rls', 'schema-relations'],
|
|
267
|
+
nextStepTemplate: [
|
|
268
|
+
'Inspect the target table/route and decide the security boundary first.',
|
|
269
|
+
'Use the specific ensure_* operation for the rule surface.',
|
|
270
|
+
'Verify with the route/query behavior the rule is meant to protect.',
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: 'flow',
|
|
275
|
+
title: 'Flow, scheduled/manual flow, or flow step',
|
|
276
|
+
useWhen: [
|
|
277
|
+
'Creating or changing manual/scheduled flows.',
|
|
278
|
+
'Choosing or writing a flow step.',
|
|
279
|
+
'Testing or triggering a flow.',
|
|
280
|
+
],
|
|
281
|
+
keywords: ['flow', 'scheduled', 'manual flow', 'flow step', 'trigger flow', 'workflow'],
|
|
282
|
+
firstTools: ['get_enfyra_required_knowledge', 'choose_flow_step_tool', 'discover_script_contexts'],
|
|
283
|
+
inspectTools: ['inspect_feature', 'query_table'],
|
|
284
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'discover_script_contexts'],
|
|
285
|
+
writeTools: ['ensure_manual_flow', 'ensure_scheduled_flow', 'ensure_query_flow_step', 'ensure_create_flow_step', 'ensure_update_flow_step', 'ensure_delete_flow_step', 'ensure_http_flow_step', 'ensure_sleep_flow_step', 'ensure_trigger_flow_step', 'ensure_log_flow_step', 'ensure_condition_flow_step', 'ensure_script_flow_step'],
|
|
286
|
+
verifyTools: ['test_flow_step', 'run_admin_test', 'trigger_flow'],
|
|
287
|
+
avoidTools: [
|
|
288
|
+
{
|
|
289
|
+
tool: 'ensure_script_flow_step',
|
|
290
|
+
when: 'a fixed query/create/update/delete/http/sleep/trigger/log step can express the operation',
|
|
291
|
+
useInstead: 'choose_flow_step_tool then the fixed-type ensure_*_flow_step',
|
|
292
|
+
reason: 'Atomic step types are easier to inspect, test, and maintain than oversized scripts.',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
requiredAck: ['globalRulesAckKey', 'dynamicCodeAckKey for script or condition source'],
|
|
296
|
+
exampleCategories: ['flows'],
|
|
297
|
+
nextStepTemplate: [
|
|
298
|
+
'Use choose_flow_step_tool before mutating when step type is unclear.',
|
|
299
|
+
'Prefer fixed-type flow step tools over script steps.',
|
|
300
|
+
'Validate/test script or condition steps before relying on the flow.',
|
|
301
|
+
'Trigger manually only after the saved steps are verified.',
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: 'websocket',
|
|
306
|
+
title: 'Socket.IO gateway or websocket event',
|
|
307
|
+
useWhen: [
|
|
308
|
+
'Creating or changing websocket gateways/events.',
|
|
309
|
+
'Writing websocket handler source.',
|
|
310
|
+
'Testing websocket event logic.',
|
|
311
|
+
],
|
|
312
|
+
keywords: ['websocket', 'socket', 'socket.io', 'gateway', 'realtime', 'room'],
|
|
313
|
+
firstTools: ['get_enfyra_required_knowledge', 'discover_script_contexts', 'inspect_feature'],
|
|
314
|
+
inspectTools: ['inspect_feature', 'discover_script_contexts'],
|
|
315
|
+
knowledgeTools: ['get_enfyra_required_knowledge', 'discover_script_contexts'],
|
|
316
|
+
writeTools: ['ensure_websocket_gateway', 'ensure_websocket_event'],
|
|
317
|
+
verifyTools: ['run_admin_test'],
|
|
318
|
+
avoidTools: [
|
|
319
|
+
{
|
|
320
|
+
tool: 'raw CRUD on enfyra_websocket_event',
|
|
321
|
+
when: 'saving websocket event source',
|
|
322
|
+
useInstead: 'ensure_websocket_event',
|
|
323
|
+
reason: 'The operation tool validates script source and preserves gateway/event linkage.',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
requiredAck: ['globalRulesAckKey', 'dynamicCodeAckKey when saving event source'],
|
|
327
|
+
exampleCategories: ['websocket'],
|
|
328
|
+
nextStepTemplate: [
|
|
329
|
+
'Discover websocket script context before writing source.',
|
|
330
|
+
'Ensure gateway first, then event.',
|
|
331
|
+
'Use run_admin_test for event/connection scripts where possible.',
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: 'graphql',
|
|
336
|
+
title: 'GraphQL enablement and query surface',
|
|
337
|
+
useWhen: [
|
|
338
|
+
'Enabling/disabling GraphQL for a table.',
|
|
339
|
+
'Checking GraphQL endpoint/schema behavior.',
|
|
340
|
+
'Clarifying REST route methods versus GraphQL table exposure.',
|
|
341
|
+
],
|
|
342
|
+
keywords: ['graphql', 'gql', 'schema endpoint'],
|
|
343
|
+
firstTools: ['discover_enfyra_system', 'inspect_table'],
|
|
344
|
+
inspectTools: ['discover_enfyra_system', 'inspect_table'],
|
|
345
|
+
knowledgeTools: ['get_enfyra_required_knowledge for writes'],
|
|
346
|
+
writeTools: ['set_table_graphql', 'update_table'],
|
|
347
|
+
verifyTools: ['reload_graphql', 'discover_enfyra_system'],
|
|
348
|
+
avoidTools: [
|
|
349
|
+
{
|
|
350
|
+
tool: 'public_route_methods',
|
|
351
|
+
when: 'trying to expose GraphQL table data',
|
|
352
|
+
useInstead: 'set_table_graphql plus GraphQL auth planning',
|
|
353
|
+
reason: 'REST publicMethods do not make GraphQL table data anonymous.',
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
requiredAck: ['globalRulesAckKey for writes'],
|
|
357
|
+
exampleCategories: ['queries-deep'],
|
|
358
|
+
nextStepTemplate: [
|
|
359
|
+
'Inspect the table and GraphQL enablement state.',
|
|
360
|
+
'Use set_table_graphql for enablement changes.',
|
|
361
|
+
'Remember GraphQL table data requires Bearer auth even when REST is public.',
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
key: 'package',
|
|
366
|
+
title: 'Runtime package install or package-backed extension/script support',
|
|
367
|
+
useWhen: [
|
|
368
|
+
'Installing npm packages for dynamic code or extension runtime use.',
|
|
369
|
+
'Checking package availability before using getPackages in extensions.',
|
|
370
|
+
],
|
|
371
|
+
keywords: ['package', 'npm', 'install', 'dependency', 'getpackages'],
|
|
372
|
+
firstTools: ['search_npm', 'get_enfyra_required_knowledge'],
|
|
373
|
+
inspectTools: ['search_npm', 'query_table'],
|
|
374
|
+
knowledgeTools: ['get_enfyra_required_knowledge'],
|
|
375
|
+
writeTools: ['install_package'],
|
|
376
|
+
verifyTools: ['query_table'],
|
|
377
|
+
avoidTools: [
|
|
378
|
+
{
|
|
379
|
+
tool: 'raw create_record on enfyra_package',
|
|
380
|
+
when: 'installing packages',
|
|
381
|
+
useInstead: 'install_package',
|
|
382
|
+
reason: 'The package tool resolves package metadata and avoids duplicate package records.',
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
requiredAck: ['globalRulesAckKey'],
|
|
386
|
+
exampleCategories: ['extensions', 'handlers-hooks'],
|
|
387
|
+
nextStepTemplate: [
|
|
388
|
+
'Search package metadata first.',
|
|
389
|
+
'Install with install_package and globalRulesAckKey.',
|
|
390
|
+
'Use getPackages inside extension runtime code rather than static imports.',
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
key: 'cache',
|
|
395
|
+
title: 'Cache reload or stale metadata/runtime diagnosis',
|
|
396
|
+
useWhen: [
|
|
397
|
+
'Diagnosing stale routes, metadata, GraphQL, or full runtime cache.',
|
|
398
|
+
'Manually reloading only after natural partial reload appears stale.',
|
|
399
|
+
],
|
|
400
|
+
keywords: ['cache', 'reload', 'stale', 'refresh metadata', 'reload routes'],
|
|
401
|
+
firstTools: ['inspect_table', 'inspect_route', 'get_enfyra_api_context'],
|
|
402
|
+
inspectTools: ['inspect_table', 'inspect_route', 'get_enfyra_api_context'],
|
|
403
|
+
knowledgeTools: ['get_enfyra_required_knowledge for manual reloads'],
|
|
404
|
+
writeTools: ['reload_metadata', 'reload_routes', 'reload_graphql', 'reload_all'],
|
|
405
|
+
verifyTools: ['inspect_table', 'inspect_route', 'discover_enfyra_system'],
|
|
406
|
+
avoidTools: [
|
|
407
|
+
{
|
|
408
|
+
tool: 'manual reload tools',
|
|
409
|
+
when: 'a successful metadata mutation already triggered natural partial reload and no stale evidence exists',
|
|
410
|
+
useInstead: 'verify behavior first with inspect/test tools',
|
|
411
|
+
reason: 'Manual reloads should be evidence-driven, not reflexive.',
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
requiredAck: ['globalRulesAckKey for manual reload tools'],
|
|
415
|
+
exampleCategories: [],
|
|
416
|
+
nextStepTemplate: [
|
|
417
|
+
'Verify stale behavior with narrow inspect/test tools.',
|
|
418
|
+
'Choose the narrowest reload surface if stale evidence exists.',
|
|
419
|
+
'Re-verify the same narrow behavior after reload.',
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
key: 'logs-debug',
|
|
424
|
+
title: 'Logs, runtime diagnostics, and test runner debugging',
|
|
425
|
+
useWhen: [
|
|
426
|
+
'Reading app/error logs.',
|
|
427
|
+
'Searching logs for route, flow, websocket, or dynamic script failures.',
|
|
428
|
+
'Running admin tests for supported runtime surfaces.',
|
|
429
|
+
],
|
|
430
|
+
keywords: ['log', 'debug', 'error', 'trace', 'tail', 'diagnostic', 'test runner'],
|
|
431
|
+
firstTools: ['get_log_files', 'search_logs'],
|
|
432
|
+
inspectTools: ['get_log_files', 'get_log_content', 'tail_log', 'search_logs'],
|
|
433
|
+
knowledgeTools: [],
|
|
434
|
+
writeTools: [],
|
|
435
|
+
verifyTools: ['run_admin_test', 'test_flow_step', 'test_rest_endpoint'],
|
|
436
|
+
avoidTools: [
|
|
437
|
+
{
|
|
438
|
+
tool: 'broad metadata discovery',
|
|
439
|
+
when: 'the problem is a concrete runtime error with known log text',
|
|
440
|
+
useInstead: 'search_logs or tail_log',
|
|
441
|
+
reason: 'Log tools are narrower and cheaper for runtime debugging.',
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
requiredAck: [],
|
|
445
|
+
exampleCategories: [],
|
|
446
|
+
nextStepTemplate: [
|
|
447
|
+
'Search or tail the narrowest log first.',
|
|
448
|
+
'Use the matching test tool to reproduce once the failing surface is known.',
|
|
449
|
+
'Patch only after the failing step is identified.',
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
key: 'auth-context',
|
|
454
|
+
title: 'MCP target, auth token, current user, and permission profile',
|
|
455
|
+
useWhen: [
|
|
456
|
+
'Confirming which Enfyra instance MCP is connected to.',
|
|
457
|
+
'Debugging API-token exchange or current MCP user permissions.',
|
|
458
|
+
'Checking whether non-root tokens can call admin helper routes.',
|
|
459
|
+
],
|
|
460
|
+
keywords: ['auth', 'token', 'current user', 'permission profile', 'target', 'api base', '403', 'exchange'],
|
|
461
|
+
firstTools: ['get_enfyra_api_context', 'get_current_user', 'get_permission_profile'],
|
|
462
|
+
inspectTools: ['get_enfyra_api_context', 'get_current_user', 'get_permission_profile'],
|
|
463
|
+
knowledgeTools: [],
|
|
464
|
+
writeTools: ['login'],
|
|
465
|
+
verifyTools: ['get_current_user', 'get_permission_profile'],
|
|
466
|
+
avoidTools: [
|
|
467
|
+
{
|
|
468
|
+
tool: 'discover_enfyra_system',
|
|
469
|
+
when: 'only confirming the connected API base',
|
|
470
|
+
useInstead: 'get_enfyra_api_context',
|
|
471
|
+
reason: 'Target sanity checks should be cheap and should not load broad metadata.',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
requiredAck: [],
|
|
475
|
+
exampleCategories: ['ssr-app-auth', 'oauth-setup'],
|
|
476
|
+
nextStepTemplate: [
|
|
477
|
+
'Use get_enfyra_api_context for target sanity checks.',
|
|
478
|
+
'Use get_permission_profile before assuming admin helper route access with non-root tokens.',
|
|
479
|
+
'Use login only when an interactive credential login is explicitly needed.',
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
function compactWorkflow(workflow) {
|
|
485
|
+
return {
|
|
486
|
+
key: workflow.key,
|
|
487
|
+
title: workflow.title,
|
|
488
|
+
useWhen: workflow.useWhen,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function planWorkflow(workflow) {
|
|
493
|
+
return {
|
|
494
|
+
...compactWorkflow(workflow),
|
|
495
|
+
firstTools: workflow.firstTools,
|
|
496
|
+
inspectTools: workflow.inspectTools,
|
|
497
|
+
knowledgeTools: workflow.knowledgeTools,
|
|
498
|
+
writeTools: workflow.writeTools,
|
|
499
|
+
verifyTools: workflow.verifyTools,
|
|
500
|
+
requiredAck: workflow.requiredAck,
|
|
501
|
+
exampleCategories: workflow.exampleCategories,
|
|
502
|
+
nextSteps: workflow.nextStepTemplate,
|
|
503
|
+
avoidTools: workflow.avoidTools,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function fullWorkflow(workflow) {
|
|
508
|
+
return {
|
|
509
|
+
...planWorkflow(workflow),
|
|
510
|
+
keywords: workflow.keywords,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function normalize(value) {
|
|
515
|
+
return String(value || '').trim().toLowerCase();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function scoreWorkflow(workflow, { intent, surface, risk }) {
|
|
519
|
+
let score = 0;
|
|
520
|
+
const text = normalize(intent);
|
|
521
|
+
if (surface && workflow.key === surface) score += 20;
|
|
522
|
+
if (surface && workflow.key.includes(surface)) score += 8;
|
|
523
|
+
if (text) {
|
|
524
|
+
for (const keyword of workflow.keywords || []) {
|
|
525
|
+
if (text.includes(keyword)) score += 4;
|
|
526
|
+
}
|
|
527
|
+
for (const phrase of workflow.useWhen || []) {
|
|
528
|
+
const words = normalize(phrase).split(/\W+/).filter((word) => word.length > 3);
|
|
529
|
+
if (words.some((word) => text.includes(word))) score += 1;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (risk === 'debug' && workflow.key === 'logs-debug') score += 6;
|
|
533
|
+
if (risk === 'write' && workflow.writeTools.length) score += 2;
|
|
534
|
+
if (risk === 'destructive' && workflow.avoidTools.some((item) => normalize(item.when).includes('delete'))) score += 2;
|
|
535
|
+
return score;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function listWorkflowSurfaces() {
|
|
539
|
+
return TOOL_WORKFLOWS.map(compactWorkflow);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function discoverWorkflowRoutes({
|
|
543
|
+
intent = '',
|
|
544
|
+
surface,
|
|
545
|
+
risk = 'unknown',
|
|
546
|
+
detail = 'summary',
|
|
547
|
+
limit = 5,
|
|
548
|
+
} = {}) {
|
|
549
|
+
const normalizedSurface = surface ? normalize(surface) : undefined;
|
|
550
|
+
const normalizedDetail = ALL_DETAILS.includes(detail) ? detail : 'summary';
|
|
551
|
+
const normalizedRisk = normalize(risk) || 'unknown';
|
|
552
|
+
const formatter = normalizedDetail === 'full'
|
|
553
|
+
? fullWorkflow
|
|
554
|
+
: normalizedDetail === 'plan'
|
|
555
|
+
? planWorkflow
|
|
556
|
+
: compactWorkflow;
|
|
557
|
+
const scored = TOOL_WORKFLOWS
|
|
558
|
+
.map((workflow) => ({ workflow, score: scoreWorkflow(workflow, { intent, surface: normalizedSurface, risk: normalizedRisk }) }))
|
|
559
|
+
.filter((item) => !normalizedSurface || item.workflow.key === normalizedSurface || item.score > 0)
|
|
560
|
+
.sort((a, b) => b.score - a.score || a.workflow.key.localeCompare(b.workflow.key));
|
|
561
|
+
const selected = (scored.length ? scored : TOOL_WORKFLOWS.map((workflow) => ({ workflow, score: 0 })))
|
|
562
|
+
.slice(0, Math.max(1, Math.min(Number(limit) || 5, 10)));
|
|
563
|
+
return {
|
|
564
|
+
action: 'enfyra_workflows_discovered',
|
|
565
|
+
intent: intent || null,
|
|
566
|
+
requestedSurface: surface || null,
|
|
567
|
+
risk: normalizedRisk,
|
|
568
|
+
detail: normalizedDetail,
|
|
569
|
+
matchedWorkflowCount: scored.length,
|
|
570
|
+
workflows: selected.map((item) => ({
|
|
571
|
+
score: item.score,
|
|
572
|
+
...formatter(item.workflow),
|
|
573
|
+
})),
|
|
574
|
+
surfaces: normalizedDetail === 'summary' ? WORKFLOW_SURFACES : undefined,
|
|
575
|
+
guidance: [
|
|
576
|
+
'Use this as progressive disclosure: pick the closest workflow, then call its firstTools instead of loading every Enfyra tool/example.',
|
|
577
|
+
'For writes, call get_enfyra_required_knowledge and pass the returned acknowledgement keys into write tools.',
|
|
578
|
+
'Treat avoidTools as negative routing boundaries; they prevent near-correct tool choices from crossing the wrong platform contract.',
|
|
579
|
+
],
|
|
580
|
+
};
|
|
581
|
+
}
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { exchangeApiToken, refreshAccessToken, getValidToken, resetTokens, getTo
|
|
|
20
20
|
import { fetchAPI, validateFilter, validateTableName } from './lib/fetch.js';
|
|
21
21
|
import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instructions.js';
|
|
22
22
|
import { getExamples, listExampleCategories } from './lib/mcp-examples.js';
|
|
23
|
+
import { WORKFLOW_SURFACES, discoverWorkflowRoutes } from './lib/tool-routing.js';
|
|
23
24
|
import { registerTableTools } from './lib/table-tools.js';
|
|
24
25
|
import { registerPlatformOperationTools } from './lib/platform-operation-tools.js';
|
|
25
26
|
import { parseRecordData, prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
@@ -331,7 +332,7 @@ function summarizeRoutes(routesResult) {
|
|
|
331
332
|
}));
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
function summarizeMetadata(metadata, { search, limit } = {}) {
|
|
335
|
+
function summarizeMetadata(metadata, { search, limit, all = false } = {}) {
|
|
335
336
|
const tables = normalizeTables(metadata);
|
|
336
337
|
const q = search ? search.toLowerCase() : null;
|
|
337
338
|
const summarized = tables.map((table) => ({
|
|
@@ -346,11 +347,13 @@ function summarizeMetadata(metadata, { search, limit } = {}) {
|
|
|
346
347
|
const matched = q
|
|
347
348
|
? summarized.filter((table) => JSON.stringify(table).toLowerCase().includes(q))
|
|
348
349
|
: summarized;
|
|
349
|
-
const outputLimit = limit || 30;
|
|
350
|
+
const outputLimit = all ? matched.length : (limit || 30);
|
|
350
351
|
return {
|
|
351
352
|
tableCount: tables.length,
|
|
352
353
|
matchedTableCount: matched.length,
|
|
353
354
|
returnedTableCount: Math.min(matched.length, outputLimit),
|
|
355
|
+
complete: all || outputLimit >= matched.length,
|
|
356
|
+
hardCap: all ? null : outputLimit,
|
|
354
357
|
search: search || null,
|
|
355
358
|
tables: matched.slice(0, outputLimit),
|
|
356
359
|
};
|
|
@@ -807,15 +810,21 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
|
|
|
807
810
|
includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
|
|
808
811
|
search: z.string().optional().describe('Optional table-name/alias substring filter.'),
|
|
809
812
|
limit: z.number().optional().describe('Maximum tables returned after search. Default 30.'),
|
|
810
|
-
|
|
813
|
+
all: z.boolean().optional().default(false).describe('Return every matched table summary. Use when a complete table list is required.'),
|
|
814
|
+
}, async ({ includeFull, search, limit, all }) => {
|
|
815
|
+
if (all && limit !== undefined) {
|
|
816
|
+
throw new Error('get_all_metadata accepts either all=true or limit, not both.');
|
|
817
|
+
}
|
|
811
818
|
const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
812
819
|
const payload = includeFull
|
|
813
820
|
? result
|
|
814
821
|
: {
|
|
815
822
|
statusCode: result?.statusCode,
|
|
816
823
|
success: result?.success,
|
|
817
|
-
...summarizeMetadata(result, { search, limit }),
|
|
818
|
-
detailHint:
|
|
824
|
+
...summarizeMetadata(result, { search, limit, all }),
|
|
825
|
+
detailHint: all
|
|
826
|
+
? 'Complete summary returned. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.'
|
|
827
|
+
: 'Default response is capped and minimal. Pass all=true for a complete summary, or call get_table_metadata({ tableName }) / inspect_table({ tableName }) for detail.',
|
|
819
828
|
};
|
|
820
829
|
return jsonContent(payload);
|
|
821
830
|
});
|
|
@@ -852,6 +861,23 @@ server.tool(
|
|
|
852
861
|
},
|
|
853
862
|
);
|
|
854
863
|
|
|
864
|
+
server.tool(
|
|
865
|
+
'discover_enfyra_workflows',
|
|
866
|
+
[
|
|
867
|
+
'Progressive-disclosure router for the Enfyra MCP tool surface.',
|
|
868
|
+
'Call this when the task intent is clear but the right Enfyra tool path is not.',
|
|
869
|
+
'Returns matched workflows, first tools, required acknowledgement keys, verification tools, and avoidTools negative-routing boundaries.',
|
|
870
|
+
].join(' '),
|
|
871
|
+
{
|
|
872
|
+
intent: z.string().optional().describe('Plain-language task goal, e.g. "add a menu chip when support tickets arrive".'),
|
|
873
|
+
surface: z.enum(WORKFLOW_SURFACES).optional().describe('Known surface when the caller can classify the task. Omit to infer from intent.'),
|
|
874
|
+
risk: z.enum(['read', 'write', 'destructive', 'debug', 'unknown']).optional().default('unknown').describe('Highest expected operation risk.'),
|
|
875
|
+
detail: z.enum(['summary', 'plan', 'full']).optional().default('summary').describe('summary lists candidate workflows; plan adds tool sequence and avoidTools; full also includes matching keywords.'),
|
|
876
|
+
limit: z.number().int().positive().max(10).optional().default(5).describe('Maximum workflows to return.'),
|
|
877
|
+
},
|
|
878
|
+
async (input) => jsonContent(discoverWorkflowRoutes(input)),
|
|
879
|
+
);
|
|
880
|
+
|
|
855
881
|
server.tool(
|
|
856
882
|
'discover_enfyra_system',
|
|
857
883
|
[
|
|
@@ -2109,16 +2135,11 @@ server.tool(
|
|
|
2109
2135
|
tableName: z.string().describe('Table name or alias to inspect'),
|
|
2110
2136
|
},
|
|
2111
2137
|
async ({ tableName }) => {
|
|
2112
|
-
|
|
2113
|
-
|
|
2138
|
+
const state = await collectRestDefinitionState();
|
|
2139
|
+
const table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
|
|
2114
2140
|
if (!table) {
|
|
2115
|
-
|
|
2116
|
-
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
2117
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2118
|
-
state = await collectRestDefinitionState();
|
|
2119
|
-
table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
|
|
2141
|
+
throw new Error(`Unknown table "${tableName}". Use get_all_tables({ search, limit }) or get_all_metadata({ search, all: true }) to confirm the table name. If a just-created table is missing, verify the create response/reload event before calling manual reload tools.`);
|
|
2120
2142
|
}
|
|
2121
|
-
if (!table) throw new Error(`Unknown table "${tableName}"`);
|
|
2122
2143
|
const tableId = getId(table);
|
|
2123
2144
|
const columnIds = new Set((table.columns || []).map((column) => String(getId(column))));
|
|
2124
2145
|
const relationIds = new Set((table.relations || []).map((relation) => String(getId(relation))));
|
|
@@ -2420,7 +2441,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
|
|
|
2420
2441
|
const queryParams = new URLSearchParams({
|
|
2421
2442
|
filter: JSON.stringify(filter),
|
|
2422
2443
|
fields: 'id,path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled',
|
|
2423
|
-
limit: '1000',
|
|
2444
|
+
limit: all ? '0' : '1000',
|
|
2424
2445
|
});
|
|
2425
2446
|
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_route?${queryParams.toString()}`);
|
|
2426
2447
|
const q = search ? search.toLowerCase() : null;
|
|
@@ -2439,6 +2460,8 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
|
|
|
2439
2460
|
matchedRouteCount: matchedRoutes.length,
|
|
2440
2461
|
returnedRouteCount: Math.min(matchedRoutes.length, routeLimit),
|
|
2441
2462
|
all: !!all,
|
|
2463
|
+
complete: all || routeLimit >= matchedRoutes.length,
|
|
2464
|
+
hardCap: all ? null : routeLimit,
|
|
2442
2465
|
search: search || null,
|
|
2443
2466
|
routes: matchedRoutes.slice(0, routeLimit),
|
|
2444
2467
|
detailHint: matchedRoutes.length > routeLimit
|
|
@@ -2885,7 +2908,7 @@ server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {
|
|
|
2885
2908
|
}, async ({ globalRulesAckKey }) => {
|
|
2886
2909
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
2887
2910
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
|
|
2888
|
-
return {
|
|
2911
|
+
return jsonContent({ action: 'reloaded_all', result });
|
|
2889
2912
|
});
|
|
2890
2913
|
|
|
2891
2914
|
server.tool('reload_metadata', 'Reload metadata cache only', {
|
|
@@ -2893,7 +2916,7 @@ server.tool('reload_metadata', 'Reload metadata cache only', {
|
|
|
2893
2916
|
}, async ({ globalRulesAckKey }) => {
|
|
2894
2917
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
2895
2918
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
|
|
2896
|
-
return {
|
|
2919
|
+
return jsonContent({ action: 'reloaded_metadata', result });
|
|
2897
2920
|
});
|
|
2898
2921
|
|
|
2899
2922
|
server.tool('reload_routes', 'Reload routes cache only', {
|
|
@@ -2901,7 +2924,7 @@ server.tool('reload_routes', 'Reload routes cache only', {
|
|
|
2901
2924
|
}, async ({ globalRulesAckKey }) => {
|
|
2902
2925
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
2903
2926
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
|
|
2904
|
-
return {
|
|
2927
|
+
return jsonContent({ action: 'reloaded_routes', result });
|
|
2905
2928
|
});
|
|
2906
2929
|
|
|
2907
2930
|
server.tool('reload_graphql', 'Reload GraphQL schema', {
|
|
@@ -2909,7 +2932,7 @@ server.tool('reload_graphql', 'Reload GraphQL schema', {
|
|
|
2909
2932
|
}, async ({ globalRulesAckKey }) => {
|
|
2910
2933
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
2911
2934
|
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
|
|
2912
|
-
return {
|
|
2935
|
+
return jsonContent({ action: 'reloaded_graphql', result });
|
|
2913
2936
|
});
|
|
2914
2937
|
|
|
2915
2938
|
// ============================================================================
|
|
@@ -3080,12 +3103,15 @@ server.tool(
|
|
|
3080
3103
|
const checkFilter = JSON.stringify({ name: { _eq: name }, type: { _eq: type } });
|
|
3081
3104
|
const existing = await fetchAPI(ENFYRA_API_URL, `/enfyra_package?filter=${encodeURIComponent(checkFilter)}&limit=1`);
|
|
3082
3105
|
if (existing.data && existing.data.length > 0) {
|
|
3083
|
-
return {
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3106
|
+
return jsonContent({
|
|
3107
|
+
action: 'package_already_installed',
|
|
3108
|
+
package: {
|
|
3109
|
+
name,
|
|
3110
|
+
version: existing.data[0].version,
|
|
3111
|
+
type: existing.data[0].type,
|
|
3112
|
+
},
|
|
3113
|
+
record: existing.data[0],
|
|
3114
|
+
});
|
|
3089
3115
|
}
|
|
3090
3116
|
|
|
3091
3117
|
// Step 3: Get current user for installedBy
|
|
@@ -3107,12 +3133,11 @@ server.tool(
|
|
|
3107
3133
|
body: JSON.stringify(body),
|
|
3108
3134
|
});
|
|
3109
3135
|
|
|
3110
|
-
return {
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
};
|
|
3136
|
+
return jsonContent({
|
|
3137
|
+
action: 'package_installed',
|
|
3138
|
+
package: { name, version: pkgVersion, type },
|
|
3139
|
+
result,
|
|
3140
|
+
});
|
|
3116
3141
|
},
|
|
3117
3142
|
);
|
|
3118
3143
|
|