@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.
- package/dist/lib/platform-operation-tools.d.ts +1 -0
- package/dist/lib/platform-operation-tools.js +149 -5
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +5 -2
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/runtime-cache-socket.d.ts +2 -2
- package/dist/lib/runtime-cache-socket.js +67 -21
- package/dist/lib/runtime-cache-socket.js.map +1 -1
- package/dist/lib/toolset-filter.js +1 -0
- package/dist/lib/toolset-filter.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
854
|
-
const
|
|
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}` : `${
|
|
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 (
|
|
864
|
-
options.push(`query: ${
|
|
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.',
|