@enfyra/mcp-server 0.1.53 → 0.1.55

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.
@@ -78,6 +78,7 @@ export declare function buildExtensionUploadModalSnippet(input: any): {
78
78
  };
79
79
  export declare function buildExtensionApiUsageSnippet(input?: AnyRecord): {
80
80
  action: string;
81
+ operation: string;
81
82
  snippet: string;
82
83
  contract: string[];
83
84
  };
@@ -847,21 +847,71 @@ function toPascalIdentifier(value, fallback = 'Items') {
847
847
  .join('');
848
848
  return raw || fallback;
849
849
  }
850
+ function buildExtensionSort(sort) {
851
+ if (!Array.isArray(sort) || sort.length === 0)
852
+ return undefined;
853
+ const fields = sort.map((entry) => {
854
+ const field = String(entry?.field || '').trim();
855
+ if (!field)
856
+ throw new Error('Each extension sort entry requires a field.');
857
+ return String(entry?.direction || 'asc').toLowerCase() === 'desc'
858
+ ? `-${field}`
859
+ : field;
860
+ });
861
+ return fields.join(',');
862
+ }
850
863
  export function buildExtensionApiUsageSnippet(input = {}) {
851
864
  const resource = String(input.resource || input.name || 'items');
852
865
  const pascal = toPascalIdentifier(resource, 'Items');
853
- const path = input.path || `/${resource}`;
854
- const method = String(input.method || 'GET').toUpperCase();
866
+ const operation = String(input.operation || input.mode || input.intent || '').toLowerCase() || String(input.method || 'GET').toLowerCase();
867
+ const normalizedOperation = {
868
+ get: 'list',
869
+ read: 'list',
870
+ load: 'list',
871
+ post: 'create',
872
+ patch: 'update',
873
+ put: 'update',
874
+ del: 'delete',
875
+ remove: 'delete',
876
+ destroy: 'delete',
877
+ }[operation] || operation;
878
+ const defaultMethodByOperation = {
879
+ list: 'GET',
880
+ find_one: 'GET',
881
+ create: 'POST',
882
+ update: 'PATCH',
883
+ delete: 'DELETE',
884
+ batch_update: 'PATCH',
885
+ batch_delete: 'DELETE',
886
+ };
887
+ const method = String(input.method || defaultMethodByOperation[normalizedOperation] || 'GET').toUpperCase();
888
+ const rawPath = String(input.path || `/${resource}`);
889
+ const path = rawPath.replace(/\/:id\/?$/, '');
855
890
  const responseName = input.responseName || `${resource}Response`;
856
891
  const pendingName = input.pendingName || `${resource}Pending`;
857
892
  const errorName = input.errorName || `${resource}Error`;
858
- const executeName = input.executeName || (method === 'GET' ? `load${pascal}` : `${method.toLowerCase()}${pascal}`);
893
+ const executeName = input.executeName || (method === 'GET' ? `load${pascal}` : `${normalizedOperation.replace(/(^|_)([a-z])/g, (_m, _p, ch) => ch.toUpperCase()).replace(/^./, (ch) => ch.toLowerCase())}${pascal}Api`);
859
894
  const refreshName = input.refreshName || `refresh${pascal}`;
895
+ const sort = buildExtensionSort(input.sort);
896
+ const rawQuery = input.query && typeof input.query === 'object' && !Array.isArray(input.query)
897
+ ? input.query
898
+ : null;
899
+ if (rawQuery?.sort !== undefined && !sort) {
900
+ throw new Error('Pass extension sort through the structured sort input, not query.sort.');
901
+ }
902
+ const structuredQuery = rawQuery || sort
903
+ ? { ...(rawQuery || {}), ...(sort ? { sort } : {}) }
904
+ : null;
905
+ if (structuredQuery && input.queryExpression) {
906
+ throw new Error('Pass either query or queryExpression to build_extension_api_usage, not both. Use structured query plus sort for Enfyra REST ordering.');
907
+ }
908
+ const queryName = input.queryName || `${resource}Query`;
909
+ const queryExpression = structuredQuery ? queryName : input.queryExpression;
860
910
  const options = [];
861
911
  if (method !== 'GET')
862
912
  options.push(`method: ${quoteJsString(method)}`);
863
- if (input.queryExpression)
864
- options.push(`query: ${input.queryExpression}`);
913
+ if (queryExpression)
914
+ options.push(`query: ${queryExpression}`);
865
915
  if (input.bodyExpression)
866
916
  options.push(`body: ${input.bodyExpression}`);
867
917
  if (input.errorContext)
@@ -870,6 +920,7 @@ export function buildExtensionApiUsageSnippet(input = {}) {
870
920
  options.push(`onError: ${input.onErrorExpression}`);
871
921
  const optionsLiteral = options.length ? `, {\n ${options.join(',\n ')}\n}` : '';
872
922
  const lines = [
923
+ ...(structuredQuery ? [`const ${queryName} = computed(() => (${JSON.stringify(structuredQuery, null, 2)}));`, ''] : []),
873
924
  `const { data: ${responseName}, pending: ${pendingName}, error: ${errorName}, execute: ${executeName}, refresh: ${refreshName} } = useApi(${quoteJsString(path)}${optionsLiteral});`,
874
925
  ];
875
926
  if (method === 'GET') {
@@ -879,6 +930,60 @@ export function buildExtensionApiUsageSnippet(input = {}) {
879
930
  lines.push(`onMounted(() => { ${executeName}(); });`);
880
931
  }
881
932
  }
933
+ else if (normalizedOperation === 'create') {
934
+ const handlerName = input.handlerName || `create${pascal.replace(/s$/, '')}`;
935
+ const payloadName = input.payloadName || 'payload';
936
+ lines.push(...[
937
+ '',
938
+ `async function ${handlerName}(${payloadName}) {`,
939
+ ` const response = await ${executeName}({ body: ${payloadName} });`,
940
+ ' if (!response) return null;',
941
+ ' return response;',
942
+ '}',
943
+ ]);
944
+ }
945
+ else if (normalizedOperation === 'update') {
946
+ const handlerName = input.handlerName || `update${pascal.replace(/s$/, '')}`;
947
+ const recordName = input.recordName || 'record';
948
+ const bodyName = input.bodyName || 'body';
949
+ const idExpression = input.idExpression || `${recordName}.id`;
950
+ const bodyArg = bodyName === 'body' ? 'body' : `body: ${bodyName}`;
951
+ lines.push(...[
952
+ '',
953
+ `async function ${handlerName}(${recordName}, ${bodyName}) {`,
954
+ ` const response = await ${executeName}({ id: ${idExpression}, ${bodyArg} });`,
955
+ ' if (!response) return null;',
956
+ ' return response;',
957
+ '}',
958
+ ]);
959
+ }
960
+ else if (normalizedOperation === 'delete') {
961
+ const handlerName = input.handlerName || `delete${pascal.replace(/s$/, '')}`;
962
+ const recordName = input.recordName || 'record';
963
+ const idExpression = input.idExpression || `${recordName}.id`;
964
+ lines.push(...[
965
+ '',
966
+ `async function ${handlerName}(${recordName}) {`,
967
+ ` const response = await ${executeName}({ id: ${idExpression} });`,
968
+ ' if (!response) return null;',
969
+ ' return response;',
970
+ '}',
971
+ ]);
972
+ }
973
+ else if (normalizedOperation === 'batch_update' || normalizedOperation === 'batch_delete') {
974
+ const handlerName = input.handlerName || `${normalizedOperation === 'batch_update' ? 'update' : 'delete'}${pascal}Batch`;
975
+ const idsName = input.idsName || 'ids';
976
+ const bodyName = input.bodyName || 'body';
977
+ const args = normalizedOperation === 'batch_update' ? `{ ids: ${idsName}, body: ${bodyName} }` : `{ ids: ${idsName} }`;
978
+ lines.push(...[
979
+ '',
980
+ `async function ${handlerName}(${normalizedOperation === 'batch_update' ? `${idsName}, ${bodyName}` : idsName}) {`,
981
+ ` const response = await ${executeName}(${args});`,
982
+ ' if (!response) return null;',
983
+ ' return response;',
984
+ '}',
985
+ ]);
986
+ }
882
987
  else {
883
988
  const handlerName = input.handlerName || `${method.toLowerCase()}${pascal}Record`;
884
989
  lines.push(...[
@@ -892,10 +997,13 @@ export function buildExtensionApiUsageSnippet(input = {}) {
892
997
  }
893
998
  return {
894
999
  action: 'extension_api_usage_built',
1000
+ operation: normalizedOperation,
895
1001
  snippet: lines.join('\n'),
896
1002
  contract: [
897
1003
  'useApi returns refs plus execute/refresh; it does not auto-run.',
1004
+ 'The useApi path is the base route string or a () => string getter; do not pass computed refs and do not put :id placeholders in the path.',
898
1005
  'Pass query/body as objects or computed objects, not JSON.stringify strings.',
1006
+ 'For Enfyra REST ordering, use structured sort entries with field and direction; the generated query always emits one comma-separated sort string such as "-isPinned,-updatedAt", never sort arrays or field:DESC tokens.',
899
1007
  'Read normal list rows from data.value?.data or from the direct execute() response.',
900
1008
  'For mutations, call execute({ body }), execute({ id, body }), execute({ id }), or execute({ ids }) from a user action.',
901
1009
  ],
@@ -2848,6 +2956,42 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2848
2956
  assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
2849
2957
  return jsonText(buildExtensionUiSnippet(kind, input));
2850
2958
  });
2959
+ server.tool('build_extension_api_usage', [
2960
+ 'Generate a contract-safe useApi snippet for Enfyra admin extensions.',
2961
+ 'Use this instead of writing useApi calls from memory so route paths, execute({ id, body }), query/body objects, and mutation handlers follow the app composable contract.',
2962
+ 'The tool returns code only; apply it with patch_extension_code or update_extension_code and then validate/save normally.',
2963
+ ].join(' '), {
2964
+ operation: z.enum(['list', 'find_one', 'create', 'update', 'delete', 'batch_update', 'batch_delete']).default('list').describe('API usage pattern to generate. Reads use the base route with query objects; mutations append ids through execute options.'),
2965
+ resource: z.string().default('items').describe('Resource variable base name, e.g. notes, projects, messages.'),
2966
+ path: z.string().optional().describe('Base API route path such as /notes. Do not include /:id; the builder strips a trailing /:id if provided.'),
2967
+ query: z.record(z.any()).optional().describe('Static Enfyra query object. Use this with sort for filter/page/limit reads; do not JSON.stringify it or put sort arrays inside it.'),
2968
+ queryExpression: z.string().optional().describe('Raw Vue expression for query object/computed. Do not JSON.stringify.'),
2969
+ queryName: z.string().optional().describe('Variable name for the generated computed query when query is provided.'),
2970
+ sort: z.array(z.object({
2971
+ field: z.string().min(1).describe('Metadata field or supported aggregate sort expression.'),
2972
+ direction: z.enum(['asc', 'desc']).default('asc').describe('Enfyra sort direction.'),
2973
+ })).optional().describe('Structured sort order. The builder emits one Enfyra REST sort string, for example [{ field: "isPinned", direction: "desc" }, { field: "updatedAt", direction: "desc" }] becomes "-isPinned,-updatedAt".'),
2974
+ bodyExpression: z.string().optional().describe('Raw Vue expression for default body object/computed when useful. Do not JSON.stringify.'),
2975
+ errorContext: z.string().optional().describe('Safe error context label for useApi error reporting.'),
2976
+ responseName: z.string().optional().describe('Optional data ref variable name.'),
2977
+ pendingName: z.string().optional().describe('Optional pending ref variable name.'),
2978
+ errorName: z.string().optional().describe('Optional error ref variable name.'),
2979
+ executeName: z.string().optional().describe('Optional execute alias name.'),
2980
+ refreshName: z.string().optional().describe('Optional refresh alias name.'),
2981
+ rowsName: z.string().optional().describe('Optional computed rows variable for list/find_one operations.'),
2982
+ handlerName: z.string().optional().describe('Optional generated handler function name for mutations.'),
2983
+ recordName: z.string().optional().describe('Record parameter name for update/delete handlers.'),
2984
+ payloadName: z.string().optional().describe('Payload parameter name for create handlers.'),
2985
+ bodyName: z.string().optional().describe('Body parameter name for update/batch_update handlers.'),
2986
+ idsName: z.string().optional().describe('Ids parameter name for batch handlers.'),
2987
+ idExpression: z.string().optional().describe('Raw id expression for update/delete handlers. Defaults to record.id.'),
2988
+ autoLoad: z.boolean().optional().default(true).describe('For reads, generate onMounted(() => execute()).'),
2989
+ onErrorExpression: z.string().optional().describe('Raw onError handler expression when custom handling is needed.'),
2990
+ extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
2991
+ }, async ({ extensionKnowledgeAckKey, ...input }) => {
2992
+ assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
2993
+ return jsonText(buildExtensionApiUsageSnippet(input));
2994
+ });
2851
2995
  server.tool('build_extension_drawer', [
2852
2996
  'Generate a contract-safe CommonDrawer Vue snippet for Enfyra admin extensions.',
2853
2997
  'Use this before writing or patching drawer/editing workflows so the model does not have to remember CommonDrawer slots, footer action props, full-width fields, or button type rules.',