@enfyra/mcp-server 0.0.60 → 0.0.62

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
@@ -3,7 +3,7 @@
3
3
  MCP server for managing Enfyra instances from **Codex**, **Claude Code**, **Cursor**, and other MCP-compatible clients. All operations go through Enfyra's REST API.
4
4
 
5
5
 
6
- **LLM rules (REST, GraphQL, auth, URL, mutation `create_{tableName}`, etc.):** not in this README — see **`src/lib/mcp-instructions.js`** (content sent via MCP `instructions`), **`src/lib/mcp-examples.js`** (concrete examples loaded through `get_enfyra_examples`), and tool descriptions in **`src/index.mjs`**. This README only covers **MCP installation and configuration** for users/devs.
6
+ **LLM rules (REST, GraphQL, auth, URL, mutation `create_{tableName}`, etc.):** not in this README — see **`src/lib/mcp-instructions.js`** (content sent via MCP `instructions`), **`src/lib/mcp-examples.js`** (concrete examples loaded through `get_enfyra_examples`), and tool descriptions in **`src/mcp-server-entry.mjs`**. This README only covers **MCP installation and configuration** for users/devs.
7
7
 
8
8
  **Official docs:** [Claude Code MCP](https://docs.anthropic.com/en/docs/claude-code/mcp) · [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings) · [Cursor MCP (`mcp.json`)](https://cursor.com/docs/context/mcp)
9
9
 
@@ -186,6 +186,10 @@ Use this block in any host-specific `mcp.json` / `mcpServers` merge (adjust env
186
186
 
187
187
  Schema and script tools include safety guards for LLM callers: generic record mutations validate request fields against live metadata, script-backed records must validate `sourceCode` before save through `/admin/script/validate` and fail closed if validation is unavailable, relation metadata rejects physical FK/junction inputs, custom routes reject `mainTableId` unless the path is the canonical table route, schema tools serialize table/column/relation changes, and destructive deletes require `confirm=true` after returning a preview.
188
188
 
189
+ Quick checklist for a new LLM using Enfyra MCP: discover the live system first, inspect the specific table/route, load the matching example category, mutate with explicit fields and relation property names, validate or test scripts/routes before relying on them, re-read the saved row when mutation output is summarized, and preview destructive operations before confirming.
190
+
191
+ Use `update_script_source` when updating existing long script-backed records such as `flow_step_definition`, `route_handler_definition`, hook tables, websocket scripts, GraphQL scripts, or bootstrap scripts. It accepts raw `sourceCode` directly, validates the source, and saves `sourceCode`/`scriptLanguage` without requiring the caller to manually JSON-escape the full script. Use generic `update_record` for small record patches or patches that include non-script metadata fields.
192
+
189
193
  For route contracts that intentionally keep workflow fields out of request bodies, generic `create_record`, `update_record`, and `delete_record` accept optional `queryParams` as a JSON object string. For example, a renewal workflow can keep `expires_at=YYYY-MM-DD` in the URL query while `validateBody` remains enabled for the table body.
190
194
 
191
195
  ### `ENFYRA_API_URL` — use the app proxy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -293,28 +293,40 @@ GET /enfyra/post?filter={"<primaryKeyFromMetadata>":{"_eq":123}}&limit=1`,
293
293
  },
294
294
  {
295
295
  name: 'Count without loading all rows',
296
- code: `GET /enfyra/chat_message_read?fields=id&limit=1&meta=filterCount&filter={
297
- "member": { "id": { "_eq": "<currentUserId>" } },
298
- "isRead": { "_eq": false }
299
- }`,
296
+ code: `query_table({
297
+ tableName: "chat_message_read",
298
+ fields: ["id"],
299
+ limit: 1,
300
+ meta: "filterCount",
301
+ filter: JSON.stringify({
302
+ member: { id: { _eq: "<currentUserId>" } },
303
+ isRead: { _eq: false }
304
+ })
305
+ })`,
300
306
  notes: [
301
307
  'Use meta=totalCount with no filter and meta=filterCount with a filter.',
308
+ 'MCP count_records wraps this pattern for simple counts.',
302
309
  'Do not fetch all rows only to count them.',
303
310
  ],
304
311
  },
305
312
  {
306
313
  name: 'Deep relation query',
307
- code: `GET /enfyra/order?fields=id,total,customer&deep={
308
- "customer": { "fields": "id,email,displayName" },
309
- "items": {
310
- "fields": "id,quantity,product",
311
- "limit": 20,
312
- "deep": {
313
- "product": { "fields": "id,name,price" }
314
+ code: `query_table({
315
+ tableName: "order",
316
+ fields: ["id", "total", "customer"],
317
+ deep: JSON.stringify({
318
+ customer: { fields: "id,email,displayName" },
319
+ items: {
320
+ fields: "id,quantity,product",
321
+ limit: 20,
322
+ deep: {
323
+ product: { fields: "id,name,price" }
324
+ }
314
325
  }
315
- }
316
- }`,
326
+ })
327
+ })`,
317
328
  notes: [
329
+ 'Use query_table deep for normal MCP reads; use test_rest_endpoint only when you need a custom raw URL or route behavior test.',
318
330
  'deep keys must be relation property names.',
319
331
  'Allowed deep options are fields, filter, sort, limit, page, and deep.',
320
332
  'Do not invent deep keys like members unless members is a relation on that table.',
@@ -497,11 +509,11 @@ return @DATA\`
497
509
  tableName: "route_definition",
498
510
  id: "<route_id>",
499
511
  data: {
500
- publishedMethods: [{ id: 1 }]
512
+ publishedMethods: [{ id: "<GET_method_id_from_list_methods>" }]
501
513
  }
502
514
  })`,
503
515
  notes: [
504
- 'Method id 1 is GET. Use method_definition if you need to confirm method ids.',
516
+ 'Method ids are instance data. Use list_methods or inspect_route output to resolve the GET method id first.',
505
517
  'publishedMethods controls anonymous route access. Route permissions are not for public access.',
506
518
  'Route permissions apply when the method is not public.',
507
519
  ],
@@ -512,11 +524,12 @@ return @DATA\`
512
524
  tableName: "user_definition",
513
525
  columnName: "email",
514
526
  ruleType: "format",
515
- ruleConfig: JSON.stringify({ format: "email" }),
527
+ value: JSON.stringify({ v: "email" }),
516
528
  message: "Please enter a valid email address"
517
529
  })`,
518
530
  notes: [
519
531
  'Column rules validate canonical POST/PATCH body payloads.',
532
+ 'The rule value payload uses the { v: ... } shape; do not pass ruleConfig.',
520
533
  'Use column rules before writing custom validation code when the rule is simple.',
521
534
  ],
522
535
  },
@@ -767,16 +780,24 @@ const uploaded = await fetch("/enfyra/files/upload", {
767
780
  },
768
781
  {
769
782
  name: 'Use uploaded file in handler',
770
- code: `const file = $ctx.$uploadedFile
783
+ code: `const file = @UPLOADED_FILE
771
784
  if (!file) @THROW400("File is required")
772
785
 
773
- return {
774
- filename: file.originalname,
775
- mimetype: file.mimetype,
776
- size: file.size
777
- }`,
786
+ const saved = await @STORAGE.$upload({
787
+ file,
788
+ storageConfig: @BODY.storageConfig,
789
+ folder: @BODY.folder,
790
+ title: @BODY.title,
791
+ description: @BODY.description
792
+ })
793
+
794
+ return saved`,
778
795
  notes: [
779
796
  'Use file-specific context only in upload-capable routes.',
797
+ 'For request uploads, pass file: @UPLOADED_FILE to @STORAGE.$upload/@STORAGE.$update so Enfyra streams from the temp file path.',
798
+ 'Use @STORAGE.$registerFile when an external process already uploaded the object and the script only needs to create the file_definition record.',
799
+ 'Do not read @UPLOADED_FILE.path into a Buffer and do not generate examples using @UPLOADED_FILE.buffer.',
800
+ 'Use buffer only for small generated or transformed files, such as image thumbnails.',
780
801
  ],
781
802
  },
782
803
  ],
@@ -802,6 +823,7 @@ update_method({
802
823
  })`,
803
824
  notes: [
804
825
  'Use dedicated method tools instead of generic CRUD on method_definition.',
826
+ 'The backend stores the method label in method_definition.name; do not send or filter a method_definition.method field.',
805
827
  'buttonColor is the badge background and textColor is the badge text color.',
806
828
  'The eApp management UI is /settings/methods.',
807
829
  'delete_method is preview-first and should only be used for unused custom methods.',
@@ -29,6 +29,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
29
29
  `**Full URL:** base + path segment. Example for table \`post\`: \`${examplePost}\`.`,
30
30
  '',
31
31
  '### First-step rule: discover before answering architecture/capability questions',
32
+ '- **New LLM checklist:** discover live context first → inspect the specific table/route → load matching examples → mutate with explicit fields and relation property names → validate/test scripts or routes → re-read saved rows when mutation output is summarized → preview destructive actions before confirming.',
32
33
  '- If the user asks what Enfyra supports, how to build a feature, which API exists, or whether a tool/schema path can do something, call **`discover_enfyra_system`** first. It reads live metadata, route definitions, method rows, route-backed tables, no-route tables, and capability areas.',
33
34
  '- If the question depends on DB type, primary key convention, cache/reload/runtime state, active GraphQL/flow/websocket/storage counts, or admin surfaces, call **`discover_runtime_context`**.',
34
35
  '- If the question depends on filters, sorting, deep relations, relation property names, field permissions, or table-specific query examples, call **`discover_query_capabilities`**; pass `tableName` when known.',
@@ -109,14 +110,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
109
110
  '- For generic MCP `create_record` and `update_record`, the `data` argument is a **JSON string**, not a JavaScript object. Example: `data: "{\\"name\\":\\"Starter\\"}"`. If the host gives a validation error saying `data` expected string, stringify the object before calling the tool.',
110
111
  '- Generic MCP `create_record` and `update_record` validate body keys against live metadata before sending REST. If a field such as `expiredAt` is not in metadata, do not bypass body validation; add the metadata field through schema tools or pass route workflow fields in the tool `queryParams` JSON when that route explicitly owns a query contract such as `{"expired_at":"2026-09-20"}`.',
111
112
  '- Generic MCP script-table mutations reject `compiledCode`, reject legacy `code` aliases, and must validate `sourceCode` with `/admin/script/validate` before saving. If validation is unavailable or fails, the save must fail closed. Prefer dedicated `create_handler`, `create_pre_hook`, and `create_post_hook` tools for route code.',
113
+ '- For long script updates on existing flow steps, handlers, hooks, websocket scripts, GraphQL scripts, or bootstrap scripts, prefer **`update_script_source`** over generic `update_record`. It accepts raw `sourceCode` as a normal tool argument, validates it, then PATCHes `sourceCode`/`scriptLanguage`; this avoids brittle manual JSON escaping for large scripts.',
112
114
  '- Generic MCP `delete_record` is destructive but preview-first: the first call without `confirm=true` returns the target preview; call it again with `confirm=true` only after the user explicitly approves deletion. Use `queryParams` for route-specific confirmation contracts instead of putting those fields in the body.',
113
115
  '- Relation fields (publishedMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
114
116
  '- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
115
117
  ' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
116
118
  ' - **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.',
119
+ '- **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.',
120
+ '- `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
121
  '- **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).',
122
+ '- **Right:** first query method records, then pass their ids, for example `"publishedMethods": [{"id": <GET_METHOD_ID>}]`. Multiple methods use multiple id objects.',
120
123
  '- **To unset:** pass empty array `"publishedMethods": []`.',
121
124
  '',
122
125
  '### Dynamic script `$repos` mutation return shape',
@@ -137,16 +140,18 @@ export function buildMcpServerInstructions(apiBaseUrl) {
137
140
  '- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
138
141
  '',
139
142
  '### Dynamic script syntax preference',
140
- '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, and `@THROW400`–`@THROW503`. `$ctx.$env` currently has no macro; access it directly when needed.',
143
+ '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@STORAGE`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, `@ENV`, and `@THROW400`–`@THROW503`.',
141
144
  '- Use Enfyra native throw helpers for intentional errors: `@THROW400("message")`, `@THROW403()`, `@THROW404("resource", id)`, or `$ctx.$throw[400]("message")`. Do not generate `throw new Error(...)` for user/domain errors in handlers, hooks, flows, websocket events, OAuth scripts, or admin-generated scripts.',
142
145
  '- For regular app data that must be encrypted at rest, create the column with `isEncrypted=true`; Enfyra database-query hooks will encrypt on insert/update and decrypt after select. `isEncrypted` does not imply immutability; use `isUpdatable=false` separately only when the field itself must be immutable. Do not filter or sort on encrypted fields. Do not generate new route pre-hooks for manual encryption.',
143
146
  '- Enfyra scripts use `$helpers.$crypto` for bounded crypto helpers such as `randomUUID()`, `randomBytes(size, encoding)`, `sha256(value, encoding)`, `hmacSha256(value, secret, encoding)`, and `generateSshKeyPair(comment)`. Do not generate legacy `$helpers.$ssh` or `$helpers.$secrets` usage.',
144
147
  '- `$ctx.$env` exposes only a sanitized process env snapshot. Current OSS deny keys are exact matches: `DB_URI`, `DB_REPLICA_URIS`, `REDIS_URI`, `SECRET_KEY`, and `ADMIN_PASSWORD`. Do not read secrets from `$ctx.$env`; model app secrets as unpublished `isEncrypted=true` fields instead.',
145
148
  '- Script-backed records use one shared persistence contract: `sourceCode` is the editable source, `scriptLanguage` controls compilation, and `compiledCode` is generated by the server from `sourceCode`. Do not hand-edit or send stale `compiledCode` from generated tools; save `sourceCode`/`scriptLanguage` through `PATCH /<script_table>/<id>` and let the server persist generated `compiledCode` internally. Public metadata may mark `compiledCode` non-updatable, but the server engine must still preserve the generated value after normalization.',
149
+ '- Use MCP `update_script_source` for existing long script-backed records so callers pass raw source text instead of hand-escaped JSON strings. Use generic `update_record` only when the patch is small or includes non-script metadata fields.',
146
150
  '- For route handlers specifically, the field is also `sourceCode`. Older names such as `logic` are wrong for current Enfyra REST CRUD and will be rejected. Use MCP `create_handler` so it writes `sourceCode` and resolves method ids correctly.',
147
151
  '- MCP `create_pre_hook` and `create_post_hook` accept a user-facing `code` argument but persist it as `sourceCode` with `scriptLanguage`. Do not call raw `create_record` with a `code` field for hook tables; backend request validation rejects `code` on REST CRUD.',
148
152
  '- Before saving generated script code, validate it with `POST /admin/script/validate` when available. It compiles with the server kernel and parses the executable async body without running side effects. Enfyra App `FormCodeEditor` also exposes a `Validate` action for this endpoint; use it before save/run when editing through the UI. If unavailable, use `run_admin_test`/`test_flow_step` as the closest validation path before saving.',
149
153
  '- Do not coerce dynamic script values with `String(...)`, `Number(...)`, or `Boolean(...)`. Enfyra payloads, user ids, record ids, and relation ids should keep their runtime type; validate required values and pass them through directly.',
154
+ '- Multipart request files are exposed as `@UPLOADED_FILE` / `$ctx.$uploadedFile` metadata with a server temp-file path. To save or replace that request file, call `@STORAGE.$upload({ file: @UPLOADED_FILE, ... })` or `@STORAGE.$update(id, { file: @UPLOADED_FILE, ... })` so Enfyra streams from disk. To register an object that already exists in storage without uploading bytes, call `@STORAGE.$registerFile({ filename, mimetype, location, size, storageConfig, ... })`. Do not generate `@UPLOADED_FILE.buffer` examples or read the temp file into a Buffer. Use `buffer` only for small generated/transformed files.',
150
155
  '- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
151
156
  '- Preferred: `const result = await @REPOS.main.create({ data: @BODY });`.',
152
157
  '- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
@@ -298,6 +303,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
298
303
  '- **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
304
  '- **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
305
  '- **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.',
306
+ '- **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
307
  '- **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
308
  '- **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
309
  '- **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,7 +312,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
306
312
  '- **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
313
  '- **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
314
  '- **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.',
309
- '- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `method_definition`. 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.',
315
+ '- **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.',
310
316
  '- **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.',
311
317
  '- **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.*`.',
312
318
  '- **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.',
@@ -405,11 +411,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
405
411
  `- \`discover_runtime_context\` → GET metadata/routes/method/runtime-backed tables and infer live primary key/backend context`,
406
412
  `- \`discover_query_capabilities\` → GET metadata/routes and summarize Query DSL/deep/table-specific query contracts`,
407
413
  `- \`discover_script_contexts\` → static runtime macro/context map for handlers/hooks/flows/websocket/GraphQL/extensions`,
408
- `- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args)`,
414
+ `- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args, including filter/sort/page/limit/fields plus optional meta/deep/aggregate)`,
409
415
  `- \`count_records\` → GET \`${base}/<tableName>?fields=id&limit=1&meta=totalCount|filterCount\``,
410
416
  `- \`find_one_record\` (by id) → GET \`${base}/<tableName>?filter=…&limit=1\``,
411
417
  `- \`create_record\` → POST \`${base}/<tableName>\` (optional tool queryParams append URL query)`,
412
418
  `- \`update_record\` → PATCH \`${base}/<tableName>/<id>\` (optional tool queryParams append URL query)`,
419
+ `- \`update_script_source\` → validates sourceCode with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
413
420
  `- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\` after preview + confirm=true (optional tool queryParams append URL query)`,
414
421
  `- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for page pass menuId). \`update_record\` on extension_definition to change code.`,
415
422
  `- Flow tables: \`${base}/flow_definition\`, \`${base}/flow_step_definition\`, \`${base}/flow_execution_definition\` — use standard CRUD tools.`,
@@ -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
  }
@@ -258,6 +258,23 @@ function parseJsonArg(value, fallback = undefined) {
258
258
  return JSON.parse(value);
259
259
  }
260
260
 
261
+ async function reloadRoutesResult() {
262
+ try {
263
+ const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
264
+ return {
265
+ attempted: true,
266
+ succeeded: true,
267
+ result,
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ attempted: true,
272
+ succeeded: false,
273
+ error: error?.message || String(error),
274
+ };
275
+ }
276
+ }
277
+
261
278
  function normalizeRestPath(path) {
262
279
  if (!path) return '/';
263
280
  if (/^https?:\/\//i.test(path)) {
@@ -376,8 +393,8 @@ function normalizeHexColorInput(value, fieldName) {
376
393
  }
377
394
 
378
395
  async function findMethodRecordByName(method) {
379
- const filter = encodeURIComponent(JSON.stringify({ method: { _eq: method } }));
380
- const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,method,buttonColor,textColor,isSystem`);
396
+ const filter = encodeURIComponent(JSON.stringify({ name: { _eq: method } }));
397
+ const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
381
398
  return unwrapData(result)[0] || null;
382
399
  }
383
400
 
@@ -476,7 +493,7 @@ server.tool(
476
493
  routes: routes.length,
477
494
  methods: methodsResult?.data?.length || 0,
478
495
  },
479
- methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, method: method.method })),
496
+ methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name, method: method.name })),
480
497
  capabilityAreas: CAPABILITY_AREAS.map((item) => ({
481
498
  ...item,
482
499
  presentTables: item.tables.filter((table) => tableNames.includes(table)),
@@ -579,7 +596,7 @@ server.tool(
579
596
  storageConfigs: storageResult?.data?.length || 0,
580
597
  settings: settingsResult?.data?.length || 0,
581
598
  },
582
- methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, method: method.method })),
599
+ methods: (methodsResult?.data || []).map((method) => ({ id: method.id || method._id, name: method.name, method: method.name })),
583
600
  routeRuntime: {
584
601
  routePattern: 'GET/POST /<route-path>; PATCH/DELETE /<route-path>/:id; no dynamic GET /<route-path>/:id.',
585
602
  adminRoutes: adminRoutes.map((route) => route.path).sort(),
@@ -698,15 +715,17 @@ server.tool(
698
715
  const payload = {
699
716
  transformer: {
700
717
  rule: 'Dynamic server scripts are transformed before sandbox execution. Macros expand to $ctx paths; comments are not transformed.',
701
- preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use @BODY/@QUERY/@PARAMS/@USER/@REPOS/@CACHE/@HELPERS/@SOCKET/@TRIGGER/@DATA/@ERROR/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
718
+ preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use @BODY/@QUERY/@PARAMS/@USER/@REPOS/@CACHE/@HELPERS/@STORAGE/@SOCKET/@TRIGGER/@DATA/@ERROR/@ENV/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
702
719
  coreMacros: {
703
720
  '@BODY': '$ctx.$body',
704
721
  '@QUERY': '$ctx.$query',
705
722
  '@PARAMS': '$ctx.$params',
706
723
  '@USER': '$ctx.$user',
724
+ '@ENV': '$ctx.$env',
707
725
  '@REPOS': '$ctx.$repos',
708
726
  '@CACHE': '$ctx.$cache',
709
727
  '@HELPERS': '$ctx.$helpers',
728
+ '@STORAGE': '$ctx.$storage',
710
729
  '@SOCKET': '$ctx.$socket',
711
730
  '@DATA': '$ctx.$data',
712
731
  '@STATUS': '$ctx.$statusCode',
@@ -731,20 +750,21 @@ server.tool(
731
750
  throws: '@THROW400 through @THROW503 and @THROW map to $ctx.$throw helpers.',
732
751
  helpers: {
733
752
  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.',
753
+ 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 file_definition 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.',
734
754
  },
735
755
  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.',
736
756
  },
737
757
  contexts: {
738
758
  preHook: {
739
759
  runs: 'Before handler.',
740
- data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@REPOS', '@CACHE', '@HELPERS', '@THROW*', '@SOCKET emit helpers'],
760
+ data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@REPOS', '@CACHE', '@HELPERS', '@STORAGE', '@THROW*', '@SOCKET emit helpers'],
741
761
  queryContract: '@QUERY.filter is initialized as an object. When adding RLS/scope filters in pre-hooks, merge directly with _and; do not add defensive type checks around @QUERY.filter.',
742
762
  rlsPattern: 'For relation-scoped reads, mutate @QUERY.filter instead of returning data. Example: const incomingFilter = @QUERY.filter; const scope = { memberships: { member: { id: { _eq: @USER.id } } } }; @QUERY.filter = Object.keys(incomingFilter).length ? { _and: [incomingFilter, scope] } : scope;',
743
763
  returnBehavior: 'Returning a non-undefined value skips handler and becomes response data.',
744
764
  },
745
765
  handler: {
746
766
  runs: 'Main route logic, or canonical CRUD if no handler overrides.',
747
- data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@REPOS.main', '@REPOS.secure', '@CACHE', '@HELPERS', '@PKGS', '@SOCKET emit helpers', '@TRIGGER'],
767
+ data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@UPLOADED_FILE for multipart request file metadata', '@REPOS.main', '@REPOS.secure', '@CACHE', '@HELPERS', '@STORAGE', '@PKGS', '@SOCKET emit helpers', '@TRIGGER'],
748
768
  returnBehavior: 'Return value becomes response body unless post-hook changes it.',
749
769
  },
750
770
  postHook: {
@@ -754,7 +774,7 @@ server.tool(
754
774
  },
755
775
  flowStep: {
756
776
  runs: 'Inside flow execution or admin flow step test.',
757
- data: ['@FLOW_PAYLOAD', '@FLOW_LAST', '@FLOW', '@FLOW_META', '#table_name', '@CACHE', '@HELPERS', '@SOCKET', '@TRIGGER'],
777
+ data: ['@FLOW_PAYLOAD', '@FLOW_LAST', '@FLOW', '@FLOW_META', '#table_name', '@CACHE', '@HELPERS', '@STORAGE', '@SOCKET', '@TRIGGER'],
758
778
  resultBehavior: 'Step return value is injected into @FLOW.<step.key> and @FLOW_LAST.',
759
779
  branching: 'Condition steps use JavaScript truthy/falsy result; child branch is true/false.',
760
780
  },
@@ -788,7 +808,7 @@ server.tool(
788
808
  },
789
809
  socketInHttpOrFlow: 'HTTP/flow context can emitToUser/emitToRoom/emitToGateway/broadcast, but cannot reply/join/leave/disconnect because there is no bound socket.',
790
810
  packages: 'Server packages installed through install_package are exposed as $ctx.$pkgs.packageName in server scripts.',
791
- files: 'Upload helpers are on $helpers; raw create_record on file_definition is not equivalent to multipart upload/storage rollback.',
811
+ files: 'Upload helpers are on $storage; raw create_record on file_definition 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 file_definition record without uploading bytes. Use buffer only for small generated files.',
792
812
  },
793
813
  adminTesting: {
794
814
  flowStep: 'Use test_flow_step or run_admin_test(kind=flow_step).',
@@ -846,15 +866,23 @@ server.tool('query_table', 'Query any route-backed table. Default response is mi
846
866
  page: z.number().optional().describe('Page number (default: 1)'),
847
867
  limit: z.number().optional().describe('Items per page. Default: 10. Use count_records for counts.'),
848
868
  fields: z.array(z.string()).optional().describe('Fields to select. If omitted, MCP selects only the table primary key to avoid oversized responses.'),
849
- }, async ({ tableName, filter, sort, page, limit, fields }) => {
869
+ meta: z.string().optional().describe('Optional REST meta request, e.g. "totalCount", "filterCount", or aggregate modes supported by the route. Use count_records for simple counts.'),
870
+ deep: z.string().optional().describe('Optional deep relation fetch object as JSON string. Keys must be relation propertyName values.'),
871
+ aggregate: z.string().optional().describe('Optional aggregate object as JSON string, keyed by real fields/relations. Results are returned in response.meta.aggregate when supported.'),
872
+ }, async ({ tableName, filter, sort, page, limit, fields, meta, deep, aggregate }) => {
850
873
  validateTableName(tableName);
851
874
  validateFilter(filter);
875
+ parseJsonArg(deep, undefined);
876
+ parseJsonArg(aggregate, undefined);
852
877
 
853
878
  const queryParams = new URLSearchParams();
854
879
  const selectedFields = fields && fields.length > 0 ? fields : [await getPrimaryFieldName(tableName)];
855
880
  if (filter) queryParams.set('filter', filter);
856
881
  if (sort) queryParams.set('sort', sort);
857
882
  if (page) queryParams.set('page', String(page));
883
+ if (meta) queryParams.set('meta', meta);
884
+ if (deep) queryParams.set('deep', deep);
885
+ if (aggregate) queryParams.set('aggregate', aggregate);
858
886
  queryParams.set('limit', String(limit || 10));
859
887
  queryParams.set('fields', selectedFields.join(','));
860
888
 
@@ -866,6 +894,11 @@ server.tool('query_table', 'Query any route-backed table. Default response is mi
866
894
  tableName,
867
895
  fields: selectedFields,
868
896
  limit: limit || 10,
897
+ queryOptions: {
898
+ meta: meta || null,
899
+ deep: deep ? parseJsonArg(deep, null) : null,
900
+ aggregate: aggregate ? parseJsonArg(aggregate, null) : null,
901
+ },
869
902
  minimalDefaultApplied: !(fields && fields.length > 0),
870
903
  meta: result?.meta,
871
904
  data: result?.data || [],
@@ -1008,6 +1041,49 @@ server.tool('update_record', 'Update an existing record by ID using PATCH. The t
1008
1041
  }, null, 2) }] };
1009
1042
  });
1010
1043
 
1044
+ server.tool(
1045
+ 'update_script_source',
1046
+ [
1047
+ 'Update sourceCode on a script-backed record without forcing the caller to JSON-escape long code.',
1048
+ 'Use this for flow_step_definition, route_handler_definition, pre_hook_definition, post_hook_definition, websocket_event_definition, websocket_definition, gql_definition, and bootstrap_script_definition.',
1049
+ 'The tool validates sourceCode through /admin/script/validate before saving and never accepts compiledCode.',
1050
+ ].join(' '),
1051
+ {
1052
+ tableName: z.enum([
1053
+ 'route_handler_definition',
1054
+ 'pre_hook_definition',
1055
+ 'post_hook_definition',
1056
+ 'flow_step_definition',
1057
+ 'websocket_event_definition',
1058
+ 'websocket_definition',
1059
+ 'gql_definition',
1060
+ 'bootstrap_script_definition',
1061
+ ]).describe('Script-backed table to update'),
1062
+ id: z.string().describe('Record ID to update'),
1063
+ sourceCode: z.string().describe('Editable script sourceCode. Pass the raw code string; do not JSON-escape it yourself.'),
1064
+ scriptLanguage: z.string().optional().default('javascript').describe('Script language, usually javascript or typescript'),
1065
+ },
1066
+ async ({ tableName, id, sourceCode, scriptLanguage }) => {
1067
+ validateTableName(tableName);
1068
+ const prepared = await prepareGenericMutation(
1069
+ tableName,
1070
+ JSON.stringify({ sourceCode, scriptLanguage }),
1071
+ );
1072
+ const result = await fetchAPI(
1073
+ ENFYRA_API_URL,
1074
+ `/${tableName}/${encodeURIComponent(String(id))}`,
1075
+ { method: 'PATCH', body: JSON.stringify(prepared.payload) },
1076
+ );
1077
+ return { content: [{ type: 'text', text: JSON.stringify({
1078
+ ...summarizeMutationResult(result, 'updated_script_source', tableName),
1079
+ id,
1080
+ sourceLength: sourceCode.length,
1081
+ scriptLanguage,
1082
+ scriptValidation: prepared.scriptValidation,
1083
+ }, null, 2) }] };
1084
+ },
1085
+ );
1086
+
1011
1087
  server.tool('delete_record', 'Delete a record by ID', {
1012
1088
  tableName: z.string().describe('Table name'),
1013
1089
  id: z.string().describe('Record ID to delete'),
@@ -1050,10 +1126,11 @@ server.tool(
1050
1126
  'List method_definition records with their UI colors. Use this before creating route methods or method-colored UI.',
1051
1127
  {},
1052
1128
  async () => {
1053
- const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?fields=id,_id,method,buttonColor,textColor,isSystem&sort=method&limit=0');
1129
+ const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?fields=id,_id,name,buttonColor,textColor,isSystem&sort=name&limit=0');
1054
1130
  const methods = unwrapData(result).map((method) => ({
1055
1131
  id: getId(method),
1056
- method: method.method,
1132
+ name: method.name,
1133
+ method: method.name,
1057
1134
  buttonColor: method.buttonColor,
1058
1135
  textColor: method.textColor,
1059
1136
  isSystem: method.isSystem === true,
@@ -1082,7 +1159,7 @@ server.tool(
1082
1159
  throw new Error(`Method ${normalizedMethod} already exists with id ${getId(existing)}. Use update_method to change colors.`);
1083
1160
  }
1084
1161
  const body = {
1085
- method: normalizedMethod,
1162
+ name: normalizedMethod,
1086
1163
  buttonColor: normalizeHexColorInput(buttonColor, 'buttonColor'),
1087
1164
  textColor: normalizeHexColorInput(textColor, 'textColor'),
1088
1165
  isSystem: isSystem === true,
@@ -1094,6 +1171,7 @@ server.tool(
1094
1171
  _methodMap = null;
1095
1172
  return { content: [{ type: 'text', text: JSON.stringify({
1096
1173
  ...summarizeMutationResult(result, 'created', 'method_definition'),
1174
+ name: normalizedMethod,
1097
1175
  method: normalizedMethod,
1098
1176
  appUi: '/settings/methods',
1099
1177
  }, null, 2) }] };
@@ -1128,7 +1206,7 @@ server.tool(
1128
1206
  body.textColor = normalizeHexColorInput(textColor, 'textColor');
1129
1207
  }
1130
1208
  if (method !== undefined && id) {
1131
- body.method = normalizeMethodNameInput(method);
1209
+ body.name = normalizeMethodNameInput(method);
1132
1210
  }
1133
1211
  if (Object.keys(body).length === 0) {
1134
1212
  throw new Error('Provide buttonColor, textColor, or a new method name.');
@@ -1168,13 +1246,14 @@ server.tool(
1168
1246
  if (!target) {
1169
1247
  const primaryKey = await getPrimaryFieldName('method_definition');
1170
1248
  const filter = encodeURIComponent(JSON.stringify({ [primaryKey]: { _eq: targetId } }));
1171
- const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,method,buttonColor,textColor,isSystem`);
1249
+ const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,name,buttonColor,textColor,isSystem`);
1172
1250
  target = unwrapData(result)[0] || null;
1173
1251
  }
1174
1252
  return { content: [{ type: 'text', text: JSON.stringify({
1175
1253
  action: 'delete_method_preview',
1176
1254
  id: targetId,
1177
- method: target?.method,
1255
+ name: target?.name,
1256
+ method: target?.name,
1178
1257
  isSystem: target?.isSystem === true,
1179
1258
  destructive: true,
1180
1259
  warning: 'Only delete unused custom methods. Deleting a method can affect route method relations.',
@@ -1265,7 +1344,7 @@ async function getMethodMap() {
1265
1344
  const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?limit=0');
1266
1345
  _methodMap = {};
1267
1346
  for (const m of result.data) {
1268
- _methodMap[m.method] = m.id || m._id;
1347
+ _methodMap[m.name] = m.id || m._id;
1269
1348
  }
1270
1349
  return _methodMap;
1271
1350
  }
@@ -1289,7 +1368,8 @@ function withMethodNames(records, methodIdNameMap, field = 'methods') {
1289
1368
  [field]: Array.isArray(record?.[field])
1290
1369
  ? record[field].map((item) => ({
1291
1370
  ...item,
1292
- method: item.method || methodIdNameMap[String(getId(item))] || null,
1371
+ name: item.name || methodIdNameMap[String(getId(item))] || null,
1372
+ method: item.name || methodIdNameMap[String(getId(item))] || null,
1293
1373
  }))
1294
1374
  : record?.[field],
1295
1375
  }));
@@ -1344,7 +1424,11 @@ function enrichRoute(route, state) {
1344
1424
  .filter((item) => sameId(refId(item.route), routeId))
1345
1425
  .map((item) => pickCodeSummary({
1346
1426
  ...item,
1347
- method: item.method ? { ...item.method, method: state.methodIdNameMap[String(getId(item.method))] || item.method.method || null } : item.method,
1427
+ method: item.method ? {
1428
+ ...item.method,
1429
+ name: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
1430
+ method: state.methodIdNameMap[String(getId(item.method))] || item.method.name || null,
1431
+ } : item.method,
1348
1432
  }, 'sourceCode'));
1349
1433
  const routePreHooks = withMethodNames(
1350
1434
  state.preHooks.filter((item) => item.isGlobal || sameId(refId(item.route), routeId)),
@@ -1369,13 +1453,25 @@ function enrichRoute(route, state) {
1369
1453
  return {
1370
1454
  ...route,
1371
1455
  availableMethods: Array.isArray(route.availableMethods)
1372
- ? route.availableMethods.map((method) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
1456
+ ? route.availableMethods.map((method) => ({
1457
+ ...method,
1458
+ name: method.name || state.methodIdNameMap[String(getId(method))] || null,
1459
+ method: method.name || state.methodIdNameMap[String(getId(method))] || null,
1460
+ }))
1373
1461
  : route.availableMethods,
1374
1462
  publishedMethods: Array.isArray(route.publishedMethods)
1375
- ? route.publishedMethods.map((method) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
1463
+ ? route.publishedMethods.map((method) => ({
1464
+ ...method,
1465
+ name: method.name || state.methodIdNameMap[String(getId(method))] || null,
1466
+ method: method.name || state.methodIdNameMap[String(getId(method))] || null,
1467
+ }))
1376
1468
  : route.publishedMethods,
1377
1469
  skipRoleGuardMethods: Array.isArray(route.skipRoleGuardMethods)
1378
- ? route.skipRoleGuardMethods.map((method) => ({ ...method, method: method.method || state.methodIdNameMap[String(getId(method))] || null }))
1470
+ ? route.skipRoleGuardMethods.map((method) => ({
1471
+ ...method,
1472
+ name: method.name || state.methodIdNameMap[String(getId(method))] || null,
1473
+ method: method.name || state.methodIdNameMap[String(getId(method))] || null,
1474
+ }))
1379
1475
  : route.skipRoleGuardMethods,
1380
1476
  handlers: routeHandlers,
1381
1477
  preHooks: routePreHooks,
@@ -1536,7 +1632,7 @@ server.tool(
1536
1632
  'Use this after inspecting a route or changing handlers/hooks/guards. Pass paths like /table_definition?limit=1, not external URLs.',
1537
1633
  ].join(' '),
1538
1634
  {
1539
- method: z.enum(['GET', 'POST', 'PATCH', 'DELETE']).default('GET').describe('HTTP method'),
1635
+ method: z.string().optional().default('GET').describe('HTTP method name. Must exist in method_definition.name for Enfyra route-backed calls.'),
1540
1636
  path: z.string().describe('Enfyra API path, e.g. /route_definition?limit=1'),
1541
1637
  query: z.string().optional().describe('Optional query params JSON object, merged onto path query string'),
1542
1638
  body: z.string().optional().describe('Optional JSON request body string'),
@@ -1544,6 +1640,7 @@ server.tool(
1544
1640
  useAuth: z.boolean().optional().default(true).describe('Attach MCP admin Bearer token. Set false to test published/public access.'),
1545
1641
  },
1546
1642
  async ({ method, path, query, body, headers, useAuth }) => {
1643
+ const httpMethod = normalizeMethodNameInput(method || 'GET');
1547
1644
  const restPath = normalizeRestPath(path);
1548
1645
  const url = new URL(`${ENFYRA_API_URL.replace(/\/$/, '')}${restPath}`);
1549
1646
  const queryObj = parseJsonArg(query, {});
@@ -1561,9 +1658,9 @@ server.tool(
1561
1658
 
1562
1659
  const started = Date.now();
1563
1660
  const response = await fetch(url, {
1564
- method,
1661
+ method: httpMethod,
1565
1662
  headers: requestHeaders,
1566
- ...(body !== undefined && body !== null && method !== 'GET' ? { body } : {}),
1663
+ ...(body !== undefined && body !== null && httpMethod !== 'GET' ? { body } : {}),
1567
1664
  });
1568
1665
  const contentType = response.headers.get('content-type') || '';
1569
1666
  const responseText = await response.text();
@@ -1574,7 +1671,7 @@ server.tool(
1574
1671
 
1575
1672
  const payload = {
1576
1673
  request: {
1577
- method,
1674
+ method: httpMethod,
1578
1675
  url: url.toString(),
1579
1676
  authenticated: !!useAuth,
1580
1677
  },
@@ -1641,9 +1738,9 @@ server.tool(
1641
1738
  {
1642
1739
  path: z.string().describe('URL path, must start with / (e.g., "/my-endpoint")'),
1643
1740
  mainTableId: z.union([z.string(), z.number()]).optional().describe('Only set for the canonical table route `/<table_name>`. Omit for every custom route.'),
1644
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE']))
1645
- .describe('HTTP methods this route supports (availableMethods). Common: ["GET","POST","PATCH","DELETE"]'),
1646
- publishedMethods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1741
+ methods: z.array(z.string())
1742
+ .describe('HTTP method names this route supports (availableMethods). Each value must exist in method_definition.name. Common: ["GET","POST","PATCH","DELETE"].'),
1743
+ publishedMethods: z.array(z.string()).optional()
1647
1744
  .describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
1648
1745
  isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
1649
1746
  description: z.string().optional().describe('Route description'),
@@ -1674,7 +1771,7 @@ server.tool(
1674
1771
  body: JSON.stringify(body),
1675
1772
  });
1676
1773
 
1677
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1774
+ const routeReload = await reloadRoutesResult();
1678
1775
 
1679
1776
  const created = firstDataRecord(result);
1680
1777
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -1686,8 +1783,8 @@ server.tool(
1686
1783
  availableMethods: methods,
1687
1784
  publishedMethods: publishedMethods || [],
1688
1785
  },
1689
- routesReloaded: true,
1690
- next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET"|"POST"|"PATCH"|"DELETE", sourceCode }) for custom code.`,
1786
+ routeReload,
1787
+ 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.`,
1691
1788
  }, null, 2) }] };
1692
1789
  },
1693
1790
  );
@@ -1704,9 +1801,9 @@ server.tool(
1704
1801
  ].join(' '),
1705
1802
  {
1706
1803
  routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
1707
- method: z.enum(['GET', 'POST', 'PATCH', 'DELETE']).optional()
1708
- .describe('Single method to create. Prefer this for one handler.'),
1709
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1804
+ method: z.string().optional()
1805
+ .describe('Single method_definition.name to create. Prefer this for one handler.'),
1806
+ methods: z.array(z.string()).optional()
1710
1807
  .describe('Batch create multiple handlers. Use only when the same sourceCode applies to every method.'),
1711
1808
  sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
1712
1809
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
@@ -1743,13 +1840,13 @@ server.tool(
1743
1840
  });
1744
1841
  }
1745
1842
 
1746
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1843
+ const routeReload = await reloadRoutesResult();
1747
1844
 
1748
1845
  return { content: [{ type: 'text', text: JSON.stringify({
1749
1846
  action: 'created',
1750
1847
  handlers: results,
1751
1848
  scriptValidation,
1752
- routesReloaded: true,
1849
+ routeReload,
1753
1850
  detailHint: 'Use inspect_route with the same routeId/path to inspect saved handlers.',
1754
1851
  }, null, 2) }] };
1755
1852
  },
@@ -1768,8 +1865,8 @@ server.tool(
1768
1865
  name: z.string().describe('Hook name (unique per route)'),
1769
1866
  code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
1770
1867
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
1771
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1772
- .describe('Methods this hook applies to. Default: all REST methods.'),
1868
+ methods: z.array(z.string()).optional()
1869
+ .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
1773
1870
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
1774
1871
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
1775
1872
  },
@@ -1794,7 +1891,7 @@ server.tool(
1794
1891
  }),
1795
1892
  });
1796
1893
 
1797
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1894
+ const routeReload = await reloadRoutesResult();
1798
1895
 
1799
1896
  const created = firstDataRecord(result);
1800
1897
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -1804,7 +1901,7 @@ server.tool(
1804
1901
  name,
1805
1902
  routeId,
1806
1903
  scriptValidation,
1807
- routesReloaded: true,
1904
+ routeReload,
1808
1905
  }, null, 2) }] };
1809
1906
  },
1810
1907
  );
@@ -1822,8 +1919,8 @@ server.tool(
1822
1919
  name: z.string().describe('Hook name (unique per route)'),
1823
1920
  code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
1824
1921
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
1825
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1826
- .describe('Methods this hook applies to. Default: all REST methods.'),
1922
+ methods: z.array(z.string()).optional()
1923
+ .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
1827
1924
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
1828
1925
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
1829
1926
  },
@@ -1848,7 +1945,7 @@ server.tool(
1848
1945
  }),
1849
1946
  });
1850
1947
 
1851
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1948
+ const routeReload = await reloadRoutesResult();
1852
1949
 
1853
1950
  const created = firstDataRecord(result);
1854
1951
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -1858,7 +1955,7 @@ server.tool(
1858
1955
  name,
1859
1956
  routeId,
1860
1957
  scriptValidation,
1861
- routesReloaded: true,
1958
+ routeReload,
1862
1959
  }, null, 2) }] };
1863
1960
  },
1864
1961
  );
@@ -1954,7 +2051,7 @@ server.tool(
1954
2051
  {
1955
2052
  path: z.string().optional().describe('Route path, e.g. /user_definition'),
1956
2053
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1957
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).describe('REST methods this permission allows'),
2054
+ methods: z.array(z.string()).describe('REST method names this permission allows. Each value must exist in method_definition.name.'),
1958
2055
  roleId: z.union([z.string(), z.number()]).optional().describe('Role id scope'),
1959
2056
  allowedUserIds: z.array(z.union([z.string(), z.number()])).optional().describe('Specific user ids scope'),
1960
2057
  description: z.string().optional().describe('Admin note'),
@@ -1983,8 +2080,14 @@ server.tool(
1983
2080
  method: 'POST',
1984
2081
  body: JSON.stringify(body),
1985
2082
  });
1986
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1987
- return { content: [{ type: 'text', text: `Route permission created for ${route.path}. Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
2083
+ const routeReload = await reloadRoutesResult();
2084
+ return { content: [{ type: 'text', text: JSON.stringify({
2085
+ action: 'created',
2086
+ kind: 'route_permission',
2087
+ route: route.path,
2088
+ routeReload,
2089
+ result,
2090
+ }, null, 2) }] };
1988
2091
  },
1989
2092
  );
1990
2093
 
@@ -1999,7 +2102,7 @@ server.tool(
1999
2102
  position: z.enum(['pre_auth', 'post_auth']).default('pre_auth').describe('Execution position for root guard'),
2000
2103
  routeId: z.union([z.string(), z.number()]).optional().describe('Optional route id'),
2001
2104
  path: z.string().optional().describe('Optional route path'),
2002
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional().describe('Methods this guard applies to. Empty means all configured behavior for route/global.'),
2105
+ methods: z.array(z.string()).optional().describe('Method names this guard applies to. Empty means all configured behavior for route/global.'),
2003
2106
  combinator: z.enum(['and', 'or']).default('and').describe('How child guards/rules combine'),
2004
2107
  priority: z.number().optional().default(0).describe('Lower runs first'),
2005
2108
  isGlobal: z.boolean().optional().default(false).describe('Apply globally instead of one route'),
@@ -2121,7 +2224,10 @@ server.tool('search_logs', 'Search for ERROR or WARN logs across recent log file
2121
2224
  }, async ({ level, keyword, limit }) => {
2122
2225
  const logFilesResult = await fetchAPI(ENFYRA_API_URL, '/logs');
2123
2226
  const logFiles = logFilesResult.files || [];
2124
- const recentFiles = logFiles.filter(f => f.name.includes('app-') || f.name.includes('error-'));
2227
+ const recentFiles = logFiles.filter((file) => {
2228
+ const name = file?.name || '';
2229
+ return /^app[.-]/.test(name) || /^error[.-]/.test(name);
2230
+ });
2125
2231
  const results = [];
2126
2232
  for (const file of recentFiles.slice(0, 3)) {
2127
2233
  try {