@enfyra/mcp-server 0.0.59 → 0.0.61
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 +1 -1
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +25 -2
- package/src/lib/mcp-instructions.js +5 -2
- package/src/mcp-server-entry.mjs +222 -28
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ When an LLM builds a Nuxt, Next, or other SSR frontend for Enfyra, follow the sa
|
|
|
215
215
|
|
|
216
216
|
## Tools (summary)
|
|
217
217
|
|
|
218
|
-
Metadata, examples, query/CRUD, route/handler/hook, tables/columns, reload cache, logs, user/roles, login, menu/extension, `get_enfyra_api_context`. For full tool list and behavior, see the app after enabling MCP or the source in `src/mcp-server-entry.mjs`.
|
|
218
|
+
Metadata, examples, query/CRUD, method management, route/handler/hook, tables/columns, reload cache, logs, user/roles, login, menu/extension, `get_enfyra_api_context`. For full tool list and behavior, see the app after enabling MCP or the source in `src/mcp-server-entry.mjs`.
|
|
219
219
|
|
|
220
220
|
Use `get_enfyra_examples` when asking an LLM to generate concrete Enfyra implementation patterns. It returns categorized examples for SSR app auth/OAuth/proxy setup, schema/relations, queries/deep, handlers/hooks, permissions/RLS, websocket, flows, files, and extensions.
|
|
221
221
|
|
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -497,11 +497,11 @@ return @DATA\`
|
|
|
497
497
|
tableName: "route_definition",
|
|
498
498
|
id: "<route_id>",
|
|
499
499
|
data: {
|
|
500
|
-
publishedMethods: [{ id:
|
|
500
|
+
publishedMethods: [{ id: "<GET_method_id_from_list_methods>" }]
|
|
501
501
|
}
|
|
502
502
|
})`,
|
|
503
503
|
notes: [
|
|
504
|
-
'Method
|
|
504
|
+
'Method ids are instance data. Use list_methods or inspect_route output to resolve the GET method id first.',
|
|
505
505
|
'publishedMethods controls anonymous route access. Route permissions are not for public access.',
|
|
506
506
|
'Route permissions apply when the method is not public.',
|
|
507
507
|
],
|
|
@@ -785,6 +785,29 @@ return {
|
|
|
785
785
|
title: 'Dynamic app extensions and menus',
|
|
786
786
|
useWhen: 'Use when adding custom UI pages to the Enfyra app.',
|
|
787
787
|
examples: [
|
|
788
|
+
{
|
|
789
|
+
name: 'Create or update HTTP method colors',
|
|
790
|
+
code: `list_methods()
|
|
791
|
+
|
|
792
|
+
create_method({
|
|
793
|
+
method: "PUT",
|
|
794
|
+
buttonColor: "#e0e7ff",
|
|
795
|
+
textColor: "#4338ca"
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
update_method({
|
|
799
|
+
method: "PATCH",
|
|
800
|
+
buttonColor: "#fef3c7",
|
|
801
|
+
textColor: "#b45309"
|
|
802
|
+
})`,
|
|
803
|
+
notes: [
|
|
804
|
+
'Use dedicated method tools instead of generic CRUD on method_definition.',
|
|
805
|
+
'The backend stores the method label in method_definition.name; do not send or filter a method_definition.method field.',
|
|
806
|
+
'buttonColor is the badge background and textColor is the badge text color.',
|
|
807
|
+
'The eApp management UI is /settings/methods.',
|
|
808
|
+
'delete_method is preview-first and should only be used for unused custom methods.',
|
|
809
|
+
],
|
|
810
|
+
},
|
|
788
811
|
{
|
|
789
812
|
name: 'Create menu then extension',
|
|
790
813
|
code: `create_menu({
|
|
@@ -114,9 +114,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
114
114
|
'- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
|
|
115
115
|
' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
|
|
116
116
|
' - **One-to-many / many-to-many:** `"publishedMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
|
|
117
|
-
'- **Method IDs**
|
|
117
|
+
'- **Method IDs** are instance data, not a stable contract. Query `method_definition` or use method names through MCP route helpers before setting `publishedMethods`, `availableMethods`, `skipRoleGuardMethods`, hook methods, handler methods, or route permissions. Default CRUD records are `GET`, `POST`, `PATCH`, and `DELETE`; create extra records such as `PUT` through method tools when a route needs them.',
|
|
118
|
+
'- `method_definition.name` is the unique backend field for the HTTP method label. Do not filter, create, or update a `method_definition.method` field. MCP method tools accept an input named `method` for usability, but they write/read `name` on the server.',
|
|
118
119
|
'- **Wrong:** `"publishedMethods": ["GET"]` or `"publishedMethods": [{"method": "GET"}]` — rejected or silently ignored.',
|
|
119
|
-
'- **Right:**
|
|
120
|
+
'- **Right:** first query method records, then pass their ids, for example `"publishedMethods": [{"id": <GET_METHOD_ID>}]`. Multiple methods use multiple id objects.',
|
|
120
121
|
'- **To unset:** pass empty array `"publishedMethods": []`.',
|
|
121
122
|
'',
|
|
122
123
|
'### Dynamic script `$repos` mutation return shape',
|
|
@@ -298,6 +299,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
298
299
|
'- **Operational list data loading:** do not use arbitrary fixed limits such as `limit=50` as the whole data strategy for admin pages. Use pagination, expose result count when the API supports `meta=filterCount`, and add search/filter controls for natural lookup keys such as id, name, slug, status, email, or external reference.',
|
|
299
300
|
'- **ESV aggregate contract:** aggregate query must be an object keyed by a real field or relation, for example `aggregate: { id: { count: true }, status: { count: { _eq: "failed" } }, amount: { sum: true } }`. Results are returned in `response.meta.aggregate`. Time windows and cross-field conditions belong in top-level `filter`, not inside a field aggregate condition. Field aggregate conditions only support operators on that same field; relation aggregates use `countRecords`.',
|
|
300
301
|
'- **Aggregate numeric rule:** `sum` and `avg` require a numeric field in ESV. Do not aggregate money stored as varchar/text. Use a numeric money field such as `amount_usd` with type `float`, `amount_cents`, or `amount` for revenue stats, or build a dedicated stats route that normalizes legacy values explicitly. If metadata says `float` but SQL aggregate still fails with `sum(character varying)`, the Enfyra Server physical schema is stale or missing the SQL float DDL mapping and must be redeployed/healed before relying on aggregate.',
|
|
302
|
+
'- **Snapshot migrations:** backend metadata/physical schema renames belong in `data/snapshot-migration.json` via table-driven `columnsToModify` entries. The server migration/self-heal path should read table name plus `oldName`/`newName` dynamically; do not hard-code one-off table repairs when the snapshot migration contract can express the change.',
|
|
301
303
|
'- **Partial reload default:** ESV/ASV automatically triggers partial reloads for metadata, routes, menus, extensions, flows, handlers, and related caches after successful writes. Do not reflexively call `/admin/reload`, `/admin/reload/metadata`, or `/admin/reload/routes` after each change. Verify naturally first; use manual reload only when verification shows stale behavior, a reload event failed, or a concrete error indicates the partial reload did not apply.',
|
|
302
304
|
'- **Menu/extension realtime reload contract:** `menu_definition` and `extension_definition` writes are runtime UI changes, not plain CRUD. The server cache orchestrator must emit `$system:reload` through the admin Socket.IO channel with identifiers that eApp handles; eApp must refetch menus/rebuild the menu registry for menu reloads and invalidate dynamic extension caches for extension reloads. Menu reloads can change route-to-extension mapping, so they should also invalidate extension cache. If an open admin tab does not reflect menu/extension changes, debug this two-sided reload contract before telling the user to refresh.',
|
|
303
305
|
'- **Dashboard stats:** time range buttons must change the query filter and reload stats. Dashboards should summarize actionable errors and high-level activity; successful/no-error background runs usually do not need a standalone page unless there is a real workflow to manage.',
|
|
@@ -306,6 +308,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
306
308
|
'- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, capacity totals, billing totals, or detail metrics there by default; keep those as body cards/tables where the operator can scan them with the page content. Only use PageHeader stats for a deliberately compact overview page where the stats are truly header-level context.',
|
|
307
309
|
'- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Sensitive registry actions must include a `permission` condition, for example `{ id: "create", label: "Create report", permission: { and: [{ route: "/report_definition", methods: ["POST"] }] }, onClick }`.',
|
|
308
310
|
'- **Header action button variants:** choose the button variant by intent. Use `color: "primary", variant: "solid"` for the main page action. Use `color: "neutral", variant: "ghost"` for back/navigation actions and `color: "neutral", variant: "outline"` for visible secondary actions. `variant: "soft"` is only for low-emphasis secondary/chrome actions; do not use soft for critical or primary header actions just because it looks acceptable in dark mode.',
|
|
311
|
+
'- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `method_definition`. The backend field is `method_definition.name`, unique per method; do not send `method_definition.method`. The eApp UI for the same records is `/settings/methods`. Method color fields are `buttonColor` for badge background and `textColor` for badge text, both full hex colors. Do not use generic `create_record` on `method_definition` unless the dedicated tool is unavailable.',
|
|
309
312
|
'- **Extension navigation:** prefer `NuxtLink` or Nuxt UI components with `:to` for visible navigation links and drill-down cards/buttons. Use `navigateTo(...)` only for imperative navigation after submit, confirm, mutation, or another side effect.',
|
|
310
313
|
'- **Extension runtime scope:** eApp exposes Vue APIs and injected Nuxt/Enfyra composables both to script global scope and Vue app `globalProperties`. Template expressions may call injected helpers directly, for example after a save handler can call `navigateTo("/data/report_definition")`, because Vue compiles template helpers to `_ctx.*`.',
|
|
311
314
|
'- **Extension CSS affects shell utility ordering:** dynamic extension CSS is injected after the app shell CSS. Shell/page-header code must not put conflicting plain Tailwind utilities on the same element, such as `flex-col` plus `flex-row`, `items-start` plus `items-center`, or `text-left` plus `text-center`. Choose one mutually exclusive class per state; otherwise extension CSS can change which utility wins and shift shell layout.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const CAPABILITY_AREAS = [
|
|
|
34
34
|
{
|
|
35
35
|
area: 'Dynamic REST API',
|
|
36
36
|
tables: ['route_definition', 'route_handler_definition', 'pre_hook_definition', 'post_hook_definition', 'route_permission_definition', 'method_definition'],
|
|
37
|
-
workflow: 'Create custom paths with create_route without mainTableId, then add handlers/hooks. mainTableId is only for canonical table routes like /table_name.
|
|
37
|
+
workflow: 'Create custom paths with create_route without mainTableId, then add handlers/hooks. mainTableId is only for canonical table routes like /table_name. Query method_definition before assigning route methods.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
area: 'Auth, roles, sessions, OAuth',
|
|
@@ -197,8 +197,8 @@ function summarizeRoutes(routesResult) {
|
|
|
197
197
|
id: route.id ?? route._id,
|
|
198
198
|
path: route.path,
|
|
199
199
|
mainTable: route.mainTable?.name || route.mainTableName || null,
|
|
200
|
-
availableMethods: (route.availableMethods || []).map((method) => method.
|
|
201
|
-
publishedMethods: (route.publishedMethods || []).map((method) => method.
|
|
200
|
+
availableMethods: (route.availableMethods || []).map((method) => method.name).filter(Boolean),
|
|
201
|
+
publishedMethods: (route.publishedMethods || []).map((method) => method.name).filter(Boolean),
|
|
202
202
|
isEnabled: route.isEnabled,
|
|
203
203
|
}));
|
|
204
204
|
}
|
|
@@ -356,6 +356,31 @@ function appendQuery(path, queryParams) {
|
|
|
356
356
|
return `${path}${path.includes('?') ? '&' : '?'}${queryParams}`;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
const METHOD_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
360
|
+
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
361
|
+
|
|
362
|
+
function normalizeMethodNameInput(method) {
|
|
363
|
+
const value = String(method || '').trim().toUpperCase();
|
|
364
|
+
if (!METHOD_NAME_RE.test(value)) {
|
|
365
|
+
throw new Error('Method must start with A-Z and contain only uppercase letters, numbers, or underscore.');
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeHexColorInput(value, fieldName) {
|
|
371
|
+
const color = String(value || '').trim().toLowerCase();
|
|
372
|
+
if (!HEX_COLOR_RE.test(color)) {
|
|
373
|
+
throw new Error(`${fieldName} must be a full hex color such as #1d4ed8.`);
|
|
374
|
+
}
|
|
375
|
+
return color;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function findMethodRecordByName(method) {
|
|
379
|
+
const filter = encodeURIComponent(JSON.stringify({ name: { _eq: method } }));
|
|
380
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
|
|
381
|
+
return unwrapData(result)[0] || null;
|
|
382
|
+
}
|
|
383
|
+
|
|
359
384
|
// Create MCP server — `instructions` is sent to the host (e.g. Claude Code) for the LLM; not README
|
|
360
385
|
const server = new McpServer(
|
|
361
386
|
{
|
|
@@ -451,7 +476,7 @@ server.tool(
|
|
|
451
476
|
routes: routes.length,
|
|
452
477
|
methods: methodsResult?.data?.length || 0,
|
|
453
478
|
},
|
|
454
|
-
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id,
|
|
479
|
+
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name, method: method.name })),
|
|
455
480
|
capabilityAreas: CAPABILITY_AREAS.map((item) => ({
|
|
456
481
|
...item,
|
|
457
482
|
presentTables: item.tables.filter((table) => tableNames.includes(table)),
|
|
@@ -554,7 +579,7 @@ server.tool(
|
|
|
554
579
|
storageConfigs: storageResult?.data?.length || 0,
|
|
555
580
|
settings: settingsResult?.data?.length || 0,
|
|
556
581
|
},
|
|
557
|
-
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id,
|
|
582
|
+
methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name, method: method.name })),
|
|
558
583
|
routeRuntime: {
|
|
559
584
|
routePattern: 'GET/POST /<route-path>; PATCH/DELETE /<route-path>/:id; no dynamic GET /<route-path>/:id.',
|
|
560
585
|
adminRoutes: adminRoutes.map((route) => route.path).sort(),
|
|
@@ -1020,6 +1045,157 @@ server.tool('delete_record', 'Delete a record by ID', {
|
|
|
1020
1045
|
}, null, 2) }] };
|
|
1021
1046
|
});
|
|
1022
1047
|
|
|
1048
|
+
server.tool(
|
|
1049
|
+
'list_methods',
|
|
1050
|
+
'List method_definition records with their UI colors. Use this before creating route methods or method-colored UI.',
|
|
1051
|
+
{},
|
|
1052
|
+
async () => {
|
|
1053
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?fields=id,_id,name,buttonColor,textColor,isSystem&sort=name&limit=0');
|
|
1054
|
+
const methods = unwrapData(result).map((method) => ({
|
|
1055
|
+
id: getId(method),
|
|
1056
|
+
name: method.name,
|
|
1057
|
+
method: method.name,
|
|
1058
|
+
buttonColor: method.buttonColor,
|
|
1059
|
+
textColor: method.textColor,
|
|
1060
|
+
isSystem: method.isSystem === true,
|
|
1061
|
+
}));
|
|
1062
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1063
|
+
tableName: 'method_definition',
|
|
1064
|
+
methods,
|
|
1065
|
+
appUi: '/settings/methods',
|
|
1066
|
+
}, null, 2) }] };
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
server.tool(
|
|
1071
|
+
'create_method',
|
|
1072
|
+
'Create a method_definition record with app badge colors. Prefer this over generic create_record for method_definition.',
|
|
1073
|
+
{
|
|
1074
|
+
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.'),
|
|
1075
|
+
buttonColor: z.string().describe('Badge background color as full hex, e.g. #dbeafe.'),
|
|
1076
|
+
textColor: z.string().describe('Badge text color as full hex, e.g. #1d4ed8.'),
|
|
1077
|
+
isSystem: z.boolean().optional().default(false).describe('Set true only for built-in/runtime-owned methods. Normal app methods should leave this false.'),
|
|
1078
|
+
},
|
|
1079
|
+
async ({ method, buttonColor, textColor, isSystem }) => {
|
|
1080
|
+
const normalizedMethod = normalizeMethodNameInput(method);
|
|
1081
|
+
const existing = await findMethodRecordByName(normalizedMethod);
|
|
1082
|
+
if (existing) {
|
|
1083
|
+
throw new Error(`Method ${normalizedMethod} already exists with id ${getId(existing)}. Use update_method to change colors.`);
|
|
1084
|
+
}
|
|
1085
|
+
const body = {
|
|
1086
|
+
name: normalizedMethod,
|
|
1087
|
+
buttonColor: normalizeHexColorInput(buttonColor, 'buttonColor'),
|
|
1088
|
+
textColor: normalizeHexColorInput(textColor, 'textColor'),
|
|
1089
|
+
isSystem: isSystem === true,
|
|
1090
|
+
};
|
|
1091
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition', {
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
body: JSON.stringify(body),
|
|
1094
|
+
});
|
|
1095
|
+
_methodMap = null;
|
|
1096
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1097
|
+
...summarizeMutationResult(result, 'created', 'method_definition'),
|
|
1098
|
+
name: normalizedMethod,
|
|
1099
|
+
method: normalizedMethod,
|
|
1100
|
+
appUi: '/settings/methods',
|
|
1101
|
+
}, null, 2) }] };
|
|
1102
|
+
},
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
server.tool(
|
|
1106
|
+
'update_method',
|
|
1107
|
+
'Update a method_definition record color pair, and optionally rename non-system methods. Prefer this over generic update_record for method_definition.',
|
|
1108
|
+
{
|
|
1109
|
+
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1110
|
+
method: z.string().optional().describe('Existing method name to find, or new name when id is provided.'),
|
|
1111
|
+
buttonColor: z.string().optional().describe('Badge background color as full hex, e.g. #dbeafe.'),
|
|
1112
|
+
textColor: z.string().optional().describe('Badge text color as full hex, e.g. #1d4ed8.'),
|
|
1113
|
+
},
|
|
1114
|
+
async ({ id, method, buttonColor, textColor }) => {
|
|
1115
|
+
let targetId = id;
|
|
1116
|
+
let existing = null;
|
|
1117
|
+
if (!targetId) {
|
|
1118
|
+
if (!method) throw new Error('Provide id or method.');
|
|
1119
|
+
const normalizedMethod = normalizeMethodNameInput(method);
|
|
1120
|
+
existing = await findMethodRecordByName(normalizedMethod);
|
|
1121
|
+
if (!existing) throw new Error(`Method ${normalizedMethod} was not found.`);
|
|
1122
|
+
targetId = getId(existing);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const body = {};
|
|
1126
|
+
if (buttonColor !== undefined) {
|
|
1127
|
+
body.buttonColor = normalizeHexColorInput(buttonColor, 'buttonColor');
|
|
1128
|
+
}
|
|
1129
|
+
if (textColor !== undefined) {
|
|
1130
|
+
body.textColor = normalizeHexColorInput(textColor, 'textColor');
|
|
1131
|
+
}
|
|
1132
|
+
if (method !== undefined && id) {
|
|
1133
|
+
body.name = normalizeMethodNameInput(method);
|
|
1134
|
+
}
|
|
1135
|
+
if (Object.keys(body).length === 0) {
|
|
1136
|
+
throw new Error('Provide buttonColor, textColor, or a new method name.');
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition/${encodeURIComponent(String(targetId))}`, {
|
|
1140
|
+
method: 'PATCH',
|
|
1141
|
+
body: JSON.stringify(body),
|
|
1142
|
+
});
|
|
1143
|
+
_methodMap = null;
|
|
1144
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1145
|
+
...summarizeMutationResult(result, 'updated', 'method_definition'),
|
|
1146
|
+
id: targetId,
|
|
1147
|
+
appUi: '/settings/methods',
|
|
1148
|
+
}, null, 2) }] };
|
|
1149
|
+
},
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
server.tool(
|
|
1153
|
+
'delete_method',
|
|
1154
|
+
'Preview or delete a method_definition record. Only delete unused custom methods; system/default methods should be kept.',
|
|
1155
|
+
{
|
|
1156
|
+
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1157
|
+
method: z.string().optional().describe('Method name to find when id is omitted.'),
|
|
1158
|
+
confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
|
|
1159
|
+
},
|
|
1160
|
+
async ({ id, method, confirm }) => {
|
|
1161
|
+
let targetId = id;
|
|
1162
|
+
let target = null;
|
|
1163
|
+
if (!targetId) {
|
|
1164
|
+
if (!method) throw new Error('Provide id or method.');
|
|
1165
|
+
target = await findMethodRecordByName(normalizeMethodNameInput(method));
|
|
1166
|
+
if (!target) throw new Error(`Method ${method} was not found.`);
|
|
1167
|
+
targetId = getId(target);
|
|
1168
|
+
}
|
|
1169
|
+
if (!confirm) {
|
|
1170
|
+
if (!target) {
|
|
1171
|
+
const primaryKey = await getPrimaryFieldName('method_definition');
|
|
1172
|
+
const filter = encodeURIComponent(JSON.stringify({ [primaryKey]: { _eq: targetId } }));
|
|
1173
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
|
|
1174
|
+
target = unwrapData(result)[0] || null;
|
|
1175
|
+
}
|
|
1176
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1177
|
+
action: 'delete_method_preview',
|
|
1178
|
+
id: targetId,
|
|
1179
|
+
name: target?.name,
|
|
1180
|
+
method: target?.name,
|
|
1181
|
+
isSystem: target?.isSystem === true,
|
|
1182
|
+
destructive: true,
|
|
1183
|
+
warning: 'Only delete unused custom methods. Deleting a method can affect route method relations.',
|
|
1184
|
+
next: 'Call delete_method again with confirm=true to delete.',
|
|
1185
|
+
}, null, 2) }] };
|
|
1186
|
+
}
|
|
1187
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition/${encodeURIComponent(String(targetId))}`, { method: 'DELETE' });
|
|
1188
|
+
_methodMap = null;
|
|
1189
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1190
|
+
action: 'deleted',
|
|
1191
|
+
tableName: 'method_definition',
|
|
1192
|
+
id: targetId,
|
|
1193
|
+
statusCode: result?.statusCode,
|
|
1194
|
+
success: result?.success,
|
|
1195
|
+
}, null, 2) }] };
|
|
1196
|
+
},
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1023
1199
|
server.tool(
|
|
1024
1200
|
'run_admin_test',
|
|
1025
1201
|
[
|
|
@@ -1092,7 +1268,7 @@ async function getMethodMap() {
|
|
|
1092
1268
|
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?limit=0');
|
|
1093
1269
|
_methodMap = {};
|
|
1094
1270
|
for (const m of result.data) {
|
|
1095
|
-
_methodMap[m.
|
|
1271
|
+
_methodMap[m.name] = m.id || m._id;
|
|
1096
1272
|
}
|
|
1097
1273
|
return _methodMap;
|
|
1098
1274
|
}
|
|
@@ -1116,7 +1292,8 @@ function withMethodNames(records, methodIdNameMap, field = 'methods') {
|
|
|
1116
1292
|
[field]: Array.isArray(record?.[field])
|
|
1117
1293
|
? record[field].map((item) => ({
|
|
1118
1294
|
...item,
|
|
1119
|
-
|
|
1295
|
+
name: item.name || methodIdNameMap[String(getId(item))] || null,
|
|
1296
|
+
method: item.name || methodIdNameMap[String(getId(item))] || null,
|
|
1120
1297
|
}))
|
|
1121
1298
|
: record?.[field],
|
|
1122
1299
|
}));
|
|
@@ -1171,7 +1348,11 @@ function enrichRoute(route, state) {
|
|
|
1171
1348
|
.filter((item) => sameId(refId(item.route), routeId))
|
|
1172
1349
|
.map((item) => pickCodeSummary({
|
|
1173
1350
|
...item,
|
|
1174
|
-
method: item.method ? {
|
|
1351
|
+
method: item.method ? {
|
|
1352
|
+
...item.method,
|
|
1353
|
+
name: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
|
|
1354
|
+
method: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
|
|
1355
|
+
} : item.method,
|
|
1175
1356
|
}, 'sourceCode'));
|
|
1176
1357
|
const routePreHooks = withMethodNames(
|
|
1177
1358
|
state.preHooks.filter((item) => item.isGlobal || sameId(refId(item.route), routeId)),
|
|
@@ -1196,13 +1377,25 @@ function enrichRoute(route, state) {
|
|
|
1196
1377
|
return {
|
|
1197
1378
|
...route,
|
|
1198
1379
|
availableMethods: Array.isArray(route.availableMethods)
|
|
1199
|
-
? route.availableMethods.map((method) => ({
|
|
1380
|
+
? route.availableMethods.map((method) => ({
|
|
1381
|
+
...method,
|
|
1382
|
+
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1383
|
+
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1384
|
+
}))
|
|
1200
1385
|
: route.availableMethods,
|
|
1201
1386
|
publishedMethods: Array.isArray(route.publishedMethods)
|
|
1202
|
-
? route.publishedMethods.map((method) => ({
|
|
1387
|
+
? route.publishedMethods.map((method) => ({
|
|
1388
|
+
...method,
|
|
1389
|
+
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1390
|
+
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1391
|
+
}))
|
|
1203
1392
|
: route.publishedMethods,
|
|
1204
1393
|
skipRoleGuardMethods: Array.isArray(route.skipRoleGuardMethods)
|
|
1205
|
-
? route.skipRoleGuardMethods.map((method) => ({
|
|
1394
|
+
? route.skipRoleGuardMethods.map((method) => ({
|
|
1395
|
+
...method,
|
|
1396
|
+
name: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1397
|
+
method: method.name || state.methodIdNameMap[String(getId(method))] || null,
|
|
1398
|
+
}))
|
|
1206
1399
|
: route.skipRoleGuardMethods,
|
|
1207
1400
|
handlers: routeHandlers,
|
|
1208
1401
|
preHooks: routePreHooks,
|
|
@@ -1363,7 +1556,7 @@ server.tool(
|
|
|
1363
1556
|
'Use this after inspecting a route or changing handlers/hooks/guards. Pass paths like /table_definition?limit=1, not external URLs.',
|
|
1364
1557
|
].join(' '),
|
|
1365
1558
|
{
|
|
1366
|
-
method: z.
|
|
1559
|
+
method: z.string().optional().default('GET').describe('HTTP method name. Must exist in method_definition.name for Enfyra route-backed calls.'),
|
|
1367
1560
|
path: z.string().describe('Enfyra API path, e.g. /route_definition?limit=1'),
|
|
1368
1561
|
query: z.string().optional().describe('Optional query params JSON object, merged onto path query string'),
|
|
1369
1562
|
body: z.string().optional().describe('Optional JSON request body string'),
|
|
@@ -1371,6 +1564,7 @@ server.tool(
|
|
|
1371
1564
|
useAuth: z.boolean().optional().default(true).describe('Attach MCP admin Bearer token. Set false to test published/public access.'),
|
|
1372
1565
|
},
|
|
1373
1566
|
async ({ method, path, query, body, headers, useAuth }) => {
|
|
1567
|
+
const httpMethod = normalizeMethodNameInput(method || 'GET');
|
|
1374
1568
|
const restPath = normalizeRestPath(path);
|
|
1375
1569
|
const url = new URL(`${ENFYRA_API_URL.replace(/\/$/, '')}${restPath}`);
|
|
1376
1570
|
const queryObj = parseJsonArg(query, {});
|
|
@@ -1388,9 +1582,9 @@ server.tool(
|
|
|
1388
1582
|
|
|
1389
1583
|
const started = Date.now();
|
|
1390
1584
|
const response = await fetch(url, {
|
|
1391
|
-
method,
|
|
1585
|
+
method: httpMethod,
|
|
1392
1586
|
headers: requestHeaders,
|
|
1393
|
-
...(body !== undefined && body !== null &&
|
|
1587
|
+
...(body !== undefined && body !== null && httpMethod !== 'GET' ? { body } : {}),
|
|
1394
1588
|
});
|
|
1395
1589
|
const contentType = response.headers.get('content-type') || '';
|
|
1396
1590
|
const responseText = await response.text();
|
|
@@ -1401,7 +1595,7 @@ server.tool(
|
|
|
1401
1595
|
|
|
1402
1596
|
const payload = {
|
|
1403
1597
|
request: {
|
|
1404
|
-
method,
|
|
1598
|
+
method: httpMethod,
|
|
1405
1599
|
url: url.toString(),
|
|
1406
1600
|
authenticated: !!useAuth,
|
|
1407
1601
|
},
|
|
@@ -1468,9 +1662,9 @@ server.tool(
|
|
|
1468
1662
|
{
|
|
1469
1663
|
path: z.string().describe('URL path, must start with / (e.g., "/my-endpoint")'),
|
|
1470
1664
|
mainTableId: z.union([z.string(), z.number()]).optional().describe('Only set for the canonical table route `/<table_name>`. Omit for every custom route.'),
|
|
1471
|
-
methods: z.array(z.
|
|
1472
|
-
.describe('HTTP
|
|
1473
|
-
publishedMethods: z.array(z.
|
|
1665
|
+
methods: z.array(z.string())
|
|
1666
|
+
.describe('HTTP method names this route supports (availableMethods). Each value must exist in method_definition.name. Common: ["GET","POST","PATCH","DELETE"].'),
|
|
1667
|
+
publishedMethods: z.array(z.string()).optional()
|
|
1474
1668
|
.describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
|
|
1475
1669
|
isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
|
|
1476
1670
|
description: z.string().optional().describe('Route description'),
|
|
@@ -1514,7 +1708,7 @@ server.tool(
|
|
|
1514
1708
|
publishedMethods: publishedMethods || [],
|
|
1515
1709
|
},
|
|
1516
1710
|
routesReloaded: true,
|
|
1517
|
-
next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET"
|
|
1711
|
+
next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET", sourceCode }) for custom code. Create extra method_definition.name rows first for custom methods such as PUT.`,
|
|
1518
1712
|
}, null, 2) }] };
|
|
1519
1713
|
},
|
|
1520
1714
|
);
|
|
@@ -1531,9 +1725,9 @@ server.tool(
|
|
|
1531
1725
|
].join(' '),
|
|
1532
1726
|
{
|
|
1533
1727
|
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
1534
|
-
method: z.
|
|
1535
|
-
.describe('Single
|
|
1536
|
-
methods: z.array(z.
|
|
1728
|
+
method: z.string().optional()
|
|
1729
|
+
.describe('Single method_definition.name to create. Prefer this for one handler.'),
|
|
1730
|
+
methods: z.array(z.string()).optional()
|
|
1537
1731
|
.describe('Batch create multiple handlers. Use only when the same sourceCode applies to every method.'),
|
|
1538
1732
|
sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
|
|
1539
1733
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
|
|
@@ -1595,8 +1789,8 @@ server.tool(
|
|
|
1595
1789
|
name: z.string().describe('Hook name (unique per route)'),
|
|
1596
1790
|
code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
|
|
1597
1791
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
|
|
1598
|
-
methods: z.array(z.
|
|
1599
|
-
.describe('
|
|
1792
|
+
methods: z.array(z.string()).optional()
|
|
1793
|
+
.describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
|
|
1600
1794
|
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
1601
1795
|
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
1602
1796
|
},
|
|
@@ -1649,8 +1843,8 @@ server.tool(
|
|
|
1649
1843
|
name: z.string().describe('Hook name (unique per route)'),
|
|
1650
1844
|
code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
|
|
1651
1845
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
|
|
1652
|
-
methods: z.array(z.
|
|
1653
|
-
.describe('
|
|
1846
|
+
methods: z.array(z.string()).optional()
|
|
1847
|
+
.describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
|
|
1654
1848
|
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
1655
1849
|
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
1656
1850
|
},
|
|
@@ -1781,7 +1975,7 @@ server.tool(
|
|
|
1781
1975
|
{
|
|
1782
1976
|
path: z.string().optional().describe('Route path, e.g. /user_definition'),
|
|
1783
1977
|
routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
|
|
1784
|
-
methods: z.array(z.
|
|
1978
|
+
methods: z.array(z.string()).describe('REST method names this permission allows. Each value must exist in method_definition.name.'),
|
|
1785
1979
|
roleId: z.union([z.string(), z.number()]).optional().describe('Role id scope'),
|
|
1786
1980
|
allowedUserIds: z.array(z.union([z.string(), z.number()])).optional().describe('Specific user ids scope'),
|
|
1787
1981
|
description: z.string().optional().describe('Admin note'),
|
|
@@ -1826,7 +2020,7 @@ server.tool(
|
|
|
1826
2020
|
position: z.enum(['pre_auth', 'post_auth']).default('pre_auth').describe('Execution position for root guard'),
|
|
1827
2021
|
routeId: z.union([z.string(), z.number()]).optional().describe('Optional route id'),
|
|
1828
2022
|
path: z.string().optional().describe('Optional route path'),
|
|
1829
|
-
methods: z.array(z.
|
|
2023
|
+
methods: z.array(z.string()).optional().describe('Method names this guard applies to. Empty means all configured behavior for route/global.'),
|
|
1830
2024
|
combinator: z.enum(['and', 'or']).default('and').describe('How child guards/rules combine'),
|
|
1831
2025
|
priority: z.number().optional().default(0).describe('Lower runs first'),
|
|
1832
2026
|
isGlobal: z.boolean().optional().default(false).describe('Apply globally instead of one route'),
|