@enfyra/mcp-server 0.1.44 → 0.1.45

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.
@@ -22,7 +22,7 @@ export function buildMcpServerInstructions(apiBaseUrl, options = {}) {
22
22
  '- For target sanity checks, call `get_enfyra_api_context`; do not load broad metadata only to confirm the API base.',
23
23
  '- When the goal is clear but the tool path is not, call `discover_enfyra_workflows` with intent/risk/surface. Use `detail: "plan"` before writes, follow `primaryPath`, and treat `avoidTools` as hard negative-routing boundaries.',
24
24
  '- Discover before deciding, then inspect narrowly. Use `inspect_table`, `inspect_route`, `inspect_feature`, `search_admin_extensions`, or `search_runtime_zone` for the exact table/route/feature/runtime artifact. Run broad discovery tools sequentially, not in parallel.',
25
- '- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `build_extension_*` tools for high-contract components such as drawers, modals, page shell headers/actions, permission gates, empty states, and resource lists, and call `get_theme_class_reference` only when exact classes are needed.',
25
+ '- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `build_extension_*` tools for high-contract components such as drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, FormEditor, Widget, menu/account panel registries, tabs, and upload modals, and call `get_theme_class_reference` only when exact classes are needed.',
26
26
  '- Prefer the most specific business operation tool over raw metadata CRUD.',
27
27
  '- Before any write, call `get_enfyra_required_knowledge({ scope })` with the right scope and pass returned ack keys into write tools. Choose scope by task domain:',
28
28
  ' • `schema` — table, column, relation, record CRUD, route permission, guard, field permission, column rule, cache reload, GraphQL enablement, log debug.',
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q,mcAAmc;QACnc,4EAA4E;QAC5E,oKAAoK;QACpK,2JAA2J;QAC3J,uJAAuJ;QACvJ,8IAA8I;QAC9I,yDAAyD;QACzD,4EAA4E;QAC5E,sMAAsM;QACtM,mLAAmL;QACnL,8LAA8L;QAC9L,EAAE;QACF,0BAA0B;QAC1B,uHAAuH;QACvH,sLAAsL;QACtL,gLAAgL;QAChL,iLAAiL;QACjL,yLAAyL;QACzL,kKAAkK;QAClK,0HAA0H;QAC1H,4LAA4L;QAC5L,EAAE;QACF,+BAA+B;QAC/B,4HAA4H;QAC5H,4GAA4G;QAC5G,mIAAmI;QACnI,kJAAkJ;QAClJ,oNAAoN;QACpN,iKAAiK;QACjK,gIAAgI;QAChI,EAAE;QACF,+BAA+B;QAC/B,iIAAiI;QACjI,uJAAuJ;QACvJ,yOAAyO;QACzO,+JAA+J;QAC/J,iMAAiM;QACjM,wSAAwS;QACxS,qLAAqL;QACrL,kPAAkP;QAClP,+PAA+P;QAC/P,EAAE;QACF,4BAA4B;QAC5B,0MAA0M;QAC1M,oHAAoH;QACpH,kIAAkI;QAClI,8MAA8M;QAC9M,kNAAkN;QAClN,wLAAwL;QACxL,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q,2gBAA2gB;QAC3gB,4EAA4E;QAC5E,oKAAoK;QACpK,2JAA2J;QAC3J,uJAAuJ;QACvJ,8IAA8I;QAC9I,yDAAyD;QACzD,4EAA4E;QAC5E,sMAAsM;QACtM,mLAAmL;QACnL,8LAA8L;QAC9L,EAAE;QACF,0BAA0B;QAC1B,uHAAuH;QACvH,sLAAsL;QACtL,gLAAgL;QAChL,iLAAiL;QACjL,yLAAyL;QACzL,kKAAkK;QAClK,0HAA0H;QAC1H,4LAA4L;QAC5L,EAAE;QACF,+BAA+B;QAC/B,4HAA4H;QAC5H,4GAA4G;QAC5G,mIAAmI;QACnI,kJAAkJ;QAClJ,oNAAoN;QACpN,iKAAiK;QACjK,gIAAgI;QAChI,EAAE;QACF,+BAA+B;QAC/B,iIAAiI;QACjI,uJAAuJ;QACvJ,yOAAyO;QACzO,+JAA+J;QAC/J,iMAAiM;QACjM,wSAAwS;QACxS,qLAAqL;QACrL,kPAAkP;QAClP,+PAA+P;QAC/P,EAAE;QACF,4BAA4B;QAC5B,0MAA0M;QAC1M,oHAAoH;QACpH,kIAAkI;QAClI,8MAA8M;QAC9M,kNAAkN;QAClN,wLAAwL;QACxL,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -39,6 +39,42 @@ export declare function buildExtensionResourceListSnippet(input: any): {
39
39
  snippet: string;
40
40
  contract: string[];
41
41
  };
42
+ export declare function buildExtensionFormEditorSnippet(input: any): {
43
+ action: string;
44
+ component: string;
45
+ snippet: string;
46
+ contract: string[];
47
+ };
48
+ export declare function buildExtensionWidgetSnippet(input: any): {
49
+ action: string;
50
+ component: string;
51
+ snippet: string;
52
+ warnings: string[];
53
+ contract: string[];
54
+ };
55
+ export declare function buildExtensionMenuNotificationSnippet(input: any): {
56
+ action: string;
57
+ snippet: string;
58
+ contract: string[];
59
+ };
60
+ export declare function buildExtensionAccountPanelSnippet(input: any): {
61
+ action: string;
62
+ snippet: string;
63
+ contract: string[];
64
+ };
65
+ export declare function buildExtensionTabsSnippet(input: any): {
66
+ action: string;
67
+ component: string;
68
+ snippet: string;
69
+ contract: string[];
70
+ };
71
+ export declare function buildExtensionUploadModalSnippet(input: any): {
72
+ action: string;
73
+ component: string;
74
+ snippet: string;
75
+ companionSnippet: string;
76
+ contract: string[];
77
+ };
42
78
  export declare function reviewExtensionUiContract(code: any): {
43
79
  action: string;
44
80
  valid: boolean;
@@ -512,6 +512,16 @@ export function buildExtensionModalSnippet(input) {
512
512
  function jsObjectLiteral(entries) {
513
513
  return `{ ${entries.filter(Boolean).join(', ')} }`;
514
514
  }
515
+ function jsArrayLiteral(values) {
516
+ return `[${(values || []).map(quoteJsString).join(', ')}]`;
517
+ }
518
+ function attrStaticOrBound(name, value, expression) {
519
+ if (expression)
520
+ return `:${name}="${expression}"`;
521
+ if (value === undefined || value === null || value === '')
522
+ return null;
523
+ return `${name}="${String(value).replace(/"/g, '"')}"`;
524
+ }
515
525
  function buildHeaderActionLiteral(action) {
516
526
  const entries = [
517
527
  action.id ? `id: ${quoteJsString(action.id)}` : null,
@@ -643,6 +653,189 @@ export function buildExtensionResourceListSnippet(input) {
643
653
  ],
644
654
  };
645
655
  }
656
+ export function buildExtensionFormEditorSnippet(input) {
657
+ const tag = input.lazy === false ? 'FormEditor' : 'FormEditorLazy';
658
+ const attrs = [
659
+ `v-model="${input.model || 'form'}"`,
660
+ `v-model:errors="${input.errors || 'errors'}"`,
661
+ attrStaticOrBound('table-name', input.tableName, input.tableNameExpression),
662
+ input.mode ? `mode="${input.mode}"` : null,
663
+ input.loadingExpression ? `:loading="${input.loadingExpression}"` : null,
664
+ input.layout ? `layout="${input.layout}"` : null,
665
+ input.includes?.length ? `:includes="${jsArrayLiteral(input.includes)}"` : null,
666
+ input.excluded?.length ? `:excluded="${jsArrayLiteral(input.excluded)}"` : null,
667
+ input.sectionsExpression ? `:sections="${input.sectionsExpression}"` : null,
668
+ input.fieldMapExpression ? `:field-map="${input.fieldMapExpression}"` : null,
669
+ input.virtualFieldsExpression ? `:virtual-fields="${input.virtualFieldsExpression}"` : null,
670
+ input.currentRecordIdExpression ? `:current-record-id="${input.currentRecordIdExpression}"` : null,
671
+ input.hasChangedHandler ? `@has-changed="${input.hasChangedHandler}"` : null,
672
+ input.virtualFieldEmitHandler ? `@virtual-field-emit="${input.virtualFieldEmitHandler}"` : null,
673
+ ].filter(Boolean);
674
+ const snippet = [
675
+ `<${tag}`,
676
+ ...attrs.map((attr) => ` ${attr}`),
677
+ '/>',
678
+ ].join('\n');
679
+ return {
680
+ action: 'extension_form_editor_built',
681
+ component: tag,
682
+ snippet,
683
+ contract: [
684
+ 'Prefer FormEditor/FormEditorLazy for direct table-backed forms instead of hand-built UInput/UTextarea fields.',
685
+ 'Use v-model for record state and v-model:errors for validation errors.',
686
+ 'Use includes/sections to keep generated forms focused; do not expose compiledCode or unrelated system fields.',
687
+ 'Use fieldMap only for behavior/renderer overrides such as code fields or custom labels.',
688
+ ],
689
+ };
690
+ }
691
+ export function buildExtensionWidgetSnippet(input) {
692
+ const attrs = [`:id="${typeof input.id === 'number' ? input.id : quoteJsString(input.id)}"`];
693
+ for (const [key, value] of Object.entries(input.props || {})) {
694
+ attrs.push(`:${key}="${value}"`);
695
+ }
696
+ for (const [event, handler] of Object.entries(input.events || {})) {
697
+ attrs.push(`@${event}="${handler}"`);
698
+ }
699
+ return {
700
+ action: 'extension_widget_built',
701
+ component: 'Widget',
702
+ snippet: `<Widget ${attrs.join(' ')} />`,
703
+ warnings: typeof input.id === 'number' ? [] : ['Widget ids should be numeric enfyra_extension ids; do not pass extension name or extensionId string.'],
704
+ contract: [
705
+ 'Widget :id is the numeric enfyra_extension id, not name or extensionId.',
706
+ 'Pass safe props/events; keep page-level mutation and modal ownership in the page unless the widget intentionally owns the full workflow.',
707
+ ],
708
+ };
709
+ }
710
+ export function buildExtensionMenuNotificationSnippet(input) {
711
+ const targetEntries = [
712
+ input.targetId !== undefined ? `id: ${quoteJsString(input.targetId)}` : null,
713
+ input.path ? `path: ${quoteJsString(input.path)}` : null,
714
+ input.route ? `route: ${quoteJsString(input.route)}` : null,
715
+ ];
716
+ const entries = [
717
+ `id: ${quoteJsString(input.id || 'extension-menu-notification')}`,
718
+ `target: ${jsObjectLiteral(targetEntries)}`,
719
+ input.valueExpression ? `value: ${input.valueExpression}` : input.value !== undefined ? `value: ${quoteJsString(input.value)}` : null,
720
+ `color: ${quoteJsString(input.color || 'primary')}`,
721
+ input.title ? `title: ${quoteJsString(input.title)}` : null,
722
+ typeof input.order === 'number' ? `order: ${input.order}` : null,
723
+ ];
724
+ return {
725
+ action: 'extension_menu_notification_built',
726
+ snippet: [
727
+ 'const { register: registerMenuNotification } = useMenuNotificationRegistry();',
728
+ `registerMenuNotification(${jsObjectLiteral(entries)});`,
729
+ ].join('\n'),
730
+ contract: [
731
+ 'Use count/value only when the signal source already owns an exact or bounded count.',
732
+ 'Omit value for a dot-only notification when realtime only proves new attention exists.',
733
+ 'Do not fetch destination domain lists solely to decorate the menu.',
734
+ ],
735
+ };
736
+ }
737
+ export function buildExtensionAccountPanelSnippet(input) {
738
+ const entries = [
739
+ `id: ${quoteJsString(input.id || 'extension-account-panel-item')}`,
740
+ typeof input.order === 'number' ? `order: ${input.order}` : null,
741
+ input.label ? `label: ${quoteJsString(input.label)}` : null,
742
+ input.description ? `description: ${quoteJsString(input.description)}` : null,
743
+ input.icon ? `icon: ${quoteJsString(input.icon)}` : null,
744
+ input.countExpression ? `count: ${input.countExpression}` : input.count !== undefined ? `count: ${quoteJsString(input.count)}` : null,
745
+ input.badgeExpression ? `badge: ${input.badgeExpression}` : input.badge !== undefined ? `badge: ${quoteJsString(input.badge)}` : null,
746
+ input.badgeColor ? `badgeColor: ${quoteJsString(input.badgeColor)}` : null,
747
+ input.trailingIcon ? `trailingIcon: ${quoteJsString(input.trailingIcon)}` : null,
748
+ input.expandedExpression ? `expanded: ${input.expandedExpression}` : null,
749
+ input.contentComponent ? `contentComponent: ${input.contentComponent}` : null,
750
+ input.contentPropsExpression ? `contentProps: ${input.contentPropsExpression}` : null,
751
+ input.onClick ? `onClick: ${input.onClick}` : null,
752
+ input.onToggle ? `onToggle: ${input.onToggle}` : null,
753
+ ];
754
+ return {
755
+ action: 'extension_account_panel_item_built',
756
+ snippet: [
757
+ 'const { register: registerAccountPanelItem } = useAccountPanelRegistry();',
758
+ `registerAccountPanelItem(${jsObjectLiteral(entries)});`,
759
+ ].join('\n'),
760
+ contract: [
761
+ 'Prefer data-driven account panel rows over fully custom row components.',
762
+ 'Use count for notification-style chips; count takes precedence over badge.',
763
+ 'Use onClick for direct actions and onToggle/contentComponent for expandable inline UI.',
764
+ ],
765
+ };
766
+ }
767
+ export function buildExtensionTabsSnippet(input) {
768
+ const model = input.model || 'activeTab';
769
+ const items = input.itemsExpression || 'tabs';
770
+ const body = input.body || '<div>{{ item.label }}</div>';
771
+ const snippet = [
772
+ `<UTabs v-model="${model}" :items="${items}" class="w-full">`,
773
+ ' <template #content="{ item }">',
774
+ indentLines(normalizeVueBodySnippet(body).code, 4),
775
+ ' </template>',
776
+ '</UTabs>',
777
+ ].join('\n');
778
+ return {
779
+ action: 'extension_tabs_built',
780
+ component: 'UTabs',
781
+ snippet,
782
+ contract: [
783
+ 'Use app-level UTabs chrome instead of custom tab bars.',
784
+ 'Do not add local full-width bottom borders/dividers to tab lists.',
785
+ 'Keep tab items data-driven and render panel content through #content.',
786
+ ],
787
+ };
788
+ }
789
+ export function buildExtensionUploadModalSnippet(input) {
790
+ const model = input.model || 'showUploadModal';
791
+ const attrs = [
792
+ `v-model="${model}"`,
793
+ `title="${String(input.title || 'Upload Files').replace(/"/g, '&quot;')}"`,
794
+ `accept="${input.accept || '*/*'}"`,
795
+ input.multiple !== false ? ':multiple="true"' : ':multiple="false"',
796
+ input.maxSizeExpression ? `:max-size="${input.maxSizeExpression}"` : ':max-size="10 * 1024 * 1024"',
797
+ input.loadingExpression ? `:loading="${input.loadingExpression}"` : null,
798
+ input.uploadProgressExpression ? `:upload-progress="${input.uploadProgressExpression}"` : null,
799
+ input.fileProgressExpression ? `:file-progress="${input.fileProgressExpression}"` : null,
800
+ input.dragText ? `drag-text="${String(input.dragText).replace(/"/g, '&quot;')}"` : null,
801
+ input.acceptText ? `accept-text="${String(input.acceptText).replace(/"/g, '&quot;')}"` : null,
802
+ input.uploadText ? `upload-text="${String(input.uploadText).replace(/"/g, '&quot;')}"` : null,
803
+ input.uploadingText ? `uploading-text="${String(input.uploadingText).replace(/"/g, '&quot;')}"` : null,
804
+ `@upload="${input.uploadHandler || 'handleUpload'}"`,
805
+ input.errorHandler ? `@error="${input.errorHandler}"` : null,
806
+ ].filter(Boolean);
807
+ const headerContent = input.headerContent ? [
808
+ '>',
809
+ ' <template #header-content>',
810
+ indentLines(normalizeVueBodySnippet(input.headerContent).code, 4),
811
+ ' </template>',
812
+ '</CommonUploadModal>',
813
+ ] : ['/>'];
814
+ return {
815
+ action: 'extension_upload_modal_built',
816
+ component: 'CommonUploadModal',
817
+ snippet: [
818
+ '<CommonUploadModal',
819
+ ...attrs.map((attr) => ` ${attr}`),
820
+ ...headerContent,
821
+ ].join('\n'),
822
+ companionSnippet: [
823
+ 'const {',
824
+ ' uploadProgress,',
825
+ ' trackedUploadProgressById,',
826
+ ' beginTrackedUploadProgress,',
827
+ ' getUploadProgressHeaders,',
828
+ ' resetUploadProgress,',
829
+ '} = useFileUploadProgress();',
830
+ ].join('\n'),
831
+ contract: [
832
+ 'Use useFileUploadProgress for admin-socket upload progress.',
833
+ 'Send x-enfyra-upload-id via getUploadProgressHeaders(id) for each uploaded file.',
834
+ 'For multi-file uploads, call the useApi batch files path once, pass per-file headers through headersByIndex, and map each upload id to fileProgress[index].',
835
+ 'CommonUploadModal owns selected-file rows and per-row progress chrome.',
836
+ ],
837
+ };
838
+ }
646
839
  export function reviewExtensionUiContract(code) {
647
840
  const source = String(code || '');
648
841
  const issues = [];
@@ -2430,6 +2623,98 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2430
2623
  emptyDescription: z.string().optional().describe('Empty description.'),
2431
2624
  emptyIcon: z.string().optional().describe('Empty icon.'),
2432
2625
  }, async (input) => jsonText(buildExtensionResourceListSnippet(input)));
2626
+ server.tool('build_extension_form_editor', [
2627
+ 'Generate a FormEditor/FormEditorLazy snippet for Enfyra table-backed extension forms.',
2628
+ 'Use this instead of hand-writing UInput/UTextarea fields when the form maps directly to a table record.',
2629
+ ].join(' '), {
2630
+ tableName: z.string().optional().describe('Static table name.'),
2631
+ tableNameExpression: z.string().optional().describe('Raw Vue expression for dynamic table name.'),
2632
+ model: z.string().optional().default('form').describe('Record state variable for v-model.'),
2633
+ errors: z.string().optional().default('errors').describe('Errors state variable for v-model:errors.'),
2634
+ mode: z.enum(['create', 'update']).optional().describe('Optional fixed form mode.'),
2635
+ loadingExpression: z.string().optional().describe('Raw Vue expression/ref for loading.'),
2636
+ layout: z.enum(['stack', 'grid']).optional().describe('Form layout.'),
2637
+ includes: z.array(z.string()).optional().describe('Fields to include. Prefer explicit includes for focused generated forms.'),
2638
+ excluded: z.array(z.string()).optional().describe('Fields to exclude. compiledCode is always excluded by FormEditor.'),
2639
+ sectionsExpression: z.string().optional().describe('Raw Vue expression for FormEditorSection[].'),
2640
+ fieldMapExpression: z.string().optional().describe('Raw Vue expression for fieldMap overrides.'),
2641
+ virtualFieldsExpression: z.string().optional().describe('Raw Vue expression for virtual fields.'),
2642
+ currentRecordIdExpression: z.string().optional().describe('Raw Vue expression for current record id.'),
2643
+ hasChangedHandler: z.string().optional().describe('Handler expression for @has-changed.'),
2644
+ virtualFieldEmitHandler: z.string().optional().describe('Handler expression for @virtual-field-emit.'),
2645
+ lazy: z.boolean().optional().default(true).describe('Use FormEditorLazy by default.'),
2646
+ }, async (input) => jsonText(buildExtensionFormEditorSnippet(input)));
2647
+ server.tool('build_extension_widget', [
2648
+ 'Generate a Widget snippet for reusing a widget extension inside an Enfyra page extension.',
2649
+ 'Use this so agents pass numeric widget ids and keep prop/event ownership explicit.',
2650
+ ].join(' '), {
2651
+ id: z.union([z.number(), z.string()]).describe('Numeric enfyra_extension widget id. Strings are allowed but return a warning because names/extensionId are wrong for Widget.'),
2652
+ props: z.record(z.string()).optional().describe('Map of prop name to raw Vue expression.'),
2653
+ events: z.record(z.string()).optional().describe('Map of event name to handler expression.'),
2654
+ }, async (input) => jsonText(buildExtensionWidgetSnippet(input)));
2655
+ server.tool('build_extension_menu_notification', [
2656
+ 'Generate useMenuNotificationRegistry registration code for a global extension.',
2657
+ 'Use this for sidebar menu count chips or dot notifications without mutating enfyra_menu records.',
2658
+ ].join(' '), {
2659
+ id: z.string().optional().describe('Stable notification id.'),
2660
+ targetId: z.union([z.string(), z.number()]).optional().describe('Target menu id.'),
2661
+ path: z.string().optional().describe('Target menu path.'),
2662
+ route: z.string().optional().describe('Target route path.'),
2663
+ value: z.union([z.string(), z.number()]).optional().describe('Static count/chip value. Omit with valueExpression for a dot.'),
2664
+ valueExpression: z.string().optional().describe('Raw Vue expression for count/chip value. Omit value for a dot-only notification.'),
2665
+ color: z.enum(['primary', 'success', 'warning', 'error', 'info', 'neutral']).optional().default('primary').describe('Chip/dot color intent.'),
2666
+ title: z.string().optional().describe('Optional tooltip/title.'),
2667
+ order: z.number().optional().describe('Sort order when multiple notifications target the same menu.'),
2668
+ }, async (input) => jsonText(buildExtensionMenuNotificationSnippet(input)));
2669
+ server.tool('build_extension_account_panel_item', [
2670
+ 'Generate useAccountPanelRegistry registration code for a global extension.',
2671
+ 'Use this for data-driven account panel rows instead of drawing full custom sidebar/account UI.',
2672
+ ].join(' '), {
2673
+ id: z.string().optional().describe('Stable account panel item id.'),
2674
+ order: z.number().optional().describe('Display order.'),
2675
+ label: z.string().optional().describe('Row label.'),
2676
+ description: z.string().optional().describe('Row description.'),
2677
+ icon: z.string().optional().describe('Leading icon.'),
2678
+ count: z.union([z.string(), z.number()]).optional().describe('Static notification chip value.'),
2679
+ countExpression: z.string().optional().describe('Raw Vue expression for notification chip value.'),
2680
+ badge: z.union([z.string(), z.number()]).optional().describe('Legacy static badge value. Prefer count.'),
2681
+ badgeExpression: z.string().optional().describe('Raw Vue expression for badge. Prefer countExpression.'),
2682
+ badgeColor: z.enum(['primary', 'neutral', 'info', 'error', 'warning', 'success']).optional().describe('Chip color.'),
2683
+ trailingIcon: z.string().optional().describe('Trailing icon.'),
2684
+ expandedExpression: z.string().optional().describe('Raw Vue expression controlling expanded state.'),
2685
+ contentComponent: z.string().optional().describe('Raw component reference for inline expanded content.'),
2686
+ contentPropsExpression: z.string().optional().describe('Raw Vue expression for content props.'),
2687
+ onClick: z.string().optional().describe('Direct action handler expression.'),
2688
+ onToggle: z.string().optional().describe('Expandable row toggle handler expression.'),
2689
+ }, async (input) => jsonText(buildExtensionAccountPanelSnippet(input)));
2690
+ server.tool('build_extension_tabs', [
2691
+ 'Generate a UTabs snippet for Enfyra extension page sections.',
2692
+ 'Use this instead of custom tab bars so app-wide tab chrome owns active indicators, focus rings, spacing, and theme contrast.',
2693
+ ].join(' '), {
2694
+ model: z.string().optional().default('activeTab').describe('Active tab model variable.'),
2695
+ itemsExpression: z.string().optional().default('tabs').describe('Raw Vue expression for tab items.'),
2696
+ body: z.string().optional().describe('Vue template body for #content="{ item }".'),
2697
+ }, async (input) => jsonText(buildExtensionTabsSnippet(input)));
2698
+ server.tool('build_extension_upload_modal', [
2699
+ 'Generate a CommonUploadModal snippet and upload-progress companion snippet for Enfyra extensions.',
2700
+ 'Use this for file upload UI so progress, selected-file rows, and x-enfyra-upload-id wiring follow the app contract.',
2701
+ ].join(' '), {
2702
+ model: z.string().optional().default('showUploadModal').describe('Modal open state variable.'),
2703
+ title: z.string().optional().describe('Upload modal title.'),
2704
+ accept: z.string().optional().default('*/*').describe('Accepted mime/extensions.'),
2705
+ multiple: z.boolean().optional().default(true).describe('Allow multiple files.'),
2706
+ maxSizeExpression: z.string().optional().describe('Raw Vue expression for max file size.'),
2707
+ loadingExpression: z.string().optional().describe('Raw Vue expression/ref for upload pending state.'),
2708
+ uploadProgressExpression: z.string().optional().describe('Raw Vue expression/ref for aggregate upload progress.'),
2709
+ fileProgressExpression: z.string().optional().describe('Raw Vue expression for per-row progress map.'),
2710
+ dragText: z.string().optional().describe('Drag/drop text.'),
2711
+ acceptText: z.string().optional().describe('Accept/help text.'),
2712
+ uploadText: z.string().optional().describe('Upload action text.'),
2713
+ uploadingText: z.string().optional().describe('Uploading action text.'),
2714
+ uploadHandler: z.string().optional().default('handleUpload').describe('@upload handler expression.'),
2715
+ errorHandler: z.string().optional().describe('@error handler expression.'),
2716
+ headerContent: z.string().optional().describe('Optional #header-content template, e.g. storage selector. Fields are normalized to w-full.'),
2717
+ }, async (input) => jsonText(buildExtensionUploadModalSnippet(input)));
2433
2718
  server.tool('extension_workflow', [
2434
2719
  'Step-by-step workflow for creating or updating Enfyra admin page, global, or widget extensions.',
2435
2720
  'Use this when an LLM is building extension UI, menu shell notifications, account panel entries, or page/menu wiring and should follow live nextSteps instead of guessing raw enfyra_extension mutations.',