@enfyra/mcp-server 0.0.105 → 0.0.106
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/README.md +4 -2
- package/package.json +1 -1
- package/src/lib/mcp-instructions.js +2 -2
- package/src/lib/platform-operation-tools.js +149 -0
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
191
191
|
- Relation tools reject physical FK/junction names.
|
|
192
192
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
193
193
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
|
194
|
-
- Platform operation tools such as `api_endpoint_workflow`, `create_api_endpoint`, `public_route_methods`, `add_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `ensure_script_flow_step`, `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
194
|
+
- Platform operation tools such as `api_endpoint_workflow`, `create_api_endpoint`, `enable_route`, `disable_route`, `delete_route`, `public_route_methods`, `add_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `ensure_script_flow_step`, `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
195
195
|
- Schema changes are serialized.
|
|
196
196
|
- Destructive deletes return a preview before requiring `confirm=true`.
|
|
197
197
|
|
|
@@ -229,7 +229,9 @@ Do not create custom login/logout/me routes that manually set Enfyra token cooki
|
|
|
229
229
|
|
|
230
230
|
## Tool Summary
|
|
231
231
|
|
|
232
|
-
The MCP server exposes tools for metadata discovery, examples, query/CRUD, method management, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, and `get_enfyra_api_context`.
|
|
232
|
+
The MCP server exposes tools for metadata discovery, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, and `get_enfyra_api_context`.
|
|
233
|
+
|
|
234
|
+
Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods`, `set_public_route_methods`, and `private_route_methods` for that access boundary.
|
|
233
235
|
|
|
234
236
|
For authenticated route access, use `audit_route_access` before changing permissions and `ensure_route_access` to grant access by route path plus role/user. For production script edits, use `trace_metadata_usage`, `get_script_source`, and `patch_script_source` so changes are targeted, hash-checked, and validated.
|
|
235
237
|
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
29
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
30
|
'- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category.',
|
|
31
31
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
|
-
'- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow` for step-by-step endpoint work; `create_api_endpoint` only when a one-shot endpoint operation is clearly safe; route tools such as `add_route_methods`, `public_route_methods`, and `private_route_methods`; `set_table_graphql`; `ensure_guard`; permission/rule tools; websocket tools; flow tools such as `ensure_manual_flow`, `ensure_scheduled_flow`, and fixed-type flow step tools; and extension tools such as `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension`.',
|
|
32
|
+
'- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow` for step-by-step endpoint work; `create_api_endpoint` only when a one-shot endpoint operation is clearly safe; route tools such as `enable_route`, `disable_route`, `delete_route`, `add_route_methods`, `public_route_methods`, and `private_route_methods`; `set_table_graphql`; `ensure_guard`; permission/rule tools; websocket tools; flow tools such as `ensure_manual_flow`, `ensure_scheduled_flow`, and fixed-type flow step tools; and extension tools such as `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension`.',
|
|
33
33
|
'- Before saving standalone dynamic script or extension code, call `validate_dynamic_script` or `validate_extension_code` unless the chosen ensure/update tool already validates the code.',
|
|
34
34
|
'- For existing script-backed records, use `trace_metadata_usage` then `get_script_source`; edit with `patch_script_source` or `update_script_source` so source is hash-checked and validated.',
|
|
35
35
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
@@ -60,7 +60,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
60
60
|
'',
|
|
61
61
|
'### Direct HTTP Mapping',
|
|
62
62
|
'- Route-backed table CRUD is REST: `GET /<table>?...`, `POST /<table>`, `PATCH /<table>/<id>`, `DELETE /<table>/<id>`. There is no `GET /<table>/<id>`; use a filtered list with `limit=1` or `find_one_record`.',
|
|
63
|
-
'- REST public access is controlled by route `publicMethods`; otherwise direct HTTP needs Bearer JWT plus route permissions. GraphQL requires Bearer auth and table GraphQL enablement.',
|
|
63
|
+
'- REST route lifecycle is controlled by `enfyra_route.isEnabled`: disabled routes are not registered at runtime and return 404. Use `enable_route`/`disable_route` instead of raw route PATCH. REST public access is controlled by route `publicMethods`; otherwise direct HTTP needs Bearer JWT plus route permissions. GraphQL requires Bearer auth and table GraphQL enablement.',
|
|
64
64
|
'',
|
|
65
65
|
'When the user asks for details, fetch only the relevant live context or example category instead of relying on broad memorized rules.',
|
|
66
66
|
].join('\n');
|
|
@@ -164,6 +164,115 @@ async function updateRoutePublicMethods(apiUrl, { path, routeId, methods, mode }
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
async function setRouteEnabled(apiUrl, { path, routeId, isEnabled }) {
|
|
168
|
+
const { route } = await resolveRoute(apiUrl, { path, routeId });
|
|
169
|
+
const before = route?.isEnabled !== false;
|
|
170
|
+
if (before === isEnabled) {
|
|
171
|
+
return {
|
|
172
|
+
action: isEnabled ? 'route_already_enabled' : 'route_already_disabled',
|
|
173
|
+
route: { id: getId(route), path: route.path },
|
|
174
|
+
before: { isEnabled: before },
|
|
175
|
+
after: { isEnabled },
|
|
176
|
+
runtimeBehavior: isEnabled ? 'Enabled routes are registered at runtime.' : 'Disabled routes are not registered at runtime and return 404.',
|
|
177
|
+
routeReload: { attempted: false, succeeded: true, reason: 'No route lifecycle change was needed.' },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = await fetchAPI(apiUrl, `/enfyra_route/${encodeURIComponent(String(getId(route)))}`, {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
body: JSON.stringify({ isEnabled }),
|
|
184
|
+
});
|
|
185
|
+
const routeReload = await reloadRoutes(apiUrl);
|
|
186
|
+
return {
|
|
187
|
+
action: isEnabled ? 'route_enabled' : 'route_disabled',
|
|
188
|
+
route: { id: getId(route), path: route.path },
|
|
189
|
+
before: { isEnabled: before },
|
|
190
|
+
after: { isEnabled },
|
|
191
|
+
runtimeBehavior: isEnabled ? 'The route should now be registered at runtime.' : 'The route should now return 404 because disabled routes are not registered at runtime.',
|
|
192
|
+
result,
|
|
193
|
+
routeReload,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function fetchRouteDependencies(apiUrl, routeId) {
|
|
198
|
+
const routeFilter = filterQuery({ route: { id: { _eq: routeId } } });
|
|
199
|
+
const routeIdFilter = filterQuery({ routeId: { _eq: routeId } });
|
|
200
|
+
const [handlers, permissions, preHooks, postHooks, guards] = await Promise.all([
|
|
201
|
+
fetchAll(apiUrl, `/enfyra_route_handler?filter=${routeIdFilter}&fields=id,_id,routeId,method.name&limit=0`),
|
|
202
|
+
fetchAll(apiUrl, `/enfyra_route_permission?filter=${routeFilter}&fields=id,_id,route.id,role.name,isEnabled&limit=0`),
|
|
203
|
+
fetchAll(apiUrl, `/enfyra_pre_hook?filter=${routeFilter}&fields=id,_id,route.id,name,isEnabled&limit=0`),
|
|
204
|
+
fetchAll(apiUrl, `/enfyra_post_hook?filter=${routeFilter}&fields=id,_id,route.id,name,isEnabled&limit=0`),
|
|
205
|
+
fetchAll(apiUrl, `/enfyra_guard?filter=${routeFilter}&fields=id,_id,route.id,name,isEnabled&limit=0`),
|
|
206
|
+
]);
|
|
207
|
+
return { handlers, permissions, preHooks, postHooks, guards };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function summarizeRouteDependencies(dependencies) {
|
|
211
|
+
return {
|
|
212
|
+
handlers: dependencies.handlers.map((item) => ({ id: getId(item), method: item?.method?.name || null })),
|
|
213
|
+
permissions: dependencies.permissions.map((item) => ({ id: getId(item), role: item?.role?.name || null, isEnabled: item?.isEnabled !== false })),
|
|
214
|
+
preHooks: dependencies.preHooks.map((item) => ({ id: getId(item), name: item?.name || null, isEnabled: item?.isEnabled !== false })),
|
|
215
|
+
postHooks: dependencies.postHooks.map((item) => ({ id: getId(item), name: item?.name || null, isEnabled: item?.isEnabled !== false })),
|
|
216
|
+
guards: dependencies.guards.map((item) => ({ id: getId(item), name: item?.name || null, isEnabled: item?.isEnabled !== false })),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function deleteRows(apiUrl, tableName, rows) {
|
|
221
|
+
const deleted = [];
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
const id = getId(row);
|
|
224
|
+
if (id === null || id === undefined) continue;
|
|
225
|
+
await fetchAPI(apiUrl, `/${tableName}/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
|
|
226
|
+
deleted.push(id);
|
|
227
|
+
}
|
|
228
|
+
return deleted;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function deleteRoute(apiUrl, { path, routeId, expectedPath, confirm }) {
|
|
232
|
+
const { route } = await resolveRoute(apiUrl, { path, routeId });
|
|
233
|
+
if (expectedPath && route.path !== normalizeRestPath(expectedPath)) {
|
|
234
|
+
throw new Error(`Route path mismatch: resolved ${route.path}, expected ${normalizeRestPath(expectedPath)}.`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const dependencies = await fetchRouteDependencies(apiUrl, getId(route));
|
|
238
|
+
const dependencySummary = summarizeRouteDependencies(dependencies);
|
|
239
|
+
const preview = {
|
|
240
|
+
route: { id: getId(route), path: route.path, isEnabled: route?.isEnabled !== false },
|
|
241
|
+
dependencies: dependencySummary,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (!confirm) {
|
|
245
|
+
return {
|
|
246
|
+
action: 'delete_route_preview',
|
|
247
|
+
...preview,
|
|
248
|
+
next: 'Call delete_route again with confirm=true and expectedPath set to this route path to delete the route and related handlers/hooks/permissions/guards.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await deleteRows(apiUrl, 'enfyra_route_handler', dependencies.handlers);
|
|
253
|
+
await deleteRows(apiUrl, 'enfyra_pre_hook', dependencies.preHooks);
|
|
254
|
+
await deleteRows(apiUrl, 'enfyra_post_hook', dependencies.postHooks);
|
|
255
|
+
await deleteRows(apiUrl, 'enfyra_guard', dependencies.guards);
|
|
256
|
+
await deleteRows(apiUrl, 'enfyra_route_permission', dependencies.permissions);
|
|
257
|
+
const result = await fetchAPI(apiUrl, `/enfyra_route/${encodeURIComponent(String(getId(route)))}`, { method: 'DELETE' });
|
|
258
|
+
const routeReload = await reloadRoutes(apiUrl);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
action: 'route_deleted',
|
|
262
|
+
...preview,
|
|
263
|
+
deleted: {
|
|
264
|
+
handlers: dependencies.handlers.map(getId).filter((id) => id !== null && id !== undefined),
|
|
265
|
+
permissions: dependencies.permissions.map(getId).filter((id) => id !== null && id !== undefined),
|
|
266
|
+
preHooks: dependencies.preHooks.map(getId).filter((id) => id !== null && id !== undefined),
|
|
267
|
+
postHooks: dependencies.postHooks.map(getId).filter((id) => id !== null && id !== undefined),
|
|
268
|
+
guards: dependencies.guards.map(getId).filter((id) => id !== null && id !== undefined),
|
|
269
|
+
route: getId(route),
|
|
270
|
+
},
|
|
271
|
+
result,
|
|
272
|
+
routeReload,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
167
276
|
async function findHandler(apiUrl, routeId, methodId) {
|
|
168
277
|
const filter = encodeURIComponent(JSON.stringify({
|
|
169
278
|
route: { id: { _eq: routeId } },
|
|
@@ -882,6 +991,46 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
882
991
|
})),
|
|
883
992
|
);
|
|
884
993
|
|
|
994
|
+
server.tool(
|
|
995
|
+
'enable_route',
|
|
996
|
+
'Business operation: enable an existing route. Enabled routes are registered at runtime; disabled routes return 404.',
|
|
997
|
+
{
|
|
998
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
999
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
1000
|
+
},
|
|
1001
|
+
async ({ path, routeId }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
|
|
1002
|
+
path,
|
|
1003
|
+
routeId,
|
|
1004
|
+
isEnabled: true,
|
|
1005
|
+
})),
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
server.tool(
|
|
1009
|
+
'disable_route',
|
|
1010
|
+
'Business operation: disable an existing route without deleting metadata. Disabled routes are not registered at runtime and return 404.',
|
|
1011
|
+
{
|
|
1012
|
+
path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
|
|
1013
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
1014
|
+
},
|
|
1015
|
+
async ({ path, routeId }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
|
|
1016
|
+
path,
|
|
1017
|
+
routeId,
|
|
1018
|
+
isEnabled: false,
|
|
1019
|
+
})),
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
server.tool(
|
|
1023
|
+
'delete_route',
|
|
1024
|
+
'Business operation: preview-first delete for a route and its route-owned handlers, hooks, guards, and permissions. Use only when a route contract is retired.',
|
|
1025
|
+
{
|
|
1026
|
+
path: z.string().optional().describe('Route path, e.g. /old-endpoint. Use either path or routeId.'),
|
|
1027
|
+
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
1028
|
+
expectedPath: z.string().optional().describe('Optional safety check. When confirm=true, pass the path returned by the preview.'),
|
|
1029
|
+
confirm: z.boolean().optional().default(false).describe('false returns a dependency preview only; true deletes the route and related route-owned records.'),
|
|
1030
|
+
},
|
|
1031
|
+
async (input) => jsonText(await deleteRoute(ENFYRA_API_URL, input)),
|
|
1032
|
+
);
|
|
1033
|
+
|
|
885
1034
|
server.tool(
|
|
886
1035
|
'public_route_methods',
|
|
887
1036
|
'Business operation: make existing route methods public/anonymous.',
|