@enfyra/mcp-server 0.1.57 → 0.1.59
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 +46 -267
- package/dist/lib/dynamic-repository-builder.d.ts +18 -0
- package/dist/lib/dynamic-repository-builder.js +105 -0
- package/dist/lib/dynamic-repository-builder.js.map +1 -0
- package/dist/lib/mcp-examples.d.ts +9 -0
- package/dist/lib/mcp-examples.js +86 -19
- package/dist/lib/mcp-examples.js.map +1 -1
- package/dist/lib/mcp-instructions.js +1 -0
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/mutation-guards.js +3 -10
- package/dist/lib/mutation-guards.js.map +1 -1
- package/dist/lib/platform-operation-tools.d.ts +7 -0
- package/dist/lib/platform-operation-tools.js +113 -17
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.d.ts +1 -1
- package/dist/lib/required-knowledge.js +18 -13
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/runtime-zone-tools.js +8 -6
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/tool-routing.d.ts +64 -4
- package/dist/lib/tool-routing.js +136 -14
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.js +8 -0
- package/dist/lib/toolset-filter.js.map +1 -1
- package/dist/mcp-server-entry.js +69 -15
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +2 -2
|
@@ -416,8 +416,6 @@ export function buildExtensionDrawerSnippet(input) {
|
|
|
416
416
|
];
|
|
417
417
|
if (input.nested)
|
|
418
418
|
attrs.push('nested');
|
|
419
|
-
if (input.handleOnly)
|
|
420
|
-
attrs.push('handle-only');
|
|
421
419
|
const cancelAction = input.cancelAction === false
|
|
422
420
|
? null
|
|
423
421
|
: buildFooterActionObject(input.cancelAction || { label: 'Cancel', onClick: `() => (${model} = false)` });
|
|
@@ -524,6 +522,10 @@ function attrStaticOrBound(name, value, expression) {
|
|
|
524
522
|
return `${name}="${String(value).replace(/"/g, '"')}"`;
|
|
525
523
|
}
|
|
526
524
|
function buildHeaderActionLiteral(action) {
|
|
525
|
+
const bareAssignment = String(action.onClick || '').match(/^\s*\(\s*\)\s*=>\s*\(?\s*([A-Za-z_$][\w$]*)\s*=(?!=)/);
|
|
526
|
+
if (bareAssignment) {
|
|
527
|
+
throw new Error(`Invalid header action onClick: assign ${bareAssignment[1]}.value inside script callbacks, or pass a handler name. Template ref auto-unwrapping does not apply in registry callbacks.`);
|
|
528
|
+
}
|
|
527
529
|
const entries = [
|
|
528
530
|
action.id ? `id: ${quoteJsString(action.id)}` : null,
|
|
529
531
|
action.label ? `label: ${quoteJsString(action.label)}` : null,
|
|
@@ -555,14 +557,14 @@ export function buildExtensionPageShellSnippet(input) {
|
|
|
555
557
|
];
|
|
556
558
|
if (actions.length) {
|
|
557
559
|
lines.push('const { register: registerHeaderActions } = useHeaderActionRegistry();');
|
|
558
|
-
lines.push(`registerHeaderActions([\n${actions.map((action) => `
|
|
560
|
+
lines.push(`onMounted(() => {\n registerHeaderActions([\n${actions.map((action) => ` ${buildHeaderActionLiteral(action)}`).join(',\n')}\n ]);\n});`);
|
|
559
561
|
}
|
|
560
562
|
return {
|
|
561
563
|
action: 'extension_page_shell_built',
|
|
562
564
|
snippet: lines.join('\n'),
|
|
563
565
|
contract: [
|
|
564
566
|
'Use usePageHeaderRegistry so the app shell renders the page header.',
|
|
565
|
-
'Use useHeaderActionRegistry for toolbar actions instead of rendering duplicate page headers or local top bars.',
|
|
567
|
+
'Use useHeaderActionRegistry for toolbar actions instead of rendering duplicate page headers or local top bars; register dynamic extension actions in onMounted after setup state exists.',
|
|
566
568
|
'Use primary solid only for the main scope action; secondary actions default to neutral outline.',
|
|
567
569
|
],
|
|
568
570
|
};
|
|
@@ -603,10 +605,10 @@ export function buildExtensionEmptyStateSnippet(input) {
|
|
|
603
605
|
: '';
|
|
604
606
|
return {
|
|
605
607
|
action: 'extension_empty_state_built',
|
|
606
|
-
component: '
|
|
607
|
-
snippet: `<
|
|
608
|
+
component: 'EmptyState',
|
|
609
|
+
snippet: `<EmptyState\n title="${String(input.title || 'No items found').replace(/"/g, '"')}"\n description="${String(input.description || '').replace(/"/g, '"')}"\n icon="${input.icon || 'lucide:inbox'}"\n size="${input.size || 'sm'}"\n variant="${input.variant || 'naked'}"${action}\n/>`,
|
|
608
610
|
contract: [
|
|
609
|
-
'
|
|
611
|
+
'Dynamic extensions expose the app empty-state component as EmptyState.',
|
|
610
612
|
'Use variant="naked" inside framed panels/lists and outline/subtle for standalone framed empty surfaces.',
|
|
611
613
|
],
|
|
612
614
|
};
|
|
@@ -655,6 +657,51 @@ export function buildExtensionResourceListSnippet(input) {
|
|
|
655
657
|
],
|
|
656
658
|
};
|
|
657
659
|
}
|
|
660
|
+
export function buildExtensionResourceGridSnippet(input) {
|
|
661
|
+
const itemsExpression = input.itemsExpression || 'items';
|
|
662
|
+
const itemName = input.itemName || 'item';
|
|
663
|
+
const keyExpression = input.keyExpression || `${itemName}.id`;
|
|
664
|
+
const defaultBody = [
|
|
665
|
+
`<h2 class="font-semibold eapp-text-primary">{{ ${itemName}.title || 'Untitled' }}</h2>`,
|
|
666
|
+
`<p v-if="${itemName}.description" class="text-sm eapp-text-secondary line-clamp-2">{{ ${itemName}.description }}</p>`,
|
|
667
|
+
].join('\n');
|
|
668
|
+
const normalized = normalizeVueBodySnippet(input.cardBody || defaultBody);
|
|
669
|
+
const frame = [
|
|
670
|
+
'<CommonResourceListFrame',
|
|
671
|
+
' variant="plain"',
|
|
672
|
+
` :loading="${input.loadingExpression || 'pending'}"`,
|
|
673
|
+
` :has-items="${itemsExpression}.length > 0"`,
|
|
674
|
+
` :total="${input.totalExpression || `${itemsExpression}.length`}"`,
|
|
675
|
+
` :items-per-page="${input.itemsPerPageExpression || '0'}"`,
|
|
676
|
+
` empty-title="${String(input.emptyTitle || 'No items found').replace(/"/g, '"')}"`,
|
|
677
|
+
` empty-description="${String(input.emptyDescription || '').replace(/"/g, '"')}"`,
|
|
678
|
+
` empty-icon="${input.emptyIcon || 'lucide:inbox'}"`,
|
|
679
|
+
'>',
|
|
680
|
+
' <div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">',
|
|
681
|
+
` <UCard v-for="${itemName} in ${itemsExpression}" :key="${keyExpression}" class="h-full eapp-surface-card eapp-radius-panel border eapp-divider">`,
|
|
682
|
+
' <div class="flex h-full flex-col gap-4">',
|
|
683
|
+
indentLines(normalized.code, 8),
|
|
684
|
+
' </div>',
|
|
685
|
+
' </UCard>',
|
|
686
|
+
' </div>',
|
|
687
|
+
'</CommonResourceListFrame>',
|
|
688
|
+
].join('\n');
|
|
689
|
+
const constrained = input.constrained === false
|
|
690
|
+
? frame
|
|
691
|
+
: ['<section class="eapp-page-constrained-wide space-y-4">', indentLines(frame, 2), '</section>'].join('\n');
|
|
692
|
+
return {
|
|
693
|
+
action: 'extension_resource_grid_built',
|
|
694
|
+
component: 'CommonResourceListFrame',
|
|
695
|
+
snippet: constrained,
|
|
696
|
+
normalizedBodyChanges: normalized.changes,
|
|
697
|
+
contract: [
|
|
698
|
+
'Use this card grid for dashboard/workboard/catalog collections; use resource_list for dense operational rows.',
|
|
699
|
+
'The default desktop layout uses three columns only at xl because the admin sidebar consumes viewport width.',
|
|
700
|
+
'Keep the page constrained unless the workflow intentionally owns a canvas or other full-bleed surface.',
|
|
701
|
+
'Keep card actions inside cardBody and align them with flex layout rather than floating them at the viewport edge.',
|
|
702
|
+
],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
658
705
|
export function buildExtensionFormEditorSnippet(input) {
|
|
659
706
|
const tag = input.lazy === false ? 'FormEditor' : 'FormEditorLazy';
|
|
660
707
|
const attrs = [
|
|
@@ -1135,6 +1182,32 @@ function collectExtensionRuntimeIssues(code) {
|
|
|
1135
1182
|
if (/\b(?:query|body|filter|deep|aggregate)\s*:\s*JSON\.stringify\s*\(/.test(source)) {
|
|
1136
1183
|
push('error', 'use-api-json-stringify-options', 'useApi query/body/filter/deep/aggregate options must be plain objects or computed objects, not JSON strings.', 'Pass the object directly to useApi or execute().');
|
|
1137
1184
|
}
|
|
1185
|
+
if (/<(?:CommonModal|UModal)\b[^>]*\bv-model\s*=/.test(source)) {
|
|
1186
|
+
push('error', 'modal-open-model', 'CommonModal/UModal must bind v-model:open, not the default v-model contract.', 'Call build_extension_ui kind=modal and preserve its v-model:open binding.');
|
|
1187
|
+
}
|
|
1188
|
+
if (/<CommonEmptyState\b/.test(source)) {
|
|
1189
|
+
push('error', 'unavailable-common-empty-state', 'CommonEmptyState is not registered in the dynamic extension runtime.', 'Use the injected EmptyState alias or build_extension_ui kind=empty_state/resource_list/resource_grid.');
|
|
1190
|
+
}
|
|
1191
|
+
const scriptBlocks = [...source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((match) => match[1]);
|
|
1192
|
+
const scriptSource = scriptBlocks.length ? scriptBlocks.join('\n') : (/<template\b/i.test(source) ? '' : source);
|
|
1193
|
+
const refNames = new Set([...scriptSource.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(?:ref|shallowRef)\s*\(/g)].map((match) => match[1]));
|
|
1194
|
+
const bareRefActionPattern = /\bonClick\s*:\s*\(\s*\)\s*=>\s*\(?\s*([A-Za-z_$][\w$]*)\s*=(?!=)/g;
|
|
1195
|
+
let bareRefActionMatch;
|
|
1196
|
+
while ((bareRefActionMatch = bareRefActionPattern.exec(scriptSource))) {
|
|
1197
|
+
const refName = bareRefActionMatch[1];
|
|
1198
|
+
if (refNames.has(refName)) {
|
|
1199
|
+
push('error', 'script-ref-assignment', `Script callback tries to reassign const ref ${refName}.`, `Assign ${refName}.value or call a handler; template ref auto-unwrapping does not apply inside registry/action callbacks.`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const executeAliasPattern = /\bexecute\s*:\s*([A-Za-z_$][\w$]*)/g;
|
|
1203
|
+
let executeAliasMatch;
|
|
1204
|
+
while ((executeAliasMatch = executeAliasPattern.exec(source))) {
|
|
1205
|
+
const executeAlias = executeAliasMatch[1];
|
|
1206
|
+
const references = source.match(new RegExp(`\\b${escapeRegExp(executeAlias)}\\b`, 'g'))?.length || 0;
|
|
1207
|
+
if (references === 1) {
|
|
1208
|
+
push('error', 'use-api-unused-execute', `useApi execute alias ${executeAlias} is never used.`, `Call or reference ${executeAlias} from onMounted, a watcher, or a user action; for read builders keep autoLoad enabled.`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1138
1211
|
if (/\buseApi\s*\(/.test(source) && !/\bexecute\s*:/.test(source) && !/\brefresh\s*:/.test(source) && !/\.\s*(?:execute|refresh)\s*\(/.test(source)) {
|
|
1139
1212
|
push('warning', 'use-api-no-execute-alias', 'useApi() appears without an execute/refresh alias or call.', 'Call useApi() as a top-level setup composable, then call or await execute()/refresh() from onMounted, watchers, or user actions when the request should run.');
|
|
1140
1213
|
}
|
|
@@ -1344,6 +1417,9 @@ export function buildExtensionUiSnippet(kind, input = {}) {
|
|
|
1344
1417
|
case 'resource_list':
|
|
1345
1418
|
result = buildExtensionResourceListSnippet(input);
|
|
1346
1419
|
break;
|
|
1420
|
+
case 'resource_grid':
|
|
1421
|
+
result = buildExtensionResourceGridSnippet(input);
|
|
1422
|
+
break;
|
|
1347
1423
|
case 'form_editor':
|
|
1348
1424
|
result = buildExtensionFormEditorSnippet(input);
|
|
1349
1425
|
break;
|
|
@@ -1422,6 +1498,7 @@ function getExtensionThemeContract() {
|
|
|
1422
1498
|
'The extension is already mounted inside the Enfyra app shell. Do not add a duplicate page header, centered page wrapper, or root-level page padding.',
|
|
1423
1499
|
'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
|
|
1424
1500
|
'Use usePageHeaderRegistry for the shell title and useHeaderActionRegistry/useSubHeaderActionRegistry for page actions.',
|
|
1501
|
+
'Register dynamic extension header actions inside onMounted after setup refs and handlers exist; build_extension_ui kind=page_shell generates this lifecycle shape.',
|
|
1425
1502
|
'Use build_extension_ui kind=menu_notification for sidebar menu notification registration snippets.',
|
|
1426
1503
|
'For shell menu notifications, first decide the signal source. Use a count only when the source already owns an exact count, such as a notification summary endpoint or bounded unread-notification query. Use a dot when a realtime event only proves that something new exists. Do not poll a domain list such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
|
|
1427
1504
|
'Use build_extension_ui kind=account_panel_item for account panel row registration snippets.',
|
|
@@ -1458,8 +1535,10 @@ function getExtensionThemeContract() {
|
|
|
1458
1535
|
'Use auto-injected components directly in the template with PascalCase names. Do not call resolveComponent() to manually resolve Nuxt UI/eApp components inside extension SFCs; it can compile but render unresolved lowercase DOM tags such as <ubutton>.',
|
|
1459
1536
|
'Buttons should have stable geometry: hover may change color, border, or shadow but must not move the button or resize its content. Disabled buttons keep disabled cursor/visual state.',
|
|
1460
1537
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
1461
|
-
'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, form editors, widgets, menu/account panel registries, tabs, upload modals, api_usage, notify, and runtime/theming reviews, call build_extension_ui with the matching kind after extension acknowledgement before patching raw Vue.',
|
|
1538
|
+
'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, resource grids, form editors, widgets, menu/account panel registries, tabs, upload modals, api_usage, notify, and runtime/theming reviews, call build_extension_ui with the matching kind after extension acknowledgement before patching raw Vue.',
|
|
1462
1539
|
'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=runtime_review, theme_review, or review before saving generated snippets that include composables, theme classes, high-contract UI, or native buttons.',
|
|
1540
|
+
'Use build_extension_ui kind=resource_grid for workboards, dashboards, catalogs, and responsive card collections instead of placing UCard children directly into a full-width list frame.',
|
|
1541
|
+
'Use the EmptyState runtime alias returned by build_extension_ui kind=empty_state; CommonEmptyState is not registered as a dynamic extension tag.',
|
|
1463
1542
|
'Extension validation rejects UInput, UTextarea, USelect, USelectMenu, UInputMenu, UInputNumber, UInputTags, UInputTime, and UInputDate without class="w-full" unless marked data-compact or data-inline.',
|
|
1464
1543
|
'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
|
|
1465
1544
|
],
|
|
@@ -1472,7 +1551,7 @@ function getExtensionThemeContract() {
|
|
|
1472
1551
|
shellComponentContracts: {
|
|
1473
1552
|
CommonDrawer: [
|
|
1474
1553
|
'Use build_extension_ui kind=drawer for generated drawer/editing snippets.',
|
|
1475
|
-
'The builder owns slots, managed footer actions, full-width fields, native button types, and loading/error/body structure.',
|
|
1554
|
+
'The builder owns slots, managed footer actions, full-width fields, native button types, and loading/error/body structure. CommonDrawer disables drag dismissal globally; do not add handle-only, drag handlers, or swipe-to-close behavior.',
|
|
1476
1555
|
],
|
|
1477
1556
|
CommonModal: [
|
|
1478
1557
|
'Use build_extension_ui kind=modal for generated modal/confirmation snippets.',
|
|
@@ -2912,13 +2991,13 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2912
2991
|
loading: z.string().optional().describe('Raw Vue expression/ref name for loading state.'),
|
|
2913
2992
|
disabled: z.string().optional().describe('Raw Vue expression/ref name for disabled state.'),
|
|
2914
2993
|
to: z.string().optional().describe('Route path for visible navigation actions.'),
|
|
2915
|
-
onClick: z.string().optional().describe('
|
|
2994
|
+
onClick: z.string().optional().describe('Script callback expression or handler reference. Prefer a handler name; bare ref assignments must use ref.value, e.g. () => (modalOpen.value = true).'),
|
|
2916
2995
|
order: z.number().optional().describe('Sort order in the shell header action area.'),
|
|
2917
2996
|
side: z.enum(['left', 'right']).optional().describe('Optional shell side.'),
|
|
2918
2997
|
});
|
|
2919
2998
|
server.tool('validate_dynamic_script', [
|
|
2920
2999
|
'Validate Enfyra dynamic script code before saving it to any script-backed metadata record.',
|
|
2921
|
-
'Use this before create/update of handlers, hooks, flow steps, websocket scripts,
|
|
3000
|
+
'Use this before create/update of handlers, hooks, flow steps, websocket scripts, OAuth provisioning scripts, or bootstrap scripts when the user is iterating on code.',
|
|
2922
3001
|
'This calls the same server compiler contract used by Enfyra, but does not save anything.',
|
|
2923
3002
|
].join(' '), {
|
|
2924
3003
|
sourceCode: z.string().describe('Raw dynamic script sourceCode.'),
|
|
@@ -2992,7 +3071,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2992
3071
|
server.tool('build_extension_ui', [
|
|
2993
3072
|
'Lazy gateway for Enfyra admin extension UI builders.',
|
|
2994
3073
|
'Use this after get_enfyra_required_knowledge(scope="extension") when a high-contract extension UI snippet is needed.',
|
|
2995
|
-
'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, api_usage, notify, confirm, runtime_review, theme_classes, theme_review, or review internally instead of exposing every builder tool up front.',
|
|
3074
|
+
'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, resource_grid, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, api_usage, notify, confirm, runtime_review, theme_classes, theme_review, or review internally instead of exposing every builder tool up front.',
|
|
2996
3075
|
].join(' '), {
|
|
2997
3076
|
kind: z.enum([
|
|
2998
3077
|
'drawer',
|
|
@@ -3001,6 +3080,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3001
3080
|
'permission_gate',
|
|
3002
3081
|
'empty_state',
|
|
3003
3082
|
'resource_list',
|
|
3083
|
+
'resource_grid',
|
|
3004
3084
|
'form_editor',
|
|
3005
3085
|
'widget',
|
|
3006
3086
|
'menu_notification',
|
|
@@ -3067,7 +3147,6 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3067
3147
|
titleExpression: z.string().optional().describe('Raw Vue expression for a dynamic title.'),
|
|
3068
3148
|
direction: z.enum(['right', 'left', 'top', 'bottom']).optional().default('right').describe('Drawer direction.'),
|
|
3069
3149
|
nested: z.boolean().optional().default(false).describe('Set true when rendering a drawer inside another modal/drawer.'),
|
|
3070
|
-
handleOnly: z.boolean().optional().default(false).describe('Set true only when the drawer should use handle-only behavior.'),
|
|
3071
3150
|
body: z.string().describe('Vue template body content for #body. UInput/UTextarea/select controls are normalized to w-full; native buttons get type="button".'),
|
|
3072
3151
|
cancelAction: z.union([extensionFooterActionSchema, z.literal(false)]).optional().describe('Cancel action object. Omit for a default Cancel that closes the model; false disables cancelAction.'),
|
|
3073
3152
|
primaryAction: extensionFooterActionSchema.optional().describe('Primary action. Editing/create drawers should wire Save/Create here.'),
|
|
@@ -3118,7 +3197,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3118
3197
|
body: z.string().describe('Vue template content to render inside PermissionGate. Field controls are normalized to w-full.'),
|
|
3119
3198
|
}, async (input) => jsonText(buildExtensionPermissionGateSnippet(input)));
|
|
3120
3199
|
server.tool('build_extension_empty_state', [
|
|
3121
|
-
'Generate
|
|
3200
|
+
'Generate an EmptyState snippet for Enfyra admin extensions.',
|
|
3122
3201
|
'Use this for app-matched empty/error/no-results states instead of hand-rolled blank panels.',
|
|
3123
3202
|
].join(' '), {
|
|
3124
3203
|
title: z.string().optional().describe('Empty state title.'),
|
|
@@ -3150,6 +3229,23 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3150
3229
|
emptyDescription: z.string().optional().describe('Empty description.'),
|
|
3151
3230
|
emptyIcon: z.string().optional().describe('Empty icon.'),
|
|
3152
3231
|
}, async (input) => jsonText(buildExtensionResourceListSnippet(input)));
|
|
3232
|
+
server.tool('build_extension_resource_grid', [
|
|
3233
|
+
'Generate a constrained responsive CommonResourceListFrame card grid for Enfyra admin extensions.',
|
|
3234
|
+
'Use this for workboards, catalogs, dashboards, and other card collections so generated pages do not become full-width horizontal strips.',
|
|
3235
|
+
'The tool owns the page constraint, plain list-frame chrome, md/two-column and xl/three-column breakpoints, semantic card surface, loading/empty frame, and stable card height.',
|
|
3236
|
+
].join(' '), {
|
|
3237
|
+
itemsExpression: z.string().optional().default('items').describe('Vue expression for the card array, e.g. notes.'),
|
|
3238
|
+
itemName: z.string().optional().default('item').describe('Loop variable name.'),
|
|
3239
|
+
keyExpression: z.string().optional().describe('Vue expression for :key. Defaults to item.id.'),
|
|
3240
|
+
cardBody: z.string().optional().describe('Card body Vue template. Defaults to semantic title/description. Fields and native buttons are normalized.'),
|
|
3241
|
+
loadingExpression: z.string().optional().default('pending').describe('Vue expression for frame loading.'),
|
|
3242
|
+
totalExpression: z.string().optional().describe('Vue expression for total cards.'),
|
|
3243
|
+
itemsPerPageExpression: z.string().optional().describe('Vue expression for items per page; use 0 to hide pagination.'),
|
|
3244
|
+
emptyTitle: z.string().optional().describe('Empty state title.'),
|
|
3245
|
+
emptyDescription: z.string().optional().describe('Empty state description.'),
|
|
3246
|
+
emptyIcon: z.string().optional().describe('Empty state icon.'),
|
|
3247
|
+
constrained: z.boolean().optional().default(true).describe('Wrap in eapp-page-constrained-wide. Disable only for intentional full-bleed surfaces.'),
|
|
3248
|
+
}, async (input) => jsonText(buildExtensionResourceGridSnippet(input)));
|
|
3153
3249
|
server.tool('build_extension_form_editor', [
|
|
3154
3250
|
'Generate a FormEditor/FormEditorLazy snippet for Enfyra table-backed extension forms.',
|
|
3155
3251
|
'Use this instead of hand-writing UInput/UTextarea fields when the form maps directly to a table record.',
|
|
@@ -3395,7 +3491,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3395
3491
|
].join(' '), {
|
|
3396
3492
|
path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
|
|
3397
3493
|
method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
|
|
3398
|
-
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER
|
|
3494
|
+
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, and @USER. Repository calls are async and reads return result.data. Use @REPOS.main or #secure.table_name/@REPOS.secure.table_name for user-facing access; reserve trusted repos for intentional field-permission bypass. Do not send compiledCode.'),
|
|
3399
3495
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
3400
3496
|
anonymousAccess: z.enum(['public', 'private']).optional().default('private').describe('public adds the method to publicMethods; private removes this method from publicMethods.'),
|
|
3401
3497
|
public: z.boolean().optional().describe('Compatibility alias for anonymousAccess. true means public, false means private.'),
|
|
@@ -3419,12 +3515,12 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3419
3515
|
'Prefer api_endpoint_workflow when route access, role/user permissions, overwrite decisions, or multi-step planning matter.',
|
|
3420
3516
|
'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.',
|
|
3421
3517
|
'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.',
|
|
3422
|
-
'For sourceCode, call discover_script_contexts first. Use #table_name or @REPOS.table_name for explicit table
|
|
3518
|
+
'For sourceCode, call discover_script_contexts first. Use #secure.table_name or @REPOS.secure.table_name for explicit user-facing table access; reserve #table_name/@REPOS.table_name for intentional trusted internal access.',
|
|
3423
3519
|
'Use table/schema tools separately when the user needs persisted data. This tool is for custom behavior endpoints.',
|
|
3424
3520
|
].join(' '), {
|
|
3425
3521
|
path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
|
|
3426
3522
|
method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
|
|
3427
|
-
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER
|
|
3523
|
+
sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, and @USER. Repository calls are async and reads return result.data. Use @REPOS.main or #secure.table_name/@REPOS.secure.table_name for user-facing access; reserve trusted repos for intentional field-permission bypass. Do not send compiledCode.'),
|
|
3428
3524
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
3429
3525
|
public: z.boolean().optional().default(false).describe('When true, the method is added to publicMethods for anonymous access.'),
|
|
3430
3526
|
description: z.string().optional().describe('Route description.'),
|