@enfyra/mcp-server 0.1.58 → 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.
@@ -522,6 +522,10 @@ function attrStaticOrBound(name, value, expression) {
522
522
  return `${name}="${String(value).replace(/"/g, '"')}"`;
523
523
  }
524
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
+ }
525
529
  const entries = [
526
530
  action.id ? `id: ${quoteJsString(action.id)}` : null,
527
531
  action.label ? `label: ${quoteJsString(action.label)}` : null,
@@ -553,14 +557,14 @@ export function buildExtensionPageShellSnippet(input) {
553
557
  ];
554
558
  if (actions.length) {
555
559
  lines.push('const { register: registerHeaderActions } = useHeaderActionRegistry();');
556
- lines.push(`registerHeaderActions([\n${actions.map((action) => ` ${buildHeaderActionLiteral(action)}`).join(',\n')}\n]);`);
560
+ lines.push(`onMounted(() => {\n registerHeaderActions([\n${actions.map((action) => ` ${buildHeaderActionLiteral(action)}`).join(',\n')}\n ]);\n});`);
557
561
  }
558
562
  return {
559
563
  action: 'extension_page_shell_built',
560
564
  snippet: lines.join('\n'),
561
565
  contract: [
562
566
  'Use usePageHeaderRegistry so the app shell renders the page header.',
563
- '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.',
564
568
  'Use primary solid only for the main scope action; secondary actions default to neutral outline.',
565
569
  ],
566
570
  };
@@ -601,10 +605,10 @@ export function buildExtensionEmptyStateSnippet(input) {
601
605
  : '';
602
606
  return {
603
607
  action: 'extension_empty_state_built',
604
- component: 'CommonEmptyState',
605
- snippet: `<CommonEmptyState\n title="${String(input.title || 'No items found').replace(/"/g, '&quot;')}"\n description="${String(input.description || '').replace(/"/g, '&quot;')}"\n icon="${input.icon || 'lucide:inbox'}"\n size="${input.size || 'sm'}"\n variant="${input.variant || 'naked'}"${action}\n/>`,
608
+ component: 'EmptyState',
609
+ snippet: `<EmptyState\n title="${String(input.title || 'No items found').replace(/"/g, '&quot;')}"\n description="${String(input.description || '').replace(/"/g, '&quot;')}"\n icon="${input.icon || 'lucide:inbox'}"\n size="${input.size || 'sm'}"\n variant="${input.variant || 'naked'}"${action}\n/>`,
606
610
  contract: [
607
- 'Use CommonEmptyState for app-matched empty states.',
611
+ 'Dynamic extensions expose the app empty-state component as EmptyState.',
608
612
  'Use variant="naked" inside framed panels/lists and outline/subtle for standalone framed empty surfaces.',
609
613
  ],
610
614
  };
@@ -653,6 +657,51 @@ export function buildExtensionResourceListSnippet(input) {
653
657
  ],
654
658
  };
655
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, '&quot;')}"`,
677
+ ` empty-description="${String(input.emptyDescription || '').replace(/"/g, '&quot;')}"`,
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
+ }
656
705
  export function buildExtensionFormEditorSnippet(input) {
657
706
  const tag = input.lazy === false ? 'FormEditor' : 'FormEditorLazy';
658
707
  const attrs = [
@@ -1133,6 +1182,32 @@ function collectExtensionRuntimeIssues(code) {
1133
1182
  if (/\b(?:query|body|filter|deep|aggregate)\s*:\s*JSON\.stringify\s*\(/.test(source)) {
1134
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().');
1135
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
+ }
1136
1211
  if (/\buseApi\s*\(/.test(source) && !/\bexecute\s*:/.test(source) && !/\brefresh\s*:/.test(source) && !/\.\s*(?:execute|refresh)\s*\(/.test(source)) {
1137
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.');
1138
1213
  }
@@ -1342,6 +1417,9 @@ export function buildExtensionUiSnippet(kind, input = {}) {
1342
1417
  case 'resource_list':
1343
1418
  result = buildExtensionResourceListSnippet(input);
1344
1419
  break;
1420
+ case 'resource_grid':
1421
+ result = buildExtensionResourceGridSnippet(input);
1422
+ break;
1345
1423
  case 'form_editor':
1346
1424
  result = buildExtensionFormEditorSnippet(input);
1347
1425
  break;
@@ -1420,6 +1498,7 @@ function getExtensionThemeContract() {
1420
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.',
1421
1499
  'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
1422
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.',
1423
1502
  'Use build_extension_ui kind=menu_notification for sidebar menu notification registration snippets.',
1424
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.',
1425
1504
  'Use build_extension_ui kind=account_panel_item for account panel row registration snippets.',
@@ -1456,8 +1535,10 @@ function getExtensionThemeContract() {
1456
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>.',
1457
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.',
1458
1537
  'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
1459
- '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.',
1460
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.',
1461
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.',
1462
1543
  'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
1463
1544
  ],
@@ -2910,13 +2991,13 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2910
2991
  loading: z.string().optional().describe('Raw Vue expression/ref name for loading state.'),
2911
2992
  disabled: z.string().optional().describe('Raw Vue expression/ref name for disabled state.'),
2912
2993
  to: z.string().optional().describe('Route path for visible navigation actions.'),
2913
- onClick: z.string().optional().describe('Raw Vue expression or function reference for click behavior.'),
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).'),
2914
2995
  order: z.number().optional().describe('Sort order in the shell header action area.'),
2915
2996
  side: z.enum(['left', 'right']).optional().describe('Optional shell side.'),
2916
2997
  });
2917
2998
  server.tool('validate_dynamic_script', [
2918
2999
  'Validate Enfyra dynamic script code before saving it to any script-backed metadata record.',
2919
- 'Use this before create/update of handlers, hooks, flow steps, websocket scripts, GraphQL scripts, or bootstrap scripts when the user is iterating on code.',
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.',
2920
3001
  'This calls the same server compiler contract used by Enfyra, but does not save anything.',
2921
3002
  ].join(' '), {
2922
3003
  sourceCode: z.string().describe('Raw dynamic script sourceCode.'),
@@ -2990,7 +3071,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2990
3071
  server.tool('build_extension_ui', [
2991
3072
  'Lazy gateway for Enfyra admin extension UI builders.',
2992
3073
  'Use this after get_enfyra_required_knowledge(scope="extension") when a high-contract extension UI snippet is needed.',
2993
- '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.',
2994
3075
  ].join(' '), {
2995
3076
  kind: z.enum([
2996
3077
  'drawer',
@@ -2999,6 +3080,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2999
3080
  'permission_gate',
3000
3081
  'empty_state',
3001
3082
  'resource_list',
3083
+ 'resource_grid',
3002
3084
  'form_editor',
3003
3085
  'widget',
3004
3086
  'menu_notification',
@@ -3115,7 +3197,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3115
3197
  body: z.string().describe('Vue template content to render inside PermissionGate. Field controls are normalized to w-full.'),
3116
3198
  }, async (input) => jsonText(buildExtensionPermissionGateSnippet(input)));
3117
3199
  server.tool('build_extension_empty_state', [
3118
- 'Generate a CommonEmptyState snippet for Enfyra admin extensions.',
3200
+ 'Generate an EmptyState snippet for Enfyra admin extensions.',
3119
3201
  'Use this for app-matched empty/error/no-results states instead of hand-rolled blank panels.',
3120
3202
  ].join(' '), {
3121
3203
  title: z.string().optional().describe('Empty state title.'),
@@ -3147,6 +3229,23 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3147
3229
  emptyDescription: z.string().optional().describe('Empty description.'),
3148
3230
  emptyIcon: z.string().optional().describe('Empty icon.'),
3149
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)));
3150
3249
  server.tool('build_extension_form_editor', [
3151
3250
  'Generate a FormEditor/FormEditorLazy snippet for Enfyra table-backed extension forms.',
3152
3251
  'Use this instead of hand-writing UInput/UTextarea fields when the form maps directly to a table record.',
@@ -3392,7 +3491,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3392
3491
  ].join(' '), {
3393
3492
  path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
3394
3493
  method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
3395
- sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Repository calls are async: use `const result = await #table.find(...)` and read rows from `result.data || []`. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
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.'),
3396
3495
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
3397
3496
  anonymousAccess: z.enum(['public', 'private']).optional().default('private').describe('public adds the method to publicMethods; private removes this method from publicMethods.'),
3398
3497
  public: z.boolean().optional().describe('Compatibility alias for anonymousAccess. true means public, false means private.'),
@@ -3416,12 +3515,12 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3416
3515
  'Prefer api_endpoint_workflow when route access, role/user permissions, overwrite decisions, or multi-step planning matter.',
3417
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.',
3418
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.',
3419
- 'For sourceCode, call discover_script_contexts first. Use #table_name or @REPOS.table_name for explicit table repos with exact fields/auth checks; @REPOS.secure.<table> is rejected as non-portable.',
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.',
3420
3519
  'Use table/schema tools separately when the user needs persisted data. This tool is for custom behavior endpoints.',
3421
3520
  ].join(' '), {
3422
3521
  path: z.string().describe('Custom route path, e.g. /sum. Must not be a full URL.'),
3423
3522
  method: z.string().describe('HTTP method for the handler, e.g. GET or POST.'),
3424
- sourceCode: z.string().describe('Handler sourceCode. Use macros such as @QUERY, @BODY, @THROW400, @REPOS, @USER and #table_name. Repository calls are async: use `const result = await #table.find(...)` and read rows from `result.data || []`. Do not send compiledCode. Do not use @REPOS.secure.<table>; use @REPOS.main for route main table or #table_name/@REPOS.table_name with explicit fields/auth checks.'),
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.'),
3425
3524
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
3426
3525
  public: z.boolean().optional().default(false).describe('When true, the method is added to publicMethods for anonymous access.'),
3427
3526
  description: z.string().optional().describe('Route description.'),