@enfyra/mcp-server 0.0.85 → 0.0.86
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/package.json +1 -1
- package/src/lib/mcp-examples.js +47 -47
- package/src/lib/mcp-instructions.js +64 -64
- package/src/lib/mutation-guards.js +18 -18
- package/src/lib/route-permission-tools.js +2 -2
- package/src/lib/table-tools.js +23 -23
- package/src/mcp-server-entry.mjs +143 -151
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -40,52 +40,52 @@ initAuth(ENFYRA_API_URL, ENFYRA_API_TOKEN);
|
|
|
40
40
|
const CAPABILITY_AREAS = [
|
|
41
41
|
{
|
|
42
42
|
area: 'Schema and metadata',
|
|
43
|
-
tables: ['
|
|
44
|
-
workflow: 'Use table tools for table/column/relation schema changes.
|
|
43
|
+
tables: ['enfyra_table', 'enfyra_column', 'enfyra_relation', 'enfyra_schema_migration'],
|
|
44
|
+
workflow: 'Use table tools for table/column/relation schema changes. enfyra_column and enfyra_session are internal/no-route; do not CRUD them directly.',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
area: 'Dynamic REST API',
|
|
48
|
-
tables: ['
|
|
49
|
-
workflow: 'Create custom paths with create_route without mainTableId, then add handlers/hooks. mainTableId is only for canonical table routes like /table_name. Query
|
|
48
|
+
tables: ['enfyra_route', 'enfyra_route_handler', 'enfyra_pre_hook', 'enfyra_post_hook', 'enfyra_route_permission', 'enfyra_method'],
|
|
49
|
+
workflow: 'Create custom paths with create_route without mainTableId, then add handlers/hooks. mainTableId is only for canonical table routes like /table_name. Query enfyra_method before assigning route methods.',
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
area: 'Auth, roles, sessions, OAuth',
|
|
53
|
-
tables: ['
|
|
53
|
+
tables: ['enfyra_user', 'enfyra_role', 'enfyra_api_token', 'enfyra_session', 'enfyra_oauth_config', 'enfyra_oauth_account'],
|
|
54
54
|
workflow: 'MCP auth exchanges ENFYRA_API_TOKEN through /auth/token/exchange. Configure an API token from Enfyra admin UI /me.',
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
area: 'Guards and permissions',
|
|
58
|
-
tables: ['
|
|
58
|
+
tables: ['enfyra_guard', 'enfyra_guard_rule', 'enfyra_field_permission', 'enfyra_column_rule'],
|
|
59
59
|
workflow: 'Use route guard metadata for request gating, field permissions for record field access, and column rules for body validation.',
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
area: 'GraphQL',
|
|
63
|
-
tables: ['
|
|
64
|
-
workflow: 'Enable per table through
|
|
63
|
+
tables: ['enfyra_graphql'],
|
|
64
|
+
workflow: 'Enable per table through enfyra_graphql or update_table graphqlEnabled. GraphQL requires Bearer auth.',
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
area: 'Files and storage',
|
|
68
|
-
tables: ['
|
|
68
|
+
tables: ['enfyra_file', 'enfyra_file_permission', 'enfyra_folder', 'enfyra_storage_config'],
|
|
69
69
|
workflow: 'Use file endpoints/helpers for uploads and asset streaming; metadata tables describe files, permissions, folders, and storage backends.',
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
area: 'WebSocket',
|
|
73
|
-
tables: ['
|
|
73
|
+
tables: ['enfyra_websocket', 'enfyra_websocket_event'],
|
|
74
74
|
workflow: 'Socket.IO gateways/events are metadata-backed. Use admin test runner for handler scripts before relying on a real client.',
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
area: 'Flows',
|
|
78
|
-
tables: ['
|
|
78
|
+
tables: ['enfyra_flow', 'enfyra_flow_step', 'enfyra_flow_execution'],
|
|
79
79
|
workflow: 'Create flows as small operation-sized steps via CRUD, test steps with test_flow_step/run_admin_test, trigger with trigger_flow. Split oversized scripts instead of adding more work to one step.',
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
area: 'Extensions, menus, packages',
|
|
83
|
-
tables: ['
|
|
84
|
-
workflow: 'Extensions are Vue SFC records. Use install_package for
|
|
83
|
+
tables: ['enfyra_extension', 'enfyra_menu', 'enfyra_package', 'enfyra_bootstrap_script'],
|
|
84
|
+
workflow: 'Extensions are Vue SFC records. Use install_package for enfyra_package rather than raw CRUD.',
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
area: 'Settings and platform config',
|
|
88
|
-
tables: ['
|
|
88
|
+
tables: ['enfyra_setting', 'enfyra_cors_origin'],
|
|
89
89
|
workflow: 'Settings and CORS origins are metadata-backed platform configuration.',
|
|
90
90
|
},
|
|
91
91
|
];
|
|
@@ -129,14 +129,14 @@ const FIELD_PERMISSION_CONDITION_OPERATORS = [
|
|
|
129
129
|
];
|
|
130
130
|
|
|
131
131
|
const SCRIPT_BACKED_TABLES = [
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
132
|
+
'enfyra_route_handler',
|
|
133
|
+
'enfyra_pre_hook',
|
|
134
|
+
'enfyra_post_hook',
|
|
135
|
+
'enfyra_flow_step',
|
|
136
|
+
'enfyra_websocket_event',
|
|
137
|
+
'enfyra_websocket',
|
|
138
|
+
'enfyra_graphql',
|
|
139
|
+
'enfyra_bootstrap_script',
|
|
140
140
|
];
|
|
141
141
|
|
|
142
142
|
const SCRIPT_SOURCE_FIELDS = [
|
|
@@ -510,7 +510,7 @@ function sourcePreview(source, aroundText) {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
function scriptRecordLabel(tableName, record) {
|
|
513
|
-
const method = record.method?.name ||
|
|
513
|
+
const method = record.method?.name || null;
|
|
514
514
|
const route = record.route?.path || null;
|
|
515
515
|
const flow = record.flow?.name || null;
|
|
516
516
|
const gateway = record.gateway?.path || null;
|
|
@@ -530,21 +530,21 @@ function scriptRecordLabel(tableName, record) {
|
|
|
530
530
|
function scriptTraceFields(tableName) {
|
|
531
531
|
const common = 'id,_id,name,key,eventName,sourceCode,handlerScript,connectionHandlerScript,code,scriptLanguage';
|
|
532
532
|
const byTable = {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
533
|
+
enfyra_route_handler: `${common},route.id,route.path,method.id,method.name`,
|
|
534
|
+
enfyra_pre_hook: `${common},route.id,route.path,methods.id,methods.name,isGlobal`,
|
|
535
|
+
enfyra_post_hook: `${common},route.id,route.path,methods.id,methods.name,isGlobal`,
|
|
536
|
+
enfyra_flow_step: `${common},flow.id,flow.name`,
|
|
537
|
+
enfyra_websocket_event: `${common},gateway.id,gateway.path`,
|
|
538
|
+
enfyra_websocket: `${common},path`,
|
|
539
|
+
enfyra_graphql: `${common},table.id,table.name`,
|
|
540
|
+
enfyra_bootstrap_script: common,
|
|
541
541
|
};
|
|
542
542
|
return byTable[tableName] || '*';
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
async function findMethodRecordByName(method) {
|
|
546
546
|
const filter = encodeURIComponent(JSON.stringify({ name: { _eq: method } }));
|
|
547
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
547
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
|
|
548
548
|
return unwrapData(result)[0] || null;
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -581,7 +581,7 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
|
|
|
581
581
|
});
|
|
582
582
|
|
|
583
583
|
server.tool('get_table_metadata', 'Get concise metadata for a specific table by name', {
|
|
584
|
-
tableName: z.string().describe('Table name (e.g., "
|
|
584
|
+
tableName: z.string().describe('Table name (e.g., "enfyra_user", "enfyra_route")'),
|
|
585
585
|
includeFull: z.boolean().optional().default(false).describe('Return full raw table metadata. Default false to keep MCP context small.'),
|
|
586
586
|
}, async ({ tableName, includeFull }) => {
|
|
587
587
|
const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
|
|
@@ -622,8 +622,8 @@ server.tool(
|
|
|
622
622
|
async () => {
|
|
623
623
|
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
624
624
|
const [routesResult, methodsResult] = await Promise.all([
|
|
625
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
626
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
625
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*&limit=1000'),
|
|
626
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_method?limit=100'),
|
|
627
627
|
]);
|
|
628
628
|
|
|
629
629
|
const tables = normalizeTables(metadata);
|
|
@@ -631,9 +631,9 @@ server.tool(
|
|
|
631
631
|
const routes = summarizeRoutes(routesResult);
|
|
632
632
|
const routeTables = new Set(routes.map((route) => route.mainTable).filter(Boolean));
|
|
633
633
|
const noRouteTables = tableNames.filter((name) => !routeTables.has(name));
|
|
634
|
-
const relationTable = tables.find((table) => table?.name === '
|
|
635
|
-
const tableDefinition = tables.find((table) => table?.name === '
|
|
636
|
-
const gqlDefinition = tables.find((table) => table?.name === '
|
|
634
|
+
const relationTable = tables.find((table) => table?.name === 'enfyra_relation');
|
|
635
|
+
const tableDefinition = tables.find((table) => table?.name === 'enfyra_table');
|
|
636
|
+
const gqlDefinition = tables.find((table) => table?.name === 'enfyra_graphql');
|
|
637
637
|
const routeTableList = [...routeTables].sort();
|
|
638
638
|
|
|
639
639
|
const payload = {
|
|
@@ -643,7 +643,7 @@ server.tool(
|
|
|
643
643
|
routes: routes.length,
|
|
644
644
|
methods: methodsResult?.data?.length || 0,
|
|
645
645
|
},
|
|
646
|
-
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name
|
|
646
|
+
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name })),
|
|
647
647
|
capabilityAreas: CAPABILITY_AREAS.map((item) => ({
|
|
648
648
|
...item,
|
|
649
649
|
presentTables: item.tables.filter((table) => tableNames.includes(table)),
|
|
@@ -659,12 +659,12 @@ server.tool(
|
|
|
659
659
|
customRouteWorkflow: 'For a new endpoint use create_route without mainTableId, then create_handler/create_pre_hook/create_post_hook. Do not create a table just to get a path.',
|
|
660
660
|
},
|
|
661
661
|
schemaManagement: {
|
|
662
|
-
createTable: 'POST /
|
|
663
|
-
updateTable: 'PATCH /
|
|
664
|
-
columns: '
|
|
665
|
-
relations: routeTables.has('
|
|
666
|
-
? '
|
|
667
|
-
: 'Use create_relation/delete_relation or
|
|
662
|
+
createTable: 'POST /enfyra_table supports isSingleRecord at create time and supports columns and relations arrays in the same cascade call. MCP create_table exposes isSingleRecord, columns, and relations directly. It does not accept alias at create time; table name drives the default route/schema behavior.',
|
|
663
|
+
updateTable: 'PATCH /enfyra_table/:id is the canonical path for table property changes and column/relation schema changes.',
|
|
664
|
+
columns: 'enfyra_column has no REST route; use create_table/create_column/update_column/delete_column.',
|
|
665
|
+
relations: routeTables.has('enfyra_relation')
|
|
666
|
+
? 'enfyra_relation has a REST route for reads/metadata, but canonical schema migration is create_relation/delete_relation or enfyra_table PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.'
|
|
667
|
+
: 'Use create_relation/delete_relation or enfyra_table PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.',
|
|
668
668
|
relationCascadeFkContract: 'Do not ask for or send physical FK/junction column names in relation create/update payloads. Enfyra derives fk/junction columns from relation propertyName/table metadata and hides FK columns from app schema/forms. Use targetTable, type, propertyName, inversePropertyName or mappedBy, isNullable, onDelete.',
|
|
669
669
|
tableDefinitionRelations: (tableDefinition?.relations || []).map((rel) => rel.propertyName),
|
|
670
670
|
relationDefinitionRelations: (relationTable?.relations || []).map((rel) => rel.propertyName),
|
|
@@ -677,10 +677,10 @@ server.tool(
|
|
|
677
677
|
graphql: {
|
|
678
678
|
endpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql`,
|
|
679
679
|
schemaEndpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql-schema`,
|
|
680
|
-
enablement: 'A table appears in GraphQL when
|
|
680
|
+
enablement: 'A table appears in GraphQL when enfyra_graphql has an enabled row for that table. REST route availableMethods does not enable GraphQL.',
|
|
681
681
|
auth: 'GraphQL currently requires Authorization: Bearer <accessToken>; REST publicMethods does not make GraphQL anonymous.',
|
|
682
|
-
management: routeTables.has('
|
|
683
|
-
? 'Use update_table graphqlEnabled or create/update records on
|
|
682
|
+
management: routeTables.has('enfyra_graphql')
|
|
683
|
+
? 'Use update_table graphqlEnabled or create/update records on enfyra_graphql, then reload_graphql if needed.'
|
|
684
684
|
: 'Use update_table graphqlEnabled, then reload_graphql if needed.',
|
|
685
685
|
gqlDefinitionColumns: (gqlDefinition?.columns || []).map((column) => column.name),
|
|
686
686
|
},
|
|
@@ -711,13 +711,13 @@ server.tool(
|
|
|
711
711
|
settingsResult,
|
|
712
712
|
meResult,
|
|
713
713
|
] = await Promise.all([
|
|
714
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
715
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
716
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
717
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
718
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
719
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
720
|
-
fetchAPI(ENFYRA_API_URL, '/
|
|
714
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000'),
|
|
715
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_method?limit=100'),
|
|
716
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_graphql?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
717
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_flow?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
718
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_websocket?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
719
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_storage_config?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
720
|
+
fetchAPI(ENFYRA_API_URL, '/enfyra_setting?limit=1000').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
721
721
|
fetchAPI(ENFYRA_API_URL, '/me').catch((error) => ({ error: String(error.message || error), data: [] })),
|
|
722
722
|
]);
|
|
723
723
|
|
|
@@ -746,7 +746,7 @@ server.tool(
|
|
|
746
746
|
storageConfigs: storageResult?.data?.length || 0,
|
|
747
747
|
settings: settingsResult?.data?.length || 0,
|
|
748
748
|
},
|
|
749
|
-
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name
|
|
749
|
+
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name })),
|
|
750
750
|
routeRuntime: {
|
|
751
751
|
routePattern: 'GET/POST /<route-path>; PATCH/DELETE /<route-path>/:id; no dynamic GET /<route-path>/:id.',
|
|
752
752
|
adminRoutes: adminRoutes.map((route) => route.path).sort(),
|
|
@@ -786,7 +786,7 @@ server.tool(
|
|
|
786
786
|
},
|
|
787
787
|
async ({ tableName }) => {
|
|
788
788
|
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
789
|
-
const routesResult = await fetchAPI(ENFYRA_API_URL, '/
|
|
789
|
+
const routesResult = await fetchAPI(ENFYRA_API_URL, '/enfyra_route?fields=path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled&limit=1000');
|
|
790
790
|
const tables = normalizeTables(metadata);
|
|
791
791
|
const routes = summarizeRoutes(routesResult);
|
|
792
792
|
const table = tableName ? tables.find((item) => item.name === tableName) : null;
|
|
@@ -826,8 +826,8 @@ server.tool(
|
|
|
826
826
|
backendNotes: {
|
|
827
827
|
primaryKey: 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
|
|
828
828
|
relationNames: 'API relation operations use relation propertyName, not physical FK column names.',
|
|
829
|
-
relationCascadeFkContract: 'When creating relations through create_table/create_relation/
|
|
830
|
-
graphql: 'GraphQL query args also accept filter/sort/page/limit, but GraphQL requires Bearer auth and table enablement via
|
|
829
|
+
relationCascadeFkContract: 'When creating relations through create_table/create_relation/enfyra_table PATCH, never provide fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. These are physical implementation details derived by Enfyra and hidden from app schema/forms.',
|
|
830
|
+
graphql: 'GraphQL query args also accept filter/sort/page/limit, but GraphQL requires Bearer auth and table enablement via enfyra_graphql.',
|
|
831
831
|
},
|
|
832
832
|
table: tableName
|
|
833
833
|
? {
|
|
@@ -901,7 +901,7 @@ server.tool(
|
|
|
901
901
|
throws: '@THROW400 through @THROW503 and @THROW map to $ctx.$throw helpers.',
|
|
902
902
|
helpers: {
|
|
903
903
|
crypto: '$ctx.$helpers.$crypto exposes bounded runtime crypto helpers: randomUUID(), randomBytes(size, encoding), sha256(value, encoding), hmacSha256(value, secret, encoding), and generateSshKeyPair(comment). Use generateSshKeyPair for SSH key material. Do not use legacy $ctx.$helpers.$ssh.',
|
|
904
|
-
files: '$ctx.$storage.$upload and $ctx.$storage.$update accept file: @UPLOADED_FILE for request uploads and stream from the server temp file path. $ctx.$storage.$registerFile creates a
|
|
904
|
+
files: '$ctx.$storage.$upload and $ctx.$storage.$update accept file: @UPLOADED_FILE for request uploads and stream from the server temp file path. $ctx.$storage.$registerFile creates a enfyra_file record for an object that already exists in storage without uploading bytes. Use buffer only for small generated/transformed files; do not use @UPLOADED_FILE.buffer.',
|
|
905
905
|
},
|
|
906
906
|
env: '$ctx.$env exposes a sanitized process env snapshot with exact sensitive keys removed: DB_URI, DB_REPLICA_URIS, REDIS_URI, SECRET_KEY, and ADMIN_PASSWORD. Store app secrets in unpublished isEncrypted fields instead of reading them from $env.',
|
|
907
907
|
},
|
|
@@ -948,7 +948,7 @@ server.tool(
|
|
|
948
948
|
extensionVueSfc: {
|
|
949
949
|
runs: 'Frontend extension code, not server sandbox.',
|
|
950
950
|
data: ['Vue/Nuxt composables', 'Enfyra composables', 'auto-resolved UI components'],
|
|
951
|
-
caveat: 'No import statements; save as
|
|
951
|
+
caveat: 'No import statements; save as enfyra_extension Vue SFC record.',
|
|
952
952
|
},
|
|
953
953
|
},
|
|
954
954
|
helpers: {
|
|
@@ -961,7 +961,7 @@ server.tool(
|
|
|
961
961
|
},
|
|
962
962
|
socketInHttpOrFlow: 'HTTP/flow context can emitToUser/emitToRoom/emitToGateway/broadcast, but cannot reply/join/leave/disconnect/emitToCurrentRoom/broadcastToRoom because there is no bound socket. emitToRoom requires an explicit gateway path: emitToRoom(path, room, event, data).',
|
|
963
963
|
packages: 'Server packages installed through install_package are exposed as $ctx.$pkgs.packageName in server scripts.',
|
|
964
|
-
files: 'Upload helpers are on $storage; raw create_record on
|
|
964
|
+
files: 'Upload helpers are on $storage; raw create_record on enfyra_file is not equivalent to multipart upload/storage rollback. For multipart request files, pass file: @UPLOADED_FILE to @STORAGE.$upload/@STORAGE.$update so Enfyra streams from disk-backed temp storage. Use @STORAGE.$registerFile only when the object already exists in storage and the script should create the enfyra_file record without uploading bytes. Use buffer only for small generated files.',
|
|
965
965
|
},
|
|
966
966
|
adminTesting: {
|
|
967
967
|
flowStep: 'Use test_flow_step or run_admin_test(kind=flow_step).',
|
|
@@ -985,7 +985,7 @@ server.tool(
|
|
|
985
985
|
'Auth: publicMethods on a route can allow a method without Bearer; otherwise JWT + routePermissions — see server instructions.',
|
|
986
986
|
'If path might differ from table name, use get_all_routes before asserting a URL.',
|
|
987
987
|
'Same mapping as MCP tool → HTTP: query_table=GET /table?..., create_record=POST /table, update_record=PATCH /table/id, delete_record=DELETE /table/id.',
|
|
988
|
-
'GraphQL: see graphqlHttpUrl / graphqlSchemaUrl in response; enable per table via
|
|
988
|
+
'GraphQL: see graphqlHttpUrl / graphqlSchemaUrl in response; enable per table via enfyra_graphql/update_table graphqlEnabled and send Bearer auth.',
|
|
989
989
|
].join(' '),
|
|
990
990
|
{},
|
|
991
991
|
async () => {
|
|
@@ -1292,19 +1292,19 @@ server.tool(
|
|
|
1292
1292
|
'update_script_source',
|
|
1293
1293
|
[
|
|
1294
1294
|
'Update sourceCode on a script-backed record without forcing the caller to JSON-escape long code.',
|
|
1295
|
-
'Use this for
|
|
1295
|
+
'Use this for enfyra_flow_step, enfyra_route_handler, enfyra_pre_hook, enfyra_post_hook, enfyra_websocket_event, enfyra_websocket, enfyra_graphql, and enfyra_bootstrap_script.',
|
|
1296
1296
|
'The tool validates sourceCode through /admin/script/validate before saving and never accepts compiledCode.',
|
|
1297
1297
|
].join(' '),
|
|
1298
1298
|
{
|
|
1299
1299
|
tableName: z.enum([
|
|
1300
|
-
'
|
|
1301
|
-
'
|
|
1302
|
-
'
|
|
1303
|
-
'
|
|
1304
|
-
'
|
|
1305
|
-
'
|
|
1306
|
-
'
|
|
1307
|
-
'
|
|
1300
|
+
'enfyra_route_handler',
|
|
1301
|
+
'enfyra_pre_hook',
|
|
1302
|
+
'enfyra_post_hook',
|
|
1303
|
+
'enfyra_flow_step',
|
|
1304
|
+
'enfyra_websocket_event',
|
|
1305
|
+
'enfyra_websocket',
|
|
1306
|
+
'enfyra_graphql',
|
|
1307
|
+
'enfyra_bootstrap_script',
|
|
1308
1308
|
]).describe('Script-backed table to update'),
|
|
1309
1309
|
id: z.string().describe('Record ID to update'),
|
|
1310
1310
|
sourceCode: z.string().describe('Editable script sourceCode. Pass the raw code string; do not JSON-escape it yourself.'),
|
|
@@ -1370,20 +1370,19 @@ server.tool('delete_record', 'Delete a record by ID', {
|
|
|
1370
1370
|
|
|
1371
1371
|
server.tool(
|
|
1372
1372
|
'list_methods',
|
|
1373
|
-
'List
|
|
1373
|
+
'List enfyra_method records with their UI colors. Use this before creating route methods or method-colored UI.',
|
|
1374
1374
|
{},
|
|
1375
1375
|
async () => {
|
|
1376
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
1376
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_method?fields=id,_id,name,buttonColor,textColor,isSystem&sort=name&limit=0');
|
|
1377
1377
|
const methods = unwrapData(result).map((method) => ({
|
|
1378
1378
|
id: getId(method),
|
|
1379
1379
|
name: method.name,
|
|
1380
|
-
method: method.name,
|
|
1381
1380
|
buttonColor: method.buttonColor,
|
|
1382
1381
|
textColor: method.textColor,
|
|
1383
1382
|
isSystem: method.isSystem === true,
|
|
1384
1383
|
}));
|
|
1385
1384
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1386
|
-
tableName: '
|
|
1385
|
+
tableName: 'enfyra_method',
|
|
1387
1386
|
methods,
|
|
1388
1387
|
appUi: '/settings/methods',
|
|
1389
1388
|
}, null, 2) }] };
|
|
@@ -1392,7 +1391,7 @@ server.tool(
|
|
|
1392
1391
|
|
|
1393
1392
|
server.tool(
|
|
1394
1393
|
'create_method',
|
|
1395
|
-
'Create a
|
|
1394
|
+
'Create a enfyra_method record with app badge colors. Prefer this over generic create_record for enfyra_method.',
|
|
1396
1395
|
{
|
|
1397
1396
|
method: z.string().describe('Uppercase method name, e.g. GET, POST, PUT, CUSTOM_METHOD. Must start with A-Z and contain only A-Z, 0-9, or underscore.'),
|
|
1398
1397
|
buttonColor: z.string().describe('Badge background color as full hex, e.g. #dbeafe.'),
|
|
@@ -1411,15 +1410,14 @@ server.tool(
|
|
|
1411
1410
|
textColor: normalizeHexColorInput(textColor, 'textColor'),
|
|
1412
1411
|
isSystem: isSystem === true,
|
|
1413
1412
|
};
|
|
1414
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
1413
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_method', {
|
|
1415
1414
|
method: 'POST',
|
|
1416
1415
|
body: JSON.stringify(body),
|
|
1417
1416
|
});
|
|
1418
1417
|
_methodMap = null;
|
|
1419
1418
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1420
|
-
...summarizeMutationResult(result, 'created', '
|
|
1419
|
+
...summarizeMutationResult(result, 'created', 'enfyra_method'),
|
|
1421
1420
|
name: normalizedMethod,
|
|
1422
|
-
method: normalizedMethod,
|
|
1423
1421
|
appUi: '/settings/methods',
|
|
1424
1422
|
}, null, 2) }] };
|
|
1425
1423
|
},
|
|
@@ -1427,7 +1425,7 @@ server.tool(
|
|
|
1427
1425
|
|
|
1428
1426
|
server.tool(
|
|
1429
1427
|
'update_method',
|
|
1430
|
-
'Update a
|
|
1428
|
+
'Update a enfyra_method record color pair, and optionally rename non-system methods. Prefer this over generic update_record for enfyra_method.',
|
|
1431
1429
|
{
|
|
1432
1430
|
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1433
1431
|
method: z.string().optional().describe('Existing method name to find, or new name when id is provided.'),
|
|
@@ -1459,13 +1457,13 @@ server.tool(
|
|
|
1459
1457
|
throw new Error('Provide buttonColor, textColor, or a new method name.');
|
|
1460
1458
|
}
|
|
1461
1459
|
|
|
1462
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
1460
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method/${encodeURIComponent(String(targetId))}`, {
|
|
1463
1461
|
method: 'PATCH',
|
|
1464
1462
|
body: JSON.stringify(body),
|
|
1465
1463
|
});
|
|
1466
1464
|
_methodMap = null;
|
|
1467
1465
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1468
|
-
...summarizeMutationResult(result, 'updated', '
|
|
1466
|
+
...summarizeMutationResult(result, 'updated', 'enfyra_method'),
|
|
1469
1467
|
id: targetId,
|
|
1470
1468
|
appUi: '/settings/methods',
|
|
1471
1469
|
}, null, 2) }] };
|
|
@@ -1474,7 +1472,7 @@ server.tool(
|
|
|
1474
1472
|
|
|
1475
1473
|
server.tool(
|
|
1476
1474
|
'delete_method',
|
|
1477
|
-
'Preview or delete a
|
|
1475
|
+
'Preview or delete a enfyra_method record. Only delete unused custom methods; system/default methods should be kept.',
|
|
1478
1476
|
{
|
|
1479
1477
|
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1480
1478
|
method: z.string().optional().describe('Method name to find when id is omitted.'),
|
|
@@ -1491,27 +1489,26 @@ server.tool(
|
|
|
1491
1489
|
}
|
|
1492
1490
|
if (!confirm) {
|
|
1493
1491
|
if (!target) {
|
|
1494
|
-
const primaryKey = await getPrimaryFieldName('
|
|
1492
|
+
const primaryKey = await getPrimaryFieldName('enfyra_method');
|
|
1495
1493
|
const filter = encodeURIComponent(JSON.stringify({ [primaryKey]: { _eq: targetId } }));
|
|
1496
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
1494
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
|
|
1497
1495
|
target = unwrapData(result)[0] || null;
|
|
1498
1496
|
}
|
|
1499
1497
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1500
1498
|
action: 'delete_method_preview',
|
|
1501
1499
|
id: targetId,
|
|
1502
1500
|
name: target?.name,
|
|
1503
|
-
method: target?.name,
|
|
1504
1501
|
isSystem: target?.isSystem === true,
|
|
1505
1502
|
destructive: true,
|
|
1506
1503
|
warning: 'Only delete unused custom methods. Deleting a method can affect route method relations.',
|
|
1507
1504
|
next: 'Call delete_method again with confirm=true to delete.',
|
|
1508
1505
|
}, null, 2) }] };
|
|
1509
1506
|
}
|
|
1510
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
1507
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method/${encodeURIComponent(String(targetId))}`, { method: 'DELETE' });
|
|
1511
1508
|
_methodMap = null;
|
|
1512
1509
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1513
1510
|
action: 'deleted',
|
|
1514
|
-
tableName: '
|
|
1511
|
+
tableName: 'enfyra_method',
|
|
1515
1512
|
id: targetId,
|
|
1516
1513
|
statusCode: result?.statusCode,
|
|
1517
1514
|
success: result?.success,
|
|
@@ -1588,7 +1585,7 @@ server.tool(
|
|
|
1588
1585
|
let _methodMap = null;
|
|
1589
1586
|
async function getMethodMap() {
|
|
1590
1587
|
if (_methodMap) return _methodMap;
|
|
1591
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
1588
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_method?limit=0');
|
|
1592
1589
|
_methodMap = {};
|
|
1593
1590
|
for (const m of result.data) {
|
|
1594
1591
|
_methodMap[m.name] = m.id || m._id;
|
|
@@ -1616,7 +1613,6 @@ function withMethodNames(records, methodIdNameMap, field = 'methods') {
|
|
|
1616
1613
|
? record[field].map((item) => ({
|
|
1617
1614
|
...item,
|
|
1618
1615
|
name: item.name || methodIdNameMap[String(getId(item))] || null,
|
|
1619
|
-
method: item.name || methodIdNameMap[String(getId(item))] || null,
|
|
1620
1616
|
}))
|
|
1621
1617
|
: record?.[field],
|
|
1622
1618
|
}));
|
|
@@ -1638,15 +1634,15 @@ async function collectRestDefinitionState() {
|
|
|
1638
1634
|
methodIdNameMap,
|
|
1639
1635
|
] = await Promise.all([
|
|
1640
1636
|
getMetadataTables(),
|
|
1641
|
-
fetchAll('/
|
|
1642
|
-
fetchAll('/
|
|
1643
|
-
fetchAll('/
|
|
1644
|
-
fetchAll('/
|
|
1645
|
-
fetchAll('/
|
|
1646
|
-
fetchAll('/
|
|
1647
|
-
fetchAll('/
|
|
1648
|
-
fetchAll('/
|
|
1649
|
-
fetchAll('/
|
|
1637
|
+
fetchAll('/enfyra_route?limit=1000'),
|
|
1638
|
+
fetchAll('/enfyra_route_handler?limit=1000'),
|
|
1639
|
+
fetchAll('/enfyra_pre_hook?limit=1000'),
|
|
1640
|
+
fetchAll('/enfyra_post_hook?limit=1000'),
|
|
1641
|
+
fetchAll('/enfyra_route_permission?limit=1000'),
|
|
1642
|
+
fetchAll('/enfyra_guard?limit=1000'),
|
|
1643
|
+
fetchAll('/enfyra_guard_rule?limit=1000'),
|
|
1644
|
+
fetchAll('/enfyra_field_permission?limit=1000'),
|
|
1645
|
+
fetchAll('/enfyra_column_rule?limit=1000'),
|
|
1650
1646
|
getMethodIdNameMap(),
|
|
1651
1647
|
]);
|
|
1652
1648
|
|
|
@@ -1674,7 +1670,6 @@ function enrichRoute(route, state) {
|
|
|
1674
1670
|
method: item.method ? {
|
|
1675
1671
|
...item.method,
|
|
1676
1672
|
name: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
|
|
1677
|
-
method: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
|
|
1678
1673
|
} : item.method,
|
|
1679
1674
|
}, 'sourceCode'));
|
|
1680
1675
|
const routePreHooks = withMethodNames(
|
|
@@ -1703,21 +1698,18 @@ function enrichRoute(route, state) {
|
|
|
1703
1698
|
? route.availableMethods.map((method) => ({
|
|
1704
1699
|
...method,
|
|
1705
1700
|
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1706
|
-
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1707
1701
|
}))
|
|
1708
1702
|
: route.availableMethods,
|
|
1709
1703
|
publicMethods: Array.isArray(route.publicMethods)
|
|
1710
1704
|
? route.publicMethods.map((method) => ({
|
|
1711
1705
|
...method,
|
|
1712
1706
|
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1713
|
-
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1714
1707
|
}))
|
|
1715
1708
|
: route.publicMethods,
|
|
1716
1709
|
skipRoleGuardMethods: Array.isArray(route.skipRoleGuardMethods)
|
|
1717
1710
|
? route.skipRoleGuardMethods.map((method) => ({
|
|
1718
1711
|
...method,
|
|
1719
1712
|
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1720
|
-
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1721
1713
|
}))
|
|
1722
1714
|
: route.skipRoleGuardMethods,
|
|
1723
1715
|
handlers: routeHandlers,
|
|
@@ -1791,8 +1783,8 @@ server.tool(
|
|
|
1791
1783
|
'Returns the backing table, available/public methods, handlers, hooks, route permissions, guards, and exact REST URL pattern.',
|
|
1792
1784
|
].join(' '),
|
|
1793
1785
|
{
|
|
1794
|
-
path: z.string().optional().describe('Route path, e.g. /
|
|
1795
|
-
routeId: z.union([z.string(), z.number()]).optional().describe('
|
|
1786
|
+
path: z.string().optional().describe('Route path, e.g. /enfyra_user'),
|
|
1787
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('enfyra_route id. Use either path or routeId.'),
|
|
1796
1788
|
},
|
|
1797
1789
|
async ({ path, routeId }) => {
|
|
1798
1790
|
if (!path && !routeId) throw new Error('Provide path or routeId');
|
|
@@ -1962,11 +1954,11 @@ server.tool(
|
|
|
1962
1954
|
'test_rest_endpoint',
|
|
1963
1955
|
[
|
|
1964
1956
|
'Execute a real REST request against the configured Enfyra API base.',
|
|
1965
|
-
'Use this after inspecting a route or changing handlers/hooks/guards. Pass paths like /
|
|
1957
|
+
'Use this after inspecting a route or changing handlers/hooks/guards. Pass paths like /enfyra_table?limit=1, not external URLs.',
|
|
1966
1958
|
].join(' '),
|
|
1967
1959
|
{
|
|
1968
|
-
method: z.string().optional().default('GET').describe('HTTP method name. Must exist in
|
|
1969
|
-
path: z.string().describe('Enfyra API path, e.g. /
|
|
1960
|
+
method: z.string().optional().default('GET').describe('HTTP method name. Must exist in enfyra_method.name for Enfyra route-backed calls.'),
|
|
1961
|
+
path: z.string().describe('Enfyra API path, e.g. /enfyra_route?limit=1'),
|
|
1970
1962
|
query: z.string().optional().describe('Optional query params JSON object, merged onto path query string'),
|
|
1971
1963
|
body: z.string().optional().describe('Optional JSON request body string'),
|
|
1972
1964
|
headers: z.string().optional().describe('Optional headers JSON object'),
|
|
@@ -2033,7 +2025,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Call
|
|
|
2033
2025
|
fields: 'id,path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled',
|
|
2034
2026
|
limit: '1000',
|
|
2035
2027
|
});
|
|
2036
|
-
const result = await fetchAPI(ENFYRA_API_URL, `/
|
|
2028
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_route?${queryParams.toString()}`);
|
|
2037
2029
|
const routeLimit = limit || 50;
|
|
2038
2030
|
const q = search ? search.toLowerCase() : null;
|
|
2039
2031
|
const allRoutes = summarizeRoutes(result);
|
|
@@ -2063,8 +2055,8 @@ server.tool(
|
|
|
2063
2055
|
[
|
|
2064
2056
|
'**Use this when the user wants a new REST API route or path** — not `create_table`. Custom routes must omit `mainTableId`.',
|
|
2065
2057
|
'`mainTableId` is only a marker for canonical table routes such as `/orders`; do not set it for `/orders/stats`, `/reports/summary`, `/auth/login`, or any custom path.',
|
|
2066
|
-
'Do NOT create a new
|
|
2067
|
-
'availableMethods = which REST verbs the route responds to. publicMethods = which REST verbs are public (no auth). GraphQL is enabled separately through
|
|
2058
|
+
'Do NOT create a new enfyra_table only to expose an endpoint; create a route without `mainTableId`, then have the handler/hook query explicit repos such as `$ctx.$repos.orders`.',
|
|
2059
|
+
'availableMethods = which REST verbs the route responds to. publicMethods = which REST verbs are public (no auth). GraphQL is enabled separately through enfyra_graphql/update_table graphqlEnabled.',
|
|
2068
2060
|
'After creation the tool auto-reloads routes. Then create handlers for specific methods via create_handler on this route id.',
|
|
2069
2061
|
'Flow: create_route → create_handler (per method) → optionally create_pre_hook / create_post_hook → test via HTTP or admin test APIs (see server instructions).',
|
|
2070
2062
|
].join(' '),
|
|
@@ -2072,7 +2064,7 @@ server.tool(
|
|
|
2072
2064
|
path: z.string().describe('URL path, must start with / (e.g., "/my-endpoint")'),
|
|
2073
2065
|
mainTableId: z.union([z.string(), z.number()]).optional().describe('Only set for the canonical table route `/<table_name>`. Omit for every custom route.'),
|
|
2074
2066
|
methods: z.array(z.string())
|
|
2075
|
-
.describe('HTTP method names this route supports (availableMethods). Each value must exist in
|
|
2067
|
+
.describe('HTTP method names this route supports (availableMethods). Each value must exist in enfyra_method.name. Common: ["GET","POST","PATCH","DELETE"].'),
|
|
2076
2068
|
publicMethods: z.array(z.string()).optional()
|
|
2077
2069
|
.describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
|
|
2078
2070
|
isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
|
|
@@ -2099,7 +2091,7 @@ server.tool(
|
|
|
2099
2091
|
body.publicMethods = resolveMethodIds(methodMap, publicMethods);
|
|
2100
2092
|
}
|
|
2101
2093
|
|
|
2102
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2094
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_route', {
|
|
2103
2095
|
method: 'POST',
|
|
2104
2096
|
body: JSON.stringify(body),
|
|
2105
2097
|
});
|
|
@@ -2117,7 +2109,7 @@ server.tool(
|
|
|
2117
2109
|
publicMethods: publicMethods || [],
|
|
2118
2110
|
},
|
|
2119
2111
|
routeReload,
|
|
2120
|
-
next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET", sourceCode }) for custom code. Create extra
|
|
2112
|
+
next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET", sourceCode }) for custom code. Create extra enfyra_method.name rows first for custom methods such as PUT.`,
|
|
2121
2113
|
}, null, 2) }] };
|
|
2122
2114
|
},
|
|
2123
2115
|
);
|
|
@@ -2135,7 +2127,7 @@ server.tool(
|
|
|
2135
2127
|
{
|
|
2136
2128
|
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
2137
2129
|
method: z.string().optional()
|
|
2138
|
-
.describe('Single
|
|
2130
|
+
.describe('Single enfyra_method.name to create. Prefer this for one handler.'),
|
|
2139
2131
|
methods: z.array(z.string()).optional()
|
|
2140
2132
|
.describe('Batch create multiple handlers. Use only when the same sourceCode applies to every method.'),
|
|
2141
2133
|
sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
|
|
@@ -2147,7 +2139,7 @@ server.tool(
|
|
|
2147
2139
|
if (methodNames.length === 0) throw new Error('Provide method or methods');
|
|
2148
2140
|
const methodMap = await getMethodMap();
|
|
2149
2141
|
const results = [];
|
|
2150
|
-
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, '
|
|
2142
|
+
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, 'enfyra_route_handler', {
|
|
2151
2143
|
sourceCode,
|
|
2152
2144
|
scriptLanguage,
|
|
2153
2145
|
});
|
|
@@ -2159,7 +2151,7 @@ server.tool(
|
|
|
2159
2151
|
const body = { route: { id: routeId }, method: { id: methodId }, sourceCode, scriptLanguage };
|
|
2160
2152
|
if (timeout) body.timeout = timeout;
|
|
2161
2153
|
|
|
2162
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2154
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_route_handler', {
|
|
2163
2155
|
method: 'POST',
|
|
2164
2156
|
body: JSON.stringify(body),
|
|
2165
2157
|
});
|
|
@@ -2206,12 +2198,12 @@ server.tool(
|
|
|
2206
2198
|
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled }) => {
|
|
2207
2199
|
const methodMap = await getMethodMap();
|
|
2208
2200
|
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
2209
|
-
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, '
|
|
2201
|
+
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, 'enfyra_pre_hook', {
|
|
2210
2202
|
sourceCode: code,
|
|
2211
2203
|
scriptLanguage,
|
|
2212
2204
|
});
|
|
2213
2205
|
|
|
2214
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2206
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_pre_hook', {
|
|
2215
2207
|
method: 'POST',
|
|
2216
2208
|
body: JSON.stringify({
|
|
2217
2209
|
route: { id: routeId },
|
|
@@ -2260,12 +2252,12 @@ server.tool(
|
|
|
2260
2252
|
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled }) => {
|
|
2261
2253
|
const methodMap = await getMethodMap();
|
|
2262
2254
|
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
2263
|
-
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, '
|
|
2255
|
+
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, 'enfyra_post_hook', {
|
|
2264
2256
|
sourceCode: code,
|
|
2265
2257
|
scriptLanguage,
|
|
2266
2258
|
});
|
|
2267
2259
|
|
|
2268
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2260
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_post_hook', {
|
|
2269
2261
|
method: 'POST',
|
|
2270
2262
|
body: JSON.stringify({
|
|
2271
2263
|
route: { id: routeId },
|
|
@@ -2320,7 +2312,7 @@ server.tool(
|
|
|
2320
2312
|
isEnabled,
|
|
2321
2313
|
column: { id: getId(column) },
|
|
2322
2314
|
};
|
|
2323
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2315
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_column_rule', {
|
|
2324
2316
|
method: 'POST',
|
|
2325
2317
|
body: JSON.stringify(body),
|
|
2326
2318
|
});
|
|
@@ -2367,7 +2359,7 @@ server.tool(
|
|
|
2367
2359
|
} else {
|
|
2368
2360
|
body.relation = { id: getId(resolveFieldOrThrow(table, relationName, 'relation')) };
|
|
2369
2361
|
}
|
|
2370
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2362
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_field_permission', {
|
|
2371
2363
|
method: 'POST',
|
|
2372
2364
|
body: JSON.stringify(body),
|
|
2373
2365
|
});
|
|
@@ -2382,9 +2374,9 @@ server.tool(
|
|
|
2382
2374
|
'Use this when a non-root role/user should access an authenticated route. publicMethods are for public access; route permissions are for authenticated role/user access.',
|
|
2383
2375
|
].join(' '),
|
|
2384
2376
|
{
|
|
2385
|
-
path: z.string().optional().describe('Route path, e.g. /
|
|
2377
|
+
path: z.string().optional().describe('Route path, e.g. /enfyra_user'),
|
|
2386
2378
|
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
2387
|
-
methods: z.array(z.string()).describe('REST method names this permission allows. Each value must exist in
|
|
2379
|
+
methods: z.array(z.string()).describe('REST method names this permission allows. Each value must exist in enfyra_method.name.'),
|
|
2388
2380
|
roleId: z.union([z.string(), z.number()]).optional().describe('Role id scope'),
|
|
2389
2381
|
allowedUserIds: z.array(z.union([z.string(), z.number()])).optional().describe('Specific user ids scope'),
|
|
2390
2382
|
description: z.string().optional().describe('Admin note'),
|
|
@@ -2395,7 +2387,7 @@ server.tool(
|
|
|
2395
2387
|
if (!roleId && (!allowedUserIds || allowedUserIds.length === 0)) {
|
|
2396
2388
|
throw new Error('Provide roleId or allowedUserIds');
|
|
2397
2389
|
}
|
|
2398
|
-
const routes = await fetchAll('/
|
|
2390
|
+
const routes = await fetchAll('/enfyra_route?limit=1000');
|
|
2399
2391
|
const route = routes.find((item) => (
|
|
2400
2392
|
routeId ? sameId(getId(item), routeId) : item.path === normalizeRestPath(path)
|
|
2401
2393
|
));
|
|
@@ -2409,7 +2401,7 @@ server.tool(
|
|
|
2409
2401
|
...(roleId ? { role: { id: roleId } } : {}),
|
|
2410
2402
|
...(allowedUserIds?.length ? { allowedUsers: allowedUserIds.map((id) => ({ id })) } : {}),
|
|
2411
2403
|
};
|
|
2412
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2404
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_route_permission', {
|
|
2413
2405
|
method: 'POST',
|
|
2414
2406
|
body: JSON.stringify(body),
|
|
2415
2407
|
});
|
|
@@ -2447,9 +2439,9 @@ server.tool(
|
|
|
2447
2439
|
if (roleId && roleName) throw new Error('Provide roleId or roleName, not both.');
|
|
2448
2440
|
|
|
2449
2441
|
const [routes, routePermissions, roles, methodIdNameMap] = await Promise.all([
|
|
2450
|
-
fetchAll('/
|
|
2451
|
-
fetchAll('/
|
|
2452
|
-
fetchAll('/
|
|
2442
|
+
fetchAll('/enfyra_route?limit=1000'),
|
|
2443
|
+
fetchAll('/enfyra_route_permission?limit=1000'),
|
|
2444
|
+
fetchAll('/enfyra_role?limit=1000'),
|
|
2453
2445
|
getMethodIdNameMap(),
|
|
2454
2446
|
]);
|
|
2455
2447
|
|
|
@@ -2466,8 +2458,8 @@ server.tool(
|
|
|
2466
2458
|
const expectedMethods = normalizeMethodNames(methods || []);
|
|
2467
2459
|
const payload = {
|
|
2468
2460
|
guidance: {
|
|
2469
|
-
publicAccess: 'publicMethods bypass RoleGuard and do not require
|
|
2470
|
-
authenticatedAccess: 'For non-public methods, Enfyra admin UI PermissionGate and backend RoleGuard both expect enabled
|
|
2461
|
+
publicAccess: 'publicMethods bypass RoleGuard and do not require enfyra_route_permission.',
|
|
2462
|
+
authenticatedAccess: 'For non-public methods, Enfyra admin UI PermissionGate and backend RoleGuard both expect enabled enfyra_route_permission rows with matching route + HTTP method.',
|
|
2471
2463
|
directUserAccess: 'allowedRoutePermissions on /me represent direct user-scoped route permissions; role.routePermissions represent role-scoped permissions.',
|
|
2472
2464
|
},
|
|
2473
2465
|
expectedScope: {
|
|
@@ -2492,7 +2484,7 @@ server.tool(
|
|
|
2492
2484
|
'ensure_route_access',
|
|
2493
2485
|
[
|
|
2494
2486
|
'Create or update authenticated route access for one role/user scope.',
|
|
2495
|
-
'Use this instead of raw
|
|
2487
|
+
'Use this instead of raw enfyra_route_permission CRUD when fixing 403s. It resolves roleName/route/method ids, validates route.availableMethods, merges existing permission methods by default, and reloads routes.',
|
|
2496
2488
|
].join(' '),
|
|
2497
2489
|
{
|
|
2498
2490
|
path: z.string().optional().describe('Route path, e.g. /orders'),
|
|
@@ -2514,9 +2506,9 @@ server.tool(
|
|
|
2514
2506
|
}
|
|
2515
2507
|
|
|
2516
2508
|
const [routes, routePermissions, roles, methodMap, methodIdNameMap] = await Promise.all([
|
|
2517
|
-
fetchAll('/
|
|
2518
|
-
fetchAll('/
|
|
2519
|
-
fetchAll('/
|
|
2509
|
+
fetchAll('/enfyra_route?limit=1000'),
|
|
2510
|
+
fetchAll('/enfyra_route_permission?limit=1000'),
|
|
2511
|
+
fetchAll('/enfyra_role?limit=1000'),
|
|
2520
2512
|
getMethodMap(),
|
|
2521
2513
|
getMethodIdNameMap(),
|
|
2522
2514
|
]);
|
|
@@ -2547,7 +2539,7 @@ server.tool(
|
|
|
2547
2539
|
methods: methodRefs,
|
|
2548
2540
|
...(description !== undefined ? { description } : {}),
|
|
2549
2541
|
};
|
|
2550
|
-
result = await fetchAPI(ENFYRA_API_URL, `/
|
|
2542
|
+
result = await fetchAPI(ENFYRA_API_URL, `/enfyra_route_permission/${encodeURIComponent(String(getId(existing)))}`, {
|
|
2551
2543
|
method: 'PATCH',
|
|
2552
2544
|
body: JSON.stringify(patchBody),
|
|
2553
2545
|
});
|
|
@@ -2561,7 +2553,7 @@ server.tool(
|
|
|
2561
2553
|
...(scope.roleId ? { role: { id: scope.roleId } } : {}),
|
|
2562
2554
|
...(scope.allowedUserIds.length ? { allowedUsers: scope.allowedUserIds.map((id) => ({ id })) } : {}),
|
|
2563
2555
|
};
|
|
2564
|
-
result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2556
|
+
result = await fetchAPI(ENFYRA_API_URL, '/enfyra_route_permission', {
|
|
2565
2557
|
method: 'POST',
|
|
2566
2558
|
body: JSON.stringify(createBody),
|
|
2567
2559
|
});
|
|
@@ -2619,7 +2611,7 @@ server.tool(
|
|
|
2619
2611
|
async ({ name, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules }) => {
|
|
2620
2612
|
let route = null;
|
|
2621
2613
|
if (!isGlobal && (routeId || path)) {
|
|
2622
|
-
const routes = await fetchAll('/
|
|
2614
|
+
const routes = await fetchAll('/enfyra_route?limit=1000');
|
|
2623
2615
|
route = routes.find((item) => (
|
|
2624
2616
|
routeId ? sameId(getId(item), routeId) : item.path === normalizeRestPath(path)
|
|
2625
2617
|
));
|
|
@@ -2637,7 +2629,7 @@ server.tool(
|
|
|
2637
2629
|
...(route ? { route: { id: getId(route) } } : {}),
|
|
2638
2630
|
...(methods?.length ? { methods: resolveMethodIds(methodMap, methods) } : {}),
|
|
2639
2631
|
};
|
|
2640
|
-
const guard = await fetchAPI(ENFYRA_API_URL, '/
|
|
2632
|
+
const guard = await fetchAPI(ENFYRA_API_URL, '/enfyra_guard', {
|
|
2641
2633
|
method: 'POST',
|
|
2642
2634
|
body: JSON.stringify(guardBody),
|
|
2643
2635
|
});
|
|
@@ -2653,7 +2645,7 @@ server.tool(
|
|
|
2653
2645
|
guard: { id: resultRecordId(guard) },
|
|
2654
2646
|
...(Array.isArray(rule.userIds) && rule.userIds.length ? { users: rule.userIds.map((id) => ({ id })) } : {}),
|
|
2655
2647
|
};
|
|
2656
|
-
createdRules.push(await fetchAPI(ENFYRA_API_URL, '/
|
|
2648
|
+
createdRules.push(await fetchAPI(ENFYRA_API_URL, '/enfyra_guard_rule', {
|
|
2657
2649
|
method: 'POST',
|
|
2658
2650
|
body: JSON.stringify(ruleBody),
|
|
2659
2651
|
}));
|
|
@@ -2756,7 +2748,7 @@ server.tool('get_current_user', 'Get current authenticated user info', {}, async
|
|
|
2756
2748
|
});
|
|
2757
2749
|
|
|
2758
2750
|
server.tool('get_all_roles', 'Get all role definitions', {}, async () => {
|
|
2759
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2751
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_role?limit=100');
|
|
2760
2752
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2761
2753
|
});
|
|
2762
2754
|
|
|
@@ -2808,7 +2800,7 @@ server.tool(
|
|
|
2808
2800
|
server.tool(
|
|
2809
2801
|
'install_package',
|
|
2810
2802
|
[
|
|
2811
|
-
'Install an NPM package on Enfyra. Searches NPM registry for exact version, then creates
|
|
2803
|
+
'Install an NPM package on Enfyra. Searches NPM registry for exact version, then creates enfyra_package record.',
|
|
2812
2804
|
'Enfyra handles the actual yarn add internally based on type.',
|
|
2813
2805
|
'Type "Server" = available in handlers/hooks as $ctx.$pkgs.packageName.',
|
|
2814
2806
|
'Type "App" = available in extensions via getPackages().',
|
|
@@ -2838,7 +2830,7 @@ server.tool(
|
|
|
2838
2830
|
|
|
2839
2831
|
// Step 2: Check if already installed (same name AND type)
|
|
2840
2832
|
const checkFilter = JSON.stringify({ name: { _eq: name }, type: { _eq: type } });
|
|
2841
|
-
const existing = await fetchAPI(ENFYRA_API_URL, `/
|
|
2833
|
+
const existing = await fetchAPI(ENFYRA_API_URL, `/enfyra_package?filter=${encodeURIComponent(checkFilter)}&limit=1`);
|
|
2842
2834
|
if (existing.data && existing.data.length > 0) {
|
|
2843
2835
|
return {
|
|
2844
2836
|
content: [{
|
|
@@ -2853,7 +2845,7 @@ server.tool(
|
|
|
2853
2845
|
const userId = me.data?.[0]?.id || me.data?.[0]?._id;
|
|
2854
2846
|
if (!userId) throw new Error('Cannot get current user ID');
|
|
2855
2847
|
|
|
2856
|
-
// Step 4: Install via
|
|
2848
|
+
// Step 4: Install via enfyra_package
|
|
2857
2849
|
const body = {
|
|
2858
2850
|
name,
|
|
2859
2851
|
version: pkgVersion,
|
|
@@ -2862,7 +2854,7 @@ server.tool(
|
|
|
2862
2854
|
installedBy: { id: userId },
|
|
2863
2855
|
};
|
|
2864
2856
|
|
|
2865
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2857
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_package', {
|
|
2866
2858
|
method: 'POST',
|
|
2867
2859
|
body: JSON.stringify(body),
|
|
2868
2860
|
});
|
|
@@ -2901,7 +2893,7 @@ server.tool('create_menu', 'Create a menu item in the navigation. Use permission
|
|
|
2901
2893
|
if (body.path && !body.path.startsWith('/')) {
|
|
2902
2894
|
body.path = '/' + body.path;
|
|
2903
2895
|
}
|
|
2904
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2896
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_menu', { method: 'POST', body: JSON.stringify(body) });
|
|
2905
2897
|
const created = firstDataRecord(result);
|
|
2906
2898
|
return { content: [{ type: 'text', text: `Menu created (ID: ${getId(created)}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
2907
2899
|
});
|
|
@@ -2916,7 +2908,7 @@ server.tool(
|
|
|
2916
2908
|
name: z.string().describe('Extension name (unique)'),
|
|
2917
2909
|
type: z.enum(['page', 'widget', 'global']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component; global = shell-level lifecycle component'),
|
|
2918
2910
|
code: z.string().describe('Vue SFC string — <template> + <script setup>, NO import statements'),
|
|
2919
|
-
menuId: z.string().optional().describe('Required for type=page —
|
|
2911
|
+
menuId: z.string().optional().describe('Required for type=page — enfyra_menu id from create_menu. Omit for widget/global'),
|
|
2920
2912
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
|
|
2921
2913
|
description: z.string().optional().describe('Extension description'),
|
|
2922
2914
|
version: z.string().optional().default('1.0.0').describe('Extension version'),
|
|
@@ -2924,7 +2916,7 @@ server.tool(
|
|
|
2924
2916
|
async (data) => {
|
|
2925
2917
|
const body = { ...data };
|
|
2926
2918
|
if (body.type === 'page' && !body.menuId) {
|
|
2927
|
-
throw new Error('menuId is required for type=page. Create or find a
|
|
2919
|
+
throw new Error('menuId is required for type=page. Create or find a enfyra_menu record first.');
|
|
2928
2920
|
}
|
|
2929
2921
|
if (body.type !== 'page' && body.menuId) {
|
|
2930
2922
|
throw new Error('menuId is only valid for type=page. Omit menuId for widget/global extensions.');
|
|
@@ -2933,7 +2925,7 @@ server.tool(
|
|
|
2933
2925
|
body.menu = { id: body.menuId };
|
|
2934
2926
|
delete body.menuId;
|
|
2935
2927
|
}
|
|
2936
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/
|
|
2928
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_extension', { method: 'POST', body: JSON.stringify(body) });
|
|
2937
2929
|
const created = firstDataRecord(result);
|
|
2938
2930
|
return { content: [{ type: 'text', text: `Extension created (ID: ${getId(created)}). Open Enfyra admin tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
|
|
2939
2931
|
},
|