@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -497,11 +497,11 @@ return @DATA\`
497
497
  tableName: "route_definition",
498
498
  id: "<route_id>",
499
499
  data: {
500
- publishedMethods: [{ id: 1 }]
500
+ publishedMethods: [{ id: "<GET_method_id_from_list_methods>" }]
501
501
  }
502
502
  })`,
503
503
  notes: [
504
- 'Method id 1 is GET. Use method_definition if you need to confirm method ids.',
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** (for REST route publishedMethods, availableMethods, skipRoleGuardMethods): GET=1, POST=2, PATCH=3, DELETE=4. Query `method_definition` table if unsure.',
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:** `"publishedMethods": [{"id": 1}]` (publishes GET). Multiple: `[{"id": 1}, {"id": 2}]` (publishes GET + POST).',
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.',
@@ -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. REST methods are GET/POST/PATCH/DELETE.',
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.method).filter(Boolean),
201
- publishedMethods: (route.publishedMethods || []).map((method) => method.method).filter(Boolean),
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, method: method.method })),
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, method: method.method })),
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.method] = m.id || m._id;
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
- method: item.method || methodIdNameMap[String(getId(item))] || null,
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 ? { ...item.method, method: state.methodIdNameMap[String(getId(item.method))] || item.method.method || null } : 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) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
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) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
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) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
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.enum(['GET', 'POST', 'PATCH', 'DELETE']).default('GET').describe('HTTP method'),
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 && method !== 'GET' ? { body } : {}),
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.enum(['GET', 'POST', 'PATCH', 'DELETE']))
1472
- .describe('HTTP methods this route supports (availableMethods). Common: ["GET","POST","PATCH","DELETE"]'),
1473
- publishedMethods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
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"|"POST"|"PATCH"|"DELETE", sourceCode }) for custom code.`,
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.enum(['GET', 'POST', 'PATCH', 'DELETE']).optional()
1535
- .describe('Single method to create. Prefer this for one handler.'),
1536
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
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.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1599
- .describe('Methods this hook applies to. Default: all REST methods.'),
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.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1653
- .describe('Methods this hook applies to. Default: all REST methods.'),
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.enum(['GET', 'POST', 'PATCH', 'DELETE'])).describe('REST methods this permission allows'),
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.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional().describe('Methods this guard applies to. Empty means all configured behavior for route/global.'),
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'),