@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.
@@ -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) => ` ${buildHeaderActionLiteral(action)}`).join(',\n')}\n]);`);
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: 'CommonEmptyState',
607
- 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/>`,
608
610
  contract: [
609
- 'Use CommonEmptyState for app-matched empty states.',
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, '&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
+ }
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('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).'),
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, 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.',
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 a CommonEmptyState snippet for Enfyra admin extensions.',
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 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.'),
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 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.',
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 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.'),
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.'),