@enfyra/mcp-server 0.0.90 → 0.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.90",
3
+ "version": "0.0.92",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,10 @@
1
1
  /**
2
- * MCP server `instructions` surfaced to the host (e.g. Claude Code) for the LLM.
3
- * Single source of truth for API/REST/GraphQL/auth/mutation naming; README does NOT feed the model.
4
- * Maintain all assistant-facing rules here (and tool descriptions in index.mjs).
2
+ * MCP server instructions are sent to the host model on connect.
3
+ * Keep this small: route the model to the right discovery/example tools and
4
+ * keep only contracts that must be known before any tool call.
5
5
  */
6
6
 
7
- /** GraphQL SDL + HTTP endpoint are under the same base as REST: `{ENFYRA_API_URL}/graphql` and `{ENFYRA_API_URL}/graphql-schema` (Nuxt proxies these when base ends with `/api`). */
7
+ /** GraphQL SDL + HTTP endpoint are under the same base as REST. */
8
8
  export function buildGraphqlUrls(apiBaseUrl) {
9
9
  const base = String(apiBaseUrl || '').replace(/\/$/, '');
10
10
  return {
@@ -16,455 +16,47 @@ export function buildGraphqlUrls(apiBaseUrl) {
16
16
  export function buildMcpServerInstructions(apiBaseUrl) {
17
17
  const base = String(apiBaseUrl || '').replace(/\/$/, '');
18
18
  const { graphqlHttpUrl, graphqlSchemaUrl } = buildGraphqlUrls(apiBaseUrl);
19
- const getList = `${base}/<table_name>`;
20
- const getOneById = `${base}/<table_name>?filter={"<primaryKeyFromMetadata>":{"_eq":"<id>"}}&limit=1`;
21
- const patchOne = `${base}/<table_name>/<id>`;
22
- const delOne = `${base}/<table_name>/<id>`;
23
- const examplePost = `${base}/post`;
24
19
 
25
20
  return [
26
- '## Enfyra API endpoints (answer user questions with these rules)',
27
- '',
28
- `**API base for this session:** \`${base}\` (from env ENFYRA_API_URL, no trailing slash).`,
29
- `**Full URL:** base + path segment. Example for table \`post\`: \`${examplePost}\`.`,
30
- '',
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.',
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.',
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`**.',
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.',
36
- '- If writing or reviewing handler/hook/flow/websocket/extension logic, call **`discover_script_contexts`** first so macros and `$ctx` fields are not mixed across runtime surfaces.',
37
- '- If changing or auditing an existing feature, table, route, or long script, call **`trace_metadata_usage`** to find related tables/routes/script-backed records, then **`get_script_source`** for full untruncated source. Use **`patch_script_source`** for exact previewed search/replace edits with hash checking and validation.',
38
- '- If generating concrete code, schema payloads, SSR app config, OAuth wiring, Socket.IO clients/events, flows, files, extensions, or permission/RLS examples, call **`get_enfyra_examples`** for the matching category before writing the final answer. Examples are grouped by category and are intentionally more concrete than these global rules.',
39
- '- Treat hardcoded instructions as operating rules, but use live discovery as the final check for this running instance. Do not infer missing capabilities from a narrow tool schema; check metadata/routes or the relevant specialized tool first.',
40
- '- If there is no dedicated MCP tool for a subsystem, use the route-backed metadata table with `query_table` / `create_record` / `update_record` / `delete_record`, after confirming that table has a route. If the table is no-route, use the canonical specialized tool or parent table workflow instead.',
41
- '- MCP read tools are intentionally **minimal by default**. `query_table` without `fields` returns only the table primary key with a small hint. Always pass explicit `fields` when you need details, and use `inspect_table` / `inspect_route` before guessing field names. Every list/query call must explicitly pass either `limit` for a bounded page or `all: true` for a complete list. When the user asks for all matching rows, pass `all: true` instead of inventing arbitrary limits such as 30 or 50.',
42
- '- MCP mutation tools return only ids/status by default. If you need the saved row, immediately call `find_one_record` or `query_table` with explicit `fields`; do not expect create/update tools to echo full records.',
43
- '- **Operator posture:** act from the Enfyra contracts encoded here and in live metadata. Do not turn normal implementation details into speculative warnings. Ask the user only when a new design/product decision is needed, when metadata is genuinely ambiguous, or when a tool/runtime result proves a concrete problem. If a behavior is expected by contract, state it as expected behavior or omit it; do not present it as an audit finding.',
44
- '',
45
- '### Capability map (current Enfyra system)',
46
- '- **Schema/metadata:** `enfyra_table`, `enfyra_relation`, and schema tools manage tables, columns, relations, validation, and migrations. `enfyra_column` is internal/no-route; columns are created/updated through table schema operations.',
47
- '- **Dynamic REST API:** `enfyra_route`, `enfyra_route_handler`, `enfyra_pre_hook`, `enfyra_post_hook`, `enfyra_route_permission`, and `enfyra_method` define paths, methods, handlers, hooks, and permissions.',
48
- '- **Auth/OAuth/session:** `enfyra_user`, `enfyra_role`, `enfyra_api_token`, `enfyra_oauth_config`, `enfyra_oauth_account`; `enfyra_session` and `enfyra_api_token` are internal/no-route. OAuth is browser redirect only. MCP uses `ENFYRA_API_TOKEN` through `/auth/token/exchange`; configure tokens from the Enfyra admin UI `/me`. `enfyra_user` is the single source of truth for app users.',
49
- '- **Guards/permissions/validation:** `enfyra_guard`, `enfyra_guard_rule`, `enfyra_field_permission`, and `enfyra_column_rule` control route guards, field access, and request body validation.',
50
- '- **GraphQL:** `enfyra_graphql` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
51
- '- **Files/storage/assets:** `enfyra_file`, `enfyra_file_permission`, `enfyra_folder`, `enfyra_storage_config` plus upload/assets routes and file helpers.',
52
- '- **WebSocket:** `enfyra_websocket` and `enfyra_websocket_event` define Socket.IO gateways/events. Use `run_admin_test` for websocket scripts.',
53
- '- **Flows:** `enfyra_flow`, `enfyra_flow_step`, `enfyra_flow_execution`; compose workflows from small operation-sized steps, use `test_flow_step`, `run_admin_test`, and `trigger_flow` for runtime checks.',
54
- '- **Extensions/packages/menus:** `enfyra_extension`, `enfyra_menu`, `enfyra_package`, `enfyra_bootstrap_script`; extensions are Vue SFC only, and packages should be installed with `install_package`.',
55
- '- **Platform config:** `enfyra_setting`, `enfyra_cors_origin`, reload endpoints, logs, and metadata endpoints.',
56
- '',
57
- '### ENFYRA_API_URL (MCP must use the app proxy)',
58
- '- **Required default:** point MCP at the Nuxt/app origin proxy, e.g. `http://localhost:3000/api`. Nuxt proxies `/api/*` to the hidden Enfyra backend (`API_URL`, e.g. `http://localhost:1105`).',
59
- '- Treat the Enfyra backend as private infrastructure. Do not tell app-building agents to connect MCP, browser code, SSR code, or generated app code directly to the backend host/port.',
60
- '- Direct-to-backend URLs such as `http://localhost:1105` are only for Enfyra core/server debugging when the user explicitly asks to bypass the app proxy. They are not valid guidance for normal apps or deployable demos.',
61
- '- GraphQL: `{base}/graphql` and `{base}/graphql-schema` always share this same base.',
62
- '',
63
- '### When the user asks how to connect a Nuxt/Next/SSR app to Enfyra',
64
- '- This is guidance for the assistant to answer users and generate app code. It is not a separate MCP tool workflow.',
65
- '- Follow the app-origin proxy pattern by default: expose one same-origin proxy prefix such as **`/enfyra/**`** and route it to the hidden Enfyra API base, e.g. Nuxt `routeRules: { "/enfyra/**": { proxy: { to: `${API_URL}/**`, fetchOptions: { redirect: "manual" } } } }`. Browser/generated app code calls `/enfyra/...`, not the raw Enfyra backend URL. Keep redirects manual so OAuth set-cookie redirects reach the browser with their `Set-Cookie` headers.',
66
- '- Do **not** generate custom login/logout/me server routes that manually set `accessToken`, `refreshToken`, or `expTime` cookies when a same-origin proxy is enough. Let the proxied Enfyra API own its auth response and cookies.',
67
- '- Password login in generated Nuxt code is **`POST /enfyra/login`**, not `/enfyra/auth/login` and not a custom SSR `/api/login` wrapper.',
68
- '- Fetch the current user with **`GET /enfyra/me`** and logout with **`POST /enfyra/logout`**. Browser fetches stay same-origin and credentials/cookies flow through the proxy. Do not read JWTs in browser JavaScript for this mode.',
69
- '- OAuth starts on the same proxy prefix, e.g. **`GET /enfyra/auth/{provider}?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra`**. `redirect` must be an absolute `http(s)` URL with the app origin. `cookieBridgePrefix` is the third app proxy prefix that forwards to the Enfyra API; Enfyra normalizes it, so `enfyra`, `/enfyra`, and `/enfyra/` all mean `/enfyra`. Use token-query callback handling only when the app intentionally manages tokens itself.',
70
- '- Socket.IO uses the app bridge too. Browser clients should connect to the gateway namespace with the Socket.IO transport path on the app origin, e.g. `io("/chat", { path: "/socket.io", withCredentials: true })`, while Nuxt proxies `/socket.io/**` to the Enfyra app bridge `/ws/socket.io/**`. Do not connect browser code directly to the hidden backend Socket.IO endpoint.',
71
- '- For generated authenticated browser apps, initialize the Socket.IO client once in a client-only app bootstrap/plugin after the current user/session is known. Do not create the first socket lazily inside each page. Pages/components should only add event listeners, react to events, debounce REST refreshes when needed, and remove listeners on unmount.',
72
- '- If a project explicitly standardizes on `/api/**` instead of `/enfyra/**`, keep the same proxy behavior under that prefix: proxy to the Enfyra API and avoid generated cookie-management routes unless the user asks for a custom auth boundary.',
73
- '- If you are explaining MCP\'s own internal authentication, that is separate: this MCP server exchanges `ENFYRA_API_TOKEN` against `{ENFYRA_API_URL}/auth/token/exchange` before authenticated tool calls. The raw `efy_pat_*` token is never a Bearer token. For normal app work, `ENFYRA_API_URL` must still be the app proxy base such as `{{ nuxtApp }}/api`.',
74
- '',
75
- '### Routes vs tables (custom endpoints, handlers, hooks)',
76
- '- REST-first workflow for any feature: **`inspect_feature`** to locate candidates → **`inspect_table`** for table/field/relation/rule context → **`inspect_route`** for handlers/hooks/guards/permissions → **`test_rest_endpoint`** to verify the actual HTTP behavior.',
77
- '- Use **`audit_route_access`** before debugging route 403s or granting access. Use **`ensure_route_access`** for authenticated role/user route access because it resolves route, role, method ids, validates route.availableMethods, merges existing permissions, and reloads routes. Use low-level **`create_route_permission`** only when you intentionally need a new raw permission row.',
78
- '- Use **`create_column_rule`** for standard request validation, **`create_field_permission`** for per-field read/create/update rules, and **`create_guard`** for pre/post-auth request gates.',
79
- '- Prefer these REST inspection/operator tools over raw `query_table` on system tables when changing route behavior. They resolve ids, methods, route paths, code previews, and cache reloads for the model.',
80
- '- If the user asks for a **new route**, **URL path**, **custom API endpoint**, **handler**, **pre-hook**, **post-hook**, or to **test** that kind of logic: use MCP **`create_route`** and **omit `mainTableId`**. `mainTable` is only a marker for canonical table routes like `/orders`; custom paths such as `/orders/stats`, `/reports/summary`, `/auth/login`, or `/me` must not set it.',
81
- '- **Wrong pattern:** calling **`create_table`** just to get an HTTP path, then overriding handlers on the **default** auto route `/{table_name}`. That adds unnecessary schema and breaks the usual CRUD surface for that table.',
82
- '- **`create_table`** is only when the user needs **new persisted data** (new entity + columns). It is **not** the right tool when the goal is only a new path or custom script.',
83
- '- **Right pattern:** **`create_route`** without `mainTableId` → optional **`create_handler`** / **`create_pre_hook`** / **`create_post_hook`** on **that route’s id** (from **`get_all_routes`** after create). Handler/hook code must query explicit repos such as `$ctx.$repos.orders`; do not rely on `$repos.main` for custom routes.',
84
- '- **Handler contract:** `create_handler` takes `routeId`, `method` (or `methods` for batch), `sourceCode`, optional `scriptLanguage`, and optional `timeout`. Do **not** send `logic`, `name`, or `compiledCode`; backend CRUD rejects `logic` and `compiledCode` is generated by the server.',
85
- '',
86
- '### Guards (request gates and rate limits)',
87
- '- Guards are metadata-driven request gates stored in `enfyra_guard` and `enfyra_guard_rule`. Use them for IP allow/deny lists and coarse rate limits before writing custom pre-hooks. Use route permissions for authenticated access, field permissions for field access, and column rules for request body validation.',
88
- '- Guard execution positions: `pre_auth` runs before JWT/auth and only has client IP + route/method context; use it for anonymous/IP/route-wide gates such as `rate_limit_by_ip`, `rate_limit_by_route`, `ip_whitelist`, and `ip_blacklist`. `post_auth` runs after auth/RoleGuard and has the authenticated user id; use it for `rate_limit_by_user` or rules scoped to specific users.',
89
- '- Rule types: `rate_limit_by_ip`, `rate_limit_by_user`, `rate_limit_by_route`, `ip_whitelist`, and `ip_blacklist`. Rate-limit rule config is `{"maxRequests": number, "perSeconds": number}`. IP list config is `{"ips": ["203.0.113.10", "198.51.100.0/24"]}`; CIDR matching is IPv4-oriented.',
90
- '- Root guards can attach to one route through `path` or `routeId`, or globally with `isGlobal: true`. `methods` is an array of HTTP method names; omit it to apply the guard to all methods on that route/global scope. Child guards inherit the root `position`; only root guard `position`, `route`, `methods`, and `isGlobal` determine where it runs.',
91
- '- Guard `combinator` controls evaluation: `and` rejects on the first failing rule/child; `or` allows when any rule/child passes and rejects only if all fail. Lower `priority` runs first among sibling guards/rules.',
92
- '- `create_guard` defaults `isEnabled` to `false` to avoid lockouts. For risky global or IP whitelist guards, create disabled first, inspect the saved guard/rules, then enable only after the rule config and route/method scope are confirmed.',
93
- '- `create_guard` reloads guard cache best-effort after creation. After changing guard metadata, verify behavior with `inspect_route({ path })` and `test_rest_endpoint`; use `/admin/reload/guards` only when verification shows stale guard behavior.',
94
- '- Do not use `rate_limit_by_user` or user-scoped rule `userIds` on `pre_auth` guards. Server guard cache drops `rate_limit_by_user` from pre-auth trees and ignores user scopes there because no user exists yet.',
95
- '',
96
- '### After a new table is created',
97
- '- MCP **`create_table` supports creating columns and relations in the same call**: pass `columns` and `relations` as JSON arrays. Use `create_relation` only when adding a relation to an existing table later.',
98
- '- MCP **`create_table` supports `isSingleRecord` directly**. Set `isSingleRecord: true` in the create call for settings/config tables that should keep only one record; do not create first and then patch only for this flag.',
99
- '- MCP **`create_table` and `update_table` support `indexes` and `uniques`** as JSON arrays of logical field groups. Use compound indexes for hot filters and unread/read state, e.g. `indexes: [["member","isRead","conversation"],["conversation","member","isRead"]]` and `uniques: [["message","member"]]`. Relation property names are allowed; Enfyra resolves them to physical FK columns for SQL and Mongo.',
100
- '- Enfyra auto-generates single-field indexes for `createdAt`, `updatedAt`, and scalar time columns with type `date`, `datetime`, or `timestamp`. SQL appends `id` as the stable tie-breaker, and Mongo appends `_id`. Do not add duplicate single-field indexes for these time fields; add explicit compound indexes only when a hot query combines the time field with other filters such as status, owner, tenant, or relation fields.',
101
- '- MCP **`create_table` does not accept `alias`**. Do not invent or send alias during table creation; default route/schema behavior is based on `name`. Use `update_table` later only when alias truly needs to change.',
102
- '- In `create_table.relations`, each relation uses `targetTable`, `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. Prefer table id or `{id}` for `targetTable`; MCP also accepts an exact table name such as `"enfyra_user"` and resolves it to the live table id before mutation. The target table must already exist.',
103
- '- **Use `enfyra_user` as the only user table.** Do not create app-specific user/profile mapping tables such as `chat_profile`, `app_user`, `customer_user`, or tables that only mirror/link Enfyra users. If an app needs extra user fields or user relations, add columns/relations directly on `enfyra_user`.',
104
- '- When modeling features that involve users, relate domain tables directly to `enfyra_user` through real Enfyra relations. Examples: `chat_conversation_member.member` → `enfyra_user`, `chat_message.sender` → `enfyra_user`, `order.customer` → `enfyra_user`. Do not create duplicate scalar columns like `userId` or separate profile records just to point back to a user.',
105
- '- **Do not create reverse relations on `enfyra_user` by default.** For domain records that point to users, create only the owning relation on the domain table, e.g. `chat_message.sender -> enfyra_user`, and omit `inversePropertyName` unless the user explicitly asks for a reverse user field. Reverse fields like `enfyra_user.chatMessages`, `enfyra_user.orders`, or `enfyra_user.memberships` bloat user metadata and make `fields=*` user queries heavy.',
106
- '- **Prefer real relations over relation-shaped columns.** If a field represents another record or list of records, model it as `relations`, not as columns such as `userId`, `author_id`, `categoryIds`, `teamIds`, `itemsJson`, or object/array JSON containing related records. Ask only when the user explicitly wants denormalized snapshot data.',
107
- '- Common mapping: one owner record → `many-to-one`; one record has many children → define the child `many-to-one` and use inverse/read deep relation; peer/tag lists → `many-to-many`; one profile/settings row per parent → `one-to-one` when supported by the model.',
108
- '- If the user asks to add a foreign key field, interpret it as a relation request unless they explicitly say they need a plain scalar column. Do not create both a relation and a duplicate scalar FK column for the same concept.',
109
- '- **Never ask for or provide physical FK column names** when creating/updating relations. Do not include `fkCol`, `fkColumn`, `foreignKeyColumn`, `sourceColumn`, `targetColumn`, `junctionSourceColumn`, or `junctionTargetColumn` in create/update payloads unless you are only displaying existing metadata. Enfyra relation cascade derives physical FK/junction names from `propertyName` and table metadata, then hides FK columns from app form/schema definition.',
110
- '- For relation CRUD payloads and generated server logic, the public interface is the relation `propertyName`: example create body uses `"author": {"id": 1}` or `"author": 1`, not `"authorId"` or a physical FK column. Query/deep/filter keys also use relation `propertyName`, e.g. `{ "author": { "_eq": 1 } }` or `{ "author": { "id": { "_eq": 1 } } }`. Do not hardcode physical FK fields such as `userId`, `conversationId`, `senderId`, or `memberId` in handlers, hooks, flows, services, generated apps, or extension-adjacent code unless you are deliberately doing low-level raw SQL outside Enfyra metadata APIs. An API/event payload may carry a business identifier such as `conversationId`, but DB reads/writes still use relation properties such as `conversation`, `sender`, and `member`.',
111
- '- **Realtime/chat unread modeling:** unread/read is per user and per message. Do not put `read` or `lastRead` on `chat_conversation` globally. Prefer a join table such as `chat_message_read` with relations `message`, `conversation`, `member`, boolean `isRead`, nullable `readAt`, unique `["message","member"]`, and indexes `["member","isRead","conversation"]` plus `["conversation","member","isRead"]`. The UI can render a dot by checking existence of unread rows instead of counting every unread message.',
112
- '- **Realtime/chat latest message modeling:** keep `chat_conversation.lastMessage` as a nullable many-to-one relation to `chat_message`. Do not duplicate latest message text/date onto `chat_conversation`. Load conversation lists with relation fields such as `lastMessage.id,lastMessage.text,lastMessage.createdAt`, update `lastMessage` after the message is persisted, and repair it in a `DELETE /chat_message` post-hook when deleting the current latest message.',
113
- '- **Chat deletion modeling:** user-level delete/leave should remove the user from `chat_conversation_member` or otherwise make membership inactive. Do not add duplicated `deleted_at` state to both conversation and membership unless the product explicitly needs restore/audit behavior. A DM deleted for both sides is a membership operation for both members; a group is physically deleted only when no memberships remain.',
114
- '- **Chat conversation title:** for DMs, compute the display title from the other visible member on the server/script response when possible. Do not trust the client to rename a DM from the current user perspective. Group titles can be generated from member display names until the product adds a custom group name.',
115
- '- **Chat unread UI:** default to a boolean unread dot. Do not show unread counts unless the user explicitly asks for exact counts; exact counts require more query work and are not the default chat-list UX.',
116
- '- Enfyra creates a **default** route at `/{table_name}` using the table **name** from `create_table` (not the alias). Prefer **`create_route`** for additional or custom paths instead of new tables.',
117
- '- **Four REST HTTP operations** on that resource:',
118
- ` - **GET** \`${getList}\` — list / filter (query: filter, sort, page, limit, fields, meta).`,
119
- ` - **POST** \`${getList}\` — create (JSON body).`,
120
- ` - **PATCH** \`${patchOne}\` — update one row.`,
121
- ` - **DELETE** \`${delOne}\` — delete one row.`,
122
- `- **No** **GET** \`${base}/<table_name>/<id>\`. For one row by id use **GET** \`${getOneById}\` or MCP \`query_table\` / \`find_one_record\`.`,
123
- '',
124
- '### Relation field format (create_record / update_record)',
125
- '- 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.',
126
- '- 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"}`.',
127
- '- 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.',
128
- '- For long script updates on existing flow steps, handlers, hooks, websocket scripts, GraphQL scripts, or bootstrap scripts, call **`get_script_source`** first to read full source and hash. Prefer **`patch_script_source`** for focused exact edits with preview/hash validation, or **`update_script_source`** when replacing the entire source. Avoid generic `update_record` for long script changes.',
129
- '- 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.',
130
- '- Relation fields (publicMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
131
- '- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
132
- ' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
133
- ' - **One-to-many / many-to-many:** `"publicMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
134
- '- **Method IDs** are instance data, not a stable contract. Query `enfyra_method` or use method names through MCP route helpers before setting `publicMethods`, `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.',
135
- '- `enfyra_method.name` is the unique backend field for the HTTP method label. Do not filter, create, or update a `method` field on `enfyra_method`. MCP method tools accept an input named `method` for usability, but they write/read `name` on the server.',
136
- '- **Wrong:** `"publicMethods": ["GET"]` or `"publicMethods": [{"method": "GET"}]` — rejected or silently ignored.',
137
- '- **Right:** first query method records, then pass their ids, for example `"publicMethods": [{"id": <GET_METHOD_ID>}]`. Multiple methods use multiple id objects.',
138
- '- **To unset:** pass empty array `"publicMethods": []`.',
139
- '',
140
- '### Dynamic script `$repos` mutation return shape',
141
- '- In handler/hook/flow/websocket scripts, `$ctx.$repos.<table>.create({ data })` and `$ctx.$repos.<table>.update({ id, data })` return the same collection-shaped result as `find`: `{ data: [...], count? }`.',
142
- '- The `data` field is always an array for `create`/`update`, even when exactly one record was created or updated. If the script needs the record object, it must read `result.data[0]` or `result.data?.[0] ?? null`.',
143
- '- Wrong: `const id = result.data.id`, `return result.data`, or assuming `create`/`update` returns the bare row object when the script needs one record.',
144
- '- Right: `const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;`.',
145
- '',
146
- '### Auth and publicMethods (Enfyra server)',
147
- '- Each route has **publicMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
148
- '- If the **current request method** is listed in **publicMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
149
- '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
150
- '- Owner-scoped GET handlers must preserve caller filters and merge the owner scope for normal users, while root admin operational views may bypass only the owner scope with `@USER.isRootAdmin` and must keep caller filters intact. Never replace `@QUERY.filter` with `{}` for root admins; doing so breaks detail reads, pagination, and `debugMode=true` explain checks.',
151
- '- GET pre-hooks and handlers must preserve client-controlled query shape: do not override `@QUERY.fields`, `@QUERY.deep`, `@QUERY.sort`, `@QUERY.limit`, `@QUERY.page`, `@QUERY.meta`, `@QUERY.aggregate`, or `debugMode` for canonical table reads. RLS may only merge security filters into `@QUERY.filter`. If a route intentionally returns a fixed summary/aggregate workflow response, make it a clearly custom endpoint contract instead of changing canonical table projection.',
152
- '- MCP tools that use `fetchAPI` authenticate with the configured `ENFYRA_API_TOKEN`. Explain to users that **direct HTTP** calls need a Bearer token unless the route/method is public.',
153
- '',
154
- '### Post-hooks (REST)',
155
- '- **post-hooks always run** after the handler, including when the handler or a pre-hook throws — then `@ERROR` / `$ctx.$error` is set and `@DATA` is null.',
156
- '- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
157
- '',
158
- '### Dynamic script syntax preference',
159
- '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REQ`, `@RES`, `@REPOS`, `@HELPERS`, `@FETCH`, `@STORAGE`, `@UPLOADED_FILE`, `@CACHE`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, `@STATUS`, `@ENV`, `@PKGS`, `@LOGS`, `@SHARE`, `@API`, and `@THROW` / `@THROW400`–`@THROW503` when those context fields are available.',
160
- '- 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.',
161
- '- 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.',
162
- '- 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.',
163
- '- `$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.',
164
- '- 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`. `compiledCode` is expected to differ textually from `sourceCode` because macros such as `@USER`, `@REPOS`, and `@THROW400` are expanded during compilation; do not warn about a mismatch unless runtime behavior proves the compiled artifact is stale. 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.',
165
- '- Use MCP `get_script_source` for full untruncated source, `patch_script_source` for focused exact edits with preview/hash validation, and `update_script_source` for full-source replacement. Use generic `update_record` only when the patch is small or includes non-script metadata fields.',
166
- '- 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.',
167
- '- 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.',
168
- '- 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.',
169
- '- 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.',
170
- '- 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.',
171
- '- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
172
- '- Preferred: `const result = await @REPOS.main.create({ data: @BODY });`.',
173
- '- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
174
- '',
175
- '### Chat / realtime app rules learned from implementation review',
176
- '- Before generating a chat app, inspect live metadata for `enfyra_websocket`, `enfyra_websocket_event`, `chat_conversation`, `chat_conversation_member`, and `chat_message`. Do not assume table names, reverse relations, route permissions, or physical field casing.',
177
- '- For browser SSR apps, REST goes through the app proxy prefix and Socket.IO goes through an app-origin Socket.IO transport proxy. Third apps should connect to the namespace, e.g. `io("/chat", { path: "/socket.io", withCredentials: true })`, and proxy `/socket.io/**` to the Enfyra app bridge `/ws/socket.io/**`. Do not connect app browser code directly to the hidden backend. Do not add custom token cookies when the Enfyra app/proxy already owns cookies.',
178
- '- For authenticated realtime clients, create the socket as an application singleton from a client-only plugin/bootstrap once auth has resolved. Watch the shared current-user/session state: connect when a user exists, disconnect when the session clears, and let route components subscribe/unsubscribe listeners instead of owning the connection lifecycle.',
179
- '- On an authenticated gateway, Enfyra loads `enfyra_user` once for the socket and event scripts receive `@USER`. The server also joins `user_<userId>` after the connection script succeeds. Event scripts must not ask the client to send `senderId`; derive the sender relation from `@USER.id`. `chat:join` does not need to join `user_<userId>` again.',
180
- '- Use `chat:join` only for conversation rooms. Query `chat_conversation` with the current user membership and join `conversation:<conversationId>` rooms. Do not query all membership rows and accidentally join rooms named from member ids.',
181
- '- `chat:message` may accept a request/room identifier such as `conversationId`, but persistence must use relation properties: `conversation: { id: conversationId }` and `sender: { id: @USER.id }`. Broadcast to `conversation:<conversationId>` with `@SOCKET.broadcastToRoom`, then persist through `@REPOS` in the same event script. Do not trigger a flow just to save a chat message unless the product needs workflow semantics.',
182
- '- For new DMs, do not create an empty conversation just because the user selected a person. Navigate to a draft chat; create the conversation only when the first message is sent. If a DM already exists and is visible to the current user, navigate to it.',
183
- '- RLS for conversation lists belongs in a route pre-hook that merges into `@QUERY.filter` with `_and`; `@QUERY.filter` already defaults to `{}`. The membership filter must target the conversation membership relation, not a duplicated scalar user id.',
184
- '- Use cursor pagination for chat history. Initial load should fetch the newest messages and scroll down once; loading older messages must preserve scroll position and must not auto-scroll on new messages while the user is reading older history.',
185
- '- Typing state should be room-scoped and user-aware. Broadcast typing payloads with `@USER` identity through the conversation room, keep typing active while the input still has text, and clear it only when the input is empty or the socket disconnects.',
186
- '- If realtime disconnects, disable send controls immediately and show a prominent retry banner. The retry action can reload the page so REST state, socket rooms, and cookies all rehydrate from one known path.',
187
- '',
188
- '### `$cache` / `@CACHE` user cache',
189
- '- `$ctx.$cache` and the `@CACHE` macro use Enfyra-managed **user cache**, not the internal runtime metadata cache.',
190
- '- Script keys are logical keys such as `user:123` or `report:daily`. Do not include `NODE_NAME`, `user_cache:`, Redis prefixes, or another app namespace in script code.',
191
- '- On Redis-backed deployments, Enfyra stores user cache under the current app `NODE_NAME` namespace as `NODE_NAME:user_cache:*`. Admin Redis Key Editor uses the same storage contract, so values edited there are visible through `$cache` and `@CACHE`.',
192
- '- User cache is limited by `REDIS_USER_CACHE_LIMIT_MB` (default 30 MB). `REDIS_USER_CACHE_MAX_VALUE_BYTES` optionally rejects oversized single values when greater than 0.',
193
- '- When user cache exceeds its allocation, Enfyra evicts least-recently-used **user cache** keys only. System Redis keys such as runtime cache snapshots, BullMQ queues, Socket.IO, runtime telemetry, and locks are not counted or evicted by this quota.',
194
- '- Prefer `set(key, value, ttlMs)` with a TTL. `setNoExpire` is allowed, but persistent user-cache entries can still be evicted by the soft allocation limit.',
195
- '',
196
- '### OAuth login (browser / frontend — not the MCP `login` tool)',
197
- '- **MCP `login`** exchanges an API token through `POST {base}/auth/token/exchange`. It cannot complete OAuth (no browser redirect) and does not use email/password credentials.',
198
- '- **Supported providers (server):** `google`, `facebook`, `github` only.',
199
- '',
200
- '**Redirect URI must match everywhere (critical):**',
201
- '- Enfyra exposes OAuth callback at **`{ENFYRA_API_URL}/auth/{provider}/callback`**. Example when `ENFYRA_API_URL` is `http://localhost:3000/api`: **Google** callback URL is **`http://localhost:3000/api/auth/google/callback`** — i.e. `{ENFYRA_API_URL}/auth/google/callback` (same pattern for Facebook/GitHub: `.../auth/facebook/callback`, `.../auth/github/callback`).',
202
- '- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
203
- '- **Enfyra** (`enfyra_oauth_config` / OAuth settings): field **`redirectUri`** must be the **same string** as in Google Console — byte-for-byte. If they differ, Google or the server will reject the flow.',
204
- '- **Same-origin proxy mode:** generated Nuxt/Next apps should start OAuth through the same-origin proxy, e.g. `/enfyra/auth/google?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra`, and let Enfyra handle the auth response. Do not generate a set-cookie route unless the user explicitly chooses a custom SSR auth boundary.',
205
- '- **Manual token mode only:** `appCallbackUrl` is the frontend URL where Enfyra redirects after OAuth with `accessToken`, `refreshToken`, etc. in query. Use this only when the app intentionally manages tokens itself; it is not the preferred Nuxt/Next SSR pattern.',
206
- '',
207
- '**Server flow (for answering users or designing FE):**',
208
- '1. **Start login (redirect user in browser):** proxy-mode apps use `GET /enfyra/auth/{provider}?redirect=<URL_ENCODED_ABSOLUTE_RETURN_URL>&cookieBridgePrefix=/enfyra` from the app origin; direct/manual apps may use `GET {base}/auth/{provider}?redirect=<URL_ENCODED>`. `redirect` is required and is where to send the user after the whole flow.',
209
- '2. Server **302** to Google/Facebook/GitHub authorization page.',
210
- '3. Provider calls back: `GET {base}/auth/{provider}/callback?code=...&state=...` (server exchanges code, creates/links user, issues JWT).',
211
- '4. In same-origin proxy mode, Enfyra redirects to `{redirect.origin}{cookieBridgePrefix}/auth/set-cookies?...&redirect=<originalRedirect>`. That request goes through the app proxy to Enfyra, Enfyra returns `Set-Cookie`, then the browser is redirected to the original `redirect`. In manual token mode, backend redirects to `appCallbackUrl` with token query params. On failure, redirect includes `?error=...`.',
212
- '',
213
- '**Frontend build checklist:**',
214
- '- Nuxt/Next generated apps: implement a same-origin API proxy such as `/enfyra/**` to the Enfyra API. Browser code never stores JWTs.',
215
- '- **Password login:** `$fetch("/enfyra/login", { method: "POST", body })`.',
216
- '- **Fetch user/logout:** `$fetch("/enfyra/me")` and `$fetch("/enfyra/logout", { method: "POST" })`.',
217
- '- **“Login with Google” button:** `location.href = `/enfyra/auth/google?redirect=${encodeURIComponent(returnUrl)}&cookieBridgePrefix=${encodeURIComponent("/enfyra")}``, where `returnUrl` is absolute, e.g. `${location.origin}/chat`.',
218
- '- Manual token apps only: register `appCallbackUrl`, read token query params there, strip the URL, store tokens, and use `Authorization: Bearer`.',
219
- '- **Error handling:** If redirected with `?error=`, show message to user.',
220
- '- **Do not confuse:** Google’s **Authorized redirect URI** = Enfyra **`redirectUri`** = backend/proxy `{API_BASE}/auth/google/callback`. The app return URL is `redirect`, and the third-app proxy prefix is `cookieBridgePrefix`. `appCallbackUrl` is only for manual token mode.',
221
- '',
222
- '### System tables — which have REST routes',
223
- '- **Not all system tables have a REST route.** `query_table`, `find_one_record`, `create_record`, etc. all go through the dynamic REST API and will return **404** if the table has no registered route.',
224
- '- **`enfyra_column` and `enfyra_session` have NO route** — do NOT call `query_table("enfyra_column", …)` or `query_table("enfyra_session", …)`. They will 404.',
225
- '- Do not invent legacy system route names or physical FK tables. If a route name is not listed by `get_all_routes`, it is not a REST endpoint for generic CRUD. Use the concrete tables (`enfyra_pre_hook`, `enfyra_post_hook`, `enfyra_oauth_config`, etc.) or the dedicated MCP tool.',
226
- '- To check which tables have canonical CRUD routes, call `get_all_routes` and look for `mainTable`. Custom routes intentionally have no `mainTable`; inspect their handlers/hooks to see which repos they touch.',
227
- '- **Tables confirmed to have REST routes (system):** `enfyra_bootstrap_script`, `enfyra_column_rule`, `enfyra_cors_origin`, `enfyra_extension`, `enfyra_field_permission`, `enfyra_file`, `enfyra_file_permission`, `enfyra_flow`, `enfyra_flow_execution`, `enfyra_flow_step`, `enfyra_folder`, `enfyra_graphql`, `enfyra_guard`, `enfyra_guard_rule`, `enfyra_menu`, `enfyra_method`, `enfyra_oauth_account`, `enfyra_oauth_config`, `enfyra_package`, `enfyra_post_hook`, `enfyra_pre_hook`, `enfyra_relation`, `enfyra_role`, `enfyra_route`, `enfyra_route_handler`, `enfyra_route_permission`, `enfyra_schema_migration`, `enfyra_setting`, `enfyra_storage_config`, `enfyra_table`, `enfyra_user`, `enfyra_websocket`, `enfyra_websocket_event`.',
228
- '- **Tables without REST routes (internal/system only):** `enfyra_column`, `enfyra_session`. Columns are managed indirectly via cascade on `enfyra_table` (POST/PATCH with columns arrays). The `create_table`, `create_column`/`add_column`, `update_column`, and `delete_column`/`remove_column` MCP tools handle this automatically by reading full table metadata first.',
229
- '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isUpdatable`, `isEncrypted`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isUpdatable=false` for immutable fields and set `isPublished=false` directly when creating secret/internal fields. `isEncrypted=true` encrypts stored values at rest but does not change `isUpdatable`; encrypted fields must not be used for filter/sort. When patching an existing table, only persisted columns with an `id`/`_id` belong in the cascade payload; metadata projections such as `createdAt`, `updatedAt`, or relation-derived FK display fields without an id are not valid column-definition patch rows. Never rebuild a schema cascade from `enfyra_table?fields=columns.*`, because nested relation fields may be paginated/truncated.',
230
- '- Prefer `create_relation`/`add_relation` and `delete_relation`/`remove_relation` for relation schema changes because they preserve the full table relation list, serialize schema mutations, verify unrelated relation ids survived, and handle schema-confirm retry. Direct `create_record` on `enfyra_relation` only edits metadata and is not the canonical schema migration path; generic record mutations also reject physical FK/junction fields on `enfyra_relation`.',
231
- '- Destructive schema tools and generic `delete_record` return a preview unless `confirm=true` is passed. This applies to `delete_record`, `delete_table`, `delete_column`/`remove_column`, and `delete_relation`/`remove_relation`; do not add `confirm=true` until the user has explicitly approved the destructive operation.',
232
- '',
233
- '### Body validation & column rules',
234
- '- Each `enfyra_table` has a **`validateBody`** flag (default `true` for new tables). When on, every `POST /<table>` and `PATCH /<table>/<id>` is validated server-side against the column types + any **column rules** attached to columns of that table.',
235
- '- Failure returns **HTTP 400** with `{ statusCode: 400, message: string[], error: "Bad Request" }`. `message` is an **array of strings** (one per violation, prefixed with the field name like `"email: Invalid email"`).',
236
- '- **`enfyra_column_rule`** stores per-column rules: fields are `column` (FK to `enfyra_column`), `ruleType` (one of `min`, `max`, `minLength`, `maxLength`, `pattern`, `format`, `minItems`, `maxItems`, `custom`), `value` (JSON payload e.g. `{v:10}` or `{v:"email"}` or `{v:"^[a-z]+$", flags:"i"}`), `message` (optional override), `isEnabled`. Has a default REST route at `/enfyra_column_rule` so MCP `create_record` / `update_record` / `delete_record` work — but the **canonical workflow is the admin UI** (Collections → table → column row → ruler icon → Manage Rules), which knows the per-`column.type` allowlist (e.g. `format` only for strings, `minItems` only for `array-select`) and prevents duplicate rule types per column.',
237
- '- **Rules are additive only** — they never replace the column\'s built-in type/nullable/length checks. There is no `required` rule type; required-ness comes from `column.isNullable = false`.',
238
- '- If the user wants to add a validation constraint to a field, the right answer is: open the column rules modal in the admin UI. **Do not** suggest writing pre-hooks for standard constraints (only for truly custom logic).',
239
- '- For `pattern` rules: the `value.v` is a JavaScript RegExp body (no surrounding `/.../`). Anchors matter — `^[a-z]+$` requires a full match, plain `[a-z]+` matches any substring. Flags go in `value.flags` (e.g. `"i"`).',
240
- '- Validation cache is invalidated **automatically** when a column rule is created/updated/deleted via MCP or UI — no manual `reload_*` call needed afterward. Same for flipping `validateBody` on `enfyra_table`.',
241
- '- To turn validation off for an entire table (e.g. legacy or test tables), either toggle **Validate Body** in the table form UI, or `update_record` on `enfyra_table` with `{ validateBody: false }`.',
242
- '',
243
- '### Schema / table migration (sequential only)',
244
- '- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
245
- '- Do NOT batch multiple schema changes in parallel (e.g. create 3 tables concurrently, or create a table and separately add columns at the same time). A single `create_table` call may include its own `columns` and `relations`; treat that as one schema operation. Execute each `create_table`, `create_column`, `create_relation`, sync, or drop sequentially; wait for completion before the next.',
246
- '',
247
- '### Resolving the real REST path',
248
- '- Do **not** assume `enfyra_route.path` always equals `enfyra_table.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
249
- '- When unsure of the URL path, use MCP **`get_all_routes`** (or **`get_all_metadata`**) to read each route’s **path**. Treat `mainTable` as canonical CRUD-route metadata only, not as the owner table for custom routes.',
250
- '',
251
- '### MongoDB vs SQL primary key',
252
- '- On **SQL**, filters often use **`id`**. On **MongoDB**, documents may use **`_id`** — a filter for one row might be `{"_id":{"_eq":"..."}}` instead of `id`, depending on metadata.',
253
- '- Use **`discover_runtime_context`** to read live `dbType` and `pkField` from metadata. If an older backend does not expose them, the tool falls back to primary-key inference and labels the source clearly.',
254
- '',
255
- '### Query DSL and deep relations',
256
- '- Use **`discover_query_capabilities`** before building non-trivial filters/deep queries. It returns supported filter operators, field-permission condition operators, deep shape/rules, table columns/relations, table primary key, route paths, and examples.',
257
- '- Full filter operators: `_eq`, `_neq`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_not_in`, `_nin`, `_contains`, `_starts_with`, `_ends_with`, `_between`, `_is_null`, `_is_not_null`, `_and`, `_or`, `_not`.',
258
- '- Field permission condition DSL is narrower and does not support `_contains`, `_starts_with`, `_ends_with`, or `_between`.',
259
- '- Root `sort` accepts local fields such as `-createdAt` plus direct list-relation aggregate helpers: `_count(relationName)`, `_max(relationName.fieldName)`, and `_min(relationName.fieldName)`. Use `-_max(messages.createdAt)` to order parent rows by the latest child row. The relation must be direct `one-to-many` or `many-to-many`, and the aggregate field must be a non-encrypted scalar field on the related table.',
260
- '- Raw dotted to-many sort such as `messages.createdAt` is invalid for parent ordering. `deep: { messages: { sort: "-createdAt" } }` sorts the loaded child rows inside each parent only; it does not sort the parent list.',
261
- '- Field selection has two modes per query scope. Include mode is the default: `fields=id,email,owner.name` returns only those fields. If any token starts with `-`, that scope switches to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode` because positive tokens are ignored in exclude mode. Dotted exclusions such as `fields=-owner.avatar` and nested deep fields such as `deep: { owner: { fields: "-avatar" } }` also switch that scope to exclude mode. Unknown excluded fields/relations are request errors, so inspect metadata before excluding guessed names.',
262
- '- Deep shape: `{ relationName: { fields?, filter?, sort?, limit?, page?, deep? } }`. Relation keys are relation `propertyName`, not physical FK columns.',
263
- '- Use dotted relation fields such as `owner.email` or `lastMessage.text` when the caller only needs basic related record fields. Use `deep` when relation loading needs query options such as `filter`, `sort`, `limit`, `page`, or nested `deep`. Do not use `deep` for simple relation-id filters, one-row lookup, counts, or large child collections that should be loaded separately with pagination.',
264
- '- Deep validation rejects unknown relation keys, unknown subkeys, `limit` on many-to-one/one-to-one, and invalid dotted sort through many-side relations.',
265
- '- To count records over REST, do not fetch full rows. Use MCP **`count_records`**, or call `GET /<table>?fields=id&limit=1&meta=totalCount` without filter and read `meta.totalCount`; with a filter use `meta=filterCount` and read `meta.filterCount`.',
266
- '- In custom dynamic code, use the same lightweight pattern: `const result = await @REPOS.main.find({ fields: "id", limit: 1, meta: filter ? "filterCount" : "totalCount", ...(filter ? { filter } : {}) }); const count = filter ? result.meta?.filterCount : result.meta?.totalCount;`.',
267
- '',
268
- '### GraphQL (same prefix as REST / ENFYRA_API_URL)',
269
- `- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With the required app proxy base: e.g. \`http://localhost:3000/api/graphql\`.`,
270
- `- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
271
- '- A table appears in the schema when `enfyra_graphql` has an enabled row for that table. The REST route `availableMethods` list does not enable GraphQL.',
272
- '- **Query** field = same string as `enfyra_table.name`. **Mutations** are literal concat: `create_`+tableName, `update_`+tableName, `delete_`+tableName (e.g. tableName `post` → `create_post`, input type `postInput`). See `generate-type-defs.ts`. If every column is skipped for input (only PK, or only `createdAt`/`updatedAt`, or all unpublished), the schema emits **no** `Query.<tableName>` and **no** create/update/delete mutations for that table (an output `type` may still exist for relation wiring).',
273
- '- **Auth:** GraphQL currently requires `Authorization: Bearer <accessToken>`. REST route `publicMethods` does not make GraphQL anonymous.',
274
- '- **Management workflow:** use `update_table` with `graphqlEnabled: true|false`, or create/update `enfyra_graphql` with `table: {id}` and `isEnabled`. Reload GraphQL with `reload_graphql` if the cache has not refreshed yet.',
275
- '- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
276
- '',
277
- '### WebSocket (Socket.IO)',
278
- '- Enfyra uses **Socket.IO**. Gateways and events are stored in **`enfyra_websocket`** and **`enfyra_websocket_event`**; manage via REST (MCP `create_record`, `update_record`, `query_table` on those tables).',
279
- '- **Gateway** (`enfyra_websocket`): `path` = namespace (e.g. `/chat`), `requireAuth` (JWT in `auth.token`), `connectionHandlerScript` (runs on connect), `connectionHandlerTimeout`, `isEnabled`.',
280
- '- **Event** (`enfyra_websocket_event`): `gateway` → gateway id, `eventName` (client emits), `handlerScript`, `timeout`, `isEnabled`.',
281
- '- **@SOCKET in scripts (prefer template `@SOCKET.method()` over `$ctx.$socket.method()`):**',
282
- '- `@SOCKET.reply(event, data)` — send to this client only (WS context only).',
283
- '- `@SOCKET.join(room)` — join a room (WS context only).',
284
- '- `@SOCKET.leave(room)` — leave a room (WS context only).',
285
- '- `@SOCKET.emitToUser(userId, event, data)` — send to a specific user (across all gateways).',
286
- '- `@SOCKET.emitToRoom(path, room, event, data)` — send to a named room in a specific gateway/namespace.',
287
- '- `@SOCKET.emitToCurrentRoom(room, event, data)` — send to a named room in the current websocket gateway/namespace (WS context only).',
288
- '- `@SOCKET.broadcastToRoom(room, event, data)` — send to a named room in the current websocket gateway/namespace except the triggering socket (WS context only).',
289
- '- `@SOCKET.emitToGateway(path, event, data)` — broadcast to all connections on a gateway/namespace.',
290
- '- `@SOCKET.broadcast(event, data)` — broadcast to all connections on all gateways.',
291
- '- `await @SOCKET.roomSize(room)` — count connected sockets in a room across registered gateways; available in server script socket context.',
292
- '- `@SOCKET.disconnect()` — force-disconnect the current socket from the gateway (WS context only). Use in connection handler to reject, or in event handler to kick user.',
293
- '- In **HTTP** handler/hook context: `reply`, `join`, `leave`, `disconnect`, `emitToCurrentRoom`, and `broadcastToRoom` are not available (no bound socket). Use `emitToUser`, `emitToRoom(path, room, event, data)`, `emitToGateway`, `broadcast`, and `roomSize`.',
294
- '- **Context**: Connection — `@BODY` = {id, ip, headers}, `@USER` if auth. Event — `@BODY` = payload, `@USER` if auth. Both have `@SOCKET`.',
295
- '- **ACK + results (recommended UX):** client can emit an event with Socket.IO ack callback. Server immediately acks `{ queued: true, requestId, eventName }` (or `{ queued: false, error }`). The handler result is returned asynchronously via `ws:result` or `ws:error` with the same `requestId`.',
296
- '- **Client**: Browser apps must connect through the app/Nuxt Socket.IO bridge, not the hidden Enfyra backend. The backend gateway metadata path is the namespace, e.g. `/chat`. A third app should connect with `io("/chat", { path: "/socket.io", withCredentials: true })` and proxy `/socket.io/**` to the Enfyra app bridge `/ws/socket.io/**`. Direct Enfyra app clients may connect with `io("/ws/chat", { path: "/ws/socket.io", withCredentials: true })` when using the built-in bridge convention.',
297
- '- **Workflow**: Create gateway → `create_record` on `enfyra_websocket`. Create event → `create_record` on `enfyra_websocket_event` with `gateway: {id}`. Changes auto-reload; test handlers before saving.',
298
- '- **Test WS handler (recommended):** `POST {base}/admin/test/run` with `{ kind:"websocket_event", gatewayPath, eventName, timeoutMs, payload, script }` to run a websocket event script without a real client. Returns `{ success, result, logs, emitted }`.',
299
- '- MCP wrapper: use **`run_admin_test`** with `kind:"websocket_event"` or `kind:"websocket_connection"` instead of hand-building the HTTP call.',
300
- '- Before writing websocket scripts, call **`discover_script_contexts`** to confirm which `@SOCKET` methods are bound in websocket vs HTTP/flow contexts.',
301
- '',
302
- '### Flows (Automated Workflows)',
303
- '- Enfyra supports automated workflows via **`enfyra_flow`**, **`enfyra_flow_step`**, and **`enfyra_flow_execution`** tables.',
304
- '- **Flow** (`enfyra_flow`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
305
- '- **Step** (`enfyra_flow_step`): `flow` → flow id, `key` (unique identifier for data chain), `stepOrder`, `type` (`script`, `condition`, `query`, `create`, `update`, `delete`, `http`, `trigger_flow`, `sleep`, `log`), `config` (JSON), `timeout`, `onError` (`stop`, `skip`, `retry`), `retryAttempts`, `parent` → self-ref to condition step (null = root), `branch` (`true`/`false` — which branch of parent condition).',
306
- '- **Execution history** (`enfyra_flow_execution`): `flow` → flow id, `status`, `payload`, `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. There is no persisted `context` column; failed executions store diagnostics in `error`. Query separately — NOT nested under enfyra_flow.',
307
- '- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
308
- '- **Step config examples**: script: `{"code":"return #enfyra_user.find({limit:10})"}`, condition: `{"code":"return @FLOW_LAST?.data?.length > 0"}` (uses JS truthy/falsy: `return user` = truthy if exists, `return null` = falsy), query: `{"table":"enfyra_user","filter":{"status":{"_eq":"active"}},"limit":10}`, http: `{"url":"https://api.example.com","method":"POST","body":{}}` (auto Content-Type: application/json; **http `url` must be public-safe**—see Safety), sleep: `{"ms":5000}`, trigger_flow: `{"flowId":2}`.',
309
- '- **Flow step sizing:** prefer many small, named steps over one large script. Split workflows at real operation boundaries such as load context, reserve capacity, create remote resource, apply DB/user guardrails, start container, check health, write final state, and send notifications. A script step that mixes unrelated SSH/Docker/DB/API/email/reconciliation work, returns huge objects, or grows beyond roughly 100-150 lines should be split before saving.',
310
- '- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
311
- '- **Flow step output discipline:** Script/condition steps must return only the small values that later steps genuinely need, such as ids, booleans, status keys, or counters. Do not return full records, host objects, package objects, DB URLs, SSH keys, API tokens, generated passwords, or other secrets. Later steps should re-query the records they need from ids/payload. Flow execution history already records errors; successful runs do not need full success snapshots.',
312
- '- **Flow refactor rule:** when changing an existing oversized flow step, prefer adding or extracting adjacent focused `enfyra_flow_step` rows with clear keys and `stepOrder` instead of making the original `sourceCode` longer. Keep branch `parent`/`branch` relationships explicit, and re-read saved steps after mutation to verify order and executable source.',
313
- '- **Template syntax (flows)**: `@FLOW_PAYLOAD` → `$ctx.$flow.$payload` (input data), `@FLOW_LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@FLOW_META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@THROW400`–`@THROW503` / `@THROW` → `$ctx.$throw[...]`. Trigger other flows in handlers via `@TRIGGER(name, payload)` or `$ctx.$trigger(name, payload)`.',
314
- '- **Condition branching**: Condition step uses JavaScript truthy/falsy evaluation (e.g. `return user` → truthy if exists, falsy if null/0/undefined). Children with matching `parent: {id: conditionStepId}` and `branch: "true"/"false"` execute. Root steps (no parent) always execute sequentially.',
315
- '- **Safety**: Max nesting depth 10 (flow triggering flow). Circular flow detection prevents A→B→A loops. HTTP steps: **SSRF hardening** — only `http`/`https`; blocks `localhost`, private IPs, and hostnames resolving to private IPs (use internet-facing URLs like `https://api.example.com`, not internal services, unless server policy changes). Default HTTP timeout 30s (AbortController). `$trigger()` available inside flow steps.',
316
- '- **Workflow**: Create flow → `create_record` on `enfyra_flow`. Add steps → `create_record` on `enfyra_flow_step` with `flow: {id}`. For branch steps, set `parent: {id: conditionStepId}` and `branch: "true"` or `"false"`. Trigger manually via `POST /admin/flow/trigger/{flowId}`.',
317
- '- **Flow source sanity:** after creating or patching a multi-step flow, refetch saved `enfyra_flow_step` rows and verify every script/condition step has executable step-specific `sourceCode` with a body/return. A helper-only source block that parses successfully is still broken and must be patched before the user sees it in the Enfyra admin UI.',
318
- '- **Test step**: `POST /admin/test/run` with body `{kind:"flow_step", type, config, timeout}` — runs a single step without saving, returns `{success, result, error, duration}`.',
319
- '- MCP wrappers: use **`test_flow_step`** for one flow step, **`run_admin_test`** for flow/websocket tests, and **`trigger_flow`** for saved flows.',
320
- '- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
321
- '- Before writing flow scripts, call **`discover_script_contexts`** to confirm `@FLOW`, `@FLOW_PAYLOAD`, `@FLOW_LAST`, `#table_name`, `$ctx.$trigger`, and `$socket` behavior.',
322
- '',
323
- '### Extension (Vue SFC only — NOT React)',
324
- '- **CRITICAL:** MUST call `create_record` or `update_record` on `enfyra_extension` — outputting Vue code in chat does NOT save it. User will NOT see it.',
325
- '- **Code format:** Vue SFC only. Structure: `<template>...</template>` + `<script setup>...</script>`. Server auto-compiles; if compile fails, fix and retry.',
326
- '- **NO import statements.** All APIs are injected globally (see full list below).',
327
- '- **Design first for dashboards:** before creating/updating a dashboard extension, define the menu/page split, time range controls, tabs, and drill-down links. Keep `/dashboard` as a compact summary and routing hub only: KPIs, current signal, attention queue, and navigation cards. Put detailed lists/tables/timelines into focused pages such as jobs, orders, reports, integrations, and settings.',
328
- '- **Operational page modeling:** model admin operational pages around the operator mental entity, not raw database/event rows. For long-running jobs, group history by entity/run, translate internal step keys into readable labels, show current step, operator meaning, and next action, and keep raw history as a secondary `/data` shortcut.',
329
- '- **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.',
330
- '- **Enfyra 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`.',
331
- '- **Aggregate numeric rule:** `sum` and `avg` require a numeric field in Enfyra Server. 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.',
332
- '- **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.',
333
- '- **Partial reload default:** Enfyra Server 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.',
334
- '- **Menu/extension realtime reload contract:** `enfyra_menu` and `enfyra_extension` 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 the Enfyra admin UI handles; Enfyra admin UI must refetch menus/rebuild the menu registry for menu reloads, invalidate dynamic extension caches for extension reloads, and reload enabled `type="global"` shell extensions when extension/menu metadata changes. 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.',
335
- '- **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.',
336
- '- **Page layout default:** page extensions should render full-bleed inside the app shell by default. The extension root is already inside the Enfyra admin UI page `<main>`, so do not add root-level page padding such as `p-4 sm:p-6 xl:p-8`; use spacing between internal sections only. Do not wrap the entire page in a centered card/container unless explicitly requested. Use responsive grids/stacks from the first version so the page works on desktop, tablet, and mobile.',
337
- '- **PageHeader is mandatory for page extensions:** The Enfyra admin shell already renders `CommonPageHeader` from `usePageHeaderRegistry()` in the app shell. Page extensions must call `const { registerPageHeader } = usePageHeaderRegistry()` and register app-level context such as `{ title, description, leadingIcon, gradient, variant }` instead of rendering their own top `<header>` inside extension content. Use `variant: "minimal"` for operational/admin detail pages unless the page intentionally needs a larger title strip.',
338
- '- **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.',
339
- '- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Destructure `register` first, then call it with one action or an array, for example `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id: "create", label: "Create report", permission: { and: [{ route: "/report", methods: ["POST"] }] }, onClick }])`. Sensitive registry actions must include a `permission` condition.',
340
- '- **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.',
341
- '- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `enfyra_method`. The backend field is `enfyra_method.name`, unique per method; do not send a `method` field on `enfyra_method`. The Enfyra admin UI 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 `enfyra_method` unless the dedicated tool is unavailable.',
342
- '- **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.',
343
- '- **Extension runtime scope:** Enfyra admin UI 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")`, because Vue compiles template helpers to `_ctx.*`.',
344
- '- **Global extension lifecycle:** `enfyra_extension.type="global"` records are Vue SFC components mounted invisibly once at the Enfyra admin UI shell level. Use them for app-wide registrations such as account panel items, notification bells, admin socket listeners, and background refresh bridges. They do not need a menu, must not render page body UI, and should clean up socket/listener side effects with `onUnmounted`. Prefer registry composables such as `useAccountPanelRegistry`, `useHeaderActionRegistry`, or `useSubHeaderActionRegistry` instead of directly editing shell DOM. For account-panel entries, register data (`label`, `icon`, `description`, `badge`, `badgeColor`, `expanded`, `onToggle`, `contentComponent`) so Enfyra admin UI owns row spacing, icon sizing, badges, and expanded chrome; use a raw row `component` only for a true custom escape hatch.',
345
- '- **Extension realtime:** admin extensions can use `useAdminSocket()` to listen to the shared admin Socket.IO client instead of creating a separate browser socket. Use it for backend admin events such as operational status changes, debounce refreshes, and unsubscribe with `socket.off(...)` in `onUnmounted`. Guard generated code with `typeof useAdminSocket === "function"` when the extension may run on an older Enfyra admin UI build.',
346
- '- **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.',
347
- '- **Admin record links:** when an admin extension links to backend records for management or inspection, point to Enfyra admin UI data routes such as `/data/report` or `/data/order`. Do not use public website paths from record fields unless the explicit intent is previewing the public website.',
348
- '- **Admin menu visibility is permission-driven, not RLS:** admin menu entries are sensitive and must set `enfyra_menu.permission` so they are visible only to users who have at least GET permission for the backing route or table. Permission conditions use HTTP `methods`, not CRUD `actions`. Do not show an admin menu merely because an extension exists or because the path is hardcoded. Example: `/reports` menu can require `{ or: [{ route: "/reports", methods: ["GET"] }, { route: "/report", methods: ["GET"] }] }`.',
349
- '- **PermissionGate is mandatory inside admin extensions:** every sensitive action button, form, mutation, destructive workflow, and data shortcut must be wrapped in `PermissionGate` or guarded with `usePermissions()` before rendering/enabling. Default gates: list/detail visibility needs `methods: ["GET"]`; create and custom flow-trigger routes usually need `methods: ["POST"]`; native record edits need `methods: ["PATCH"]`; native delete routes need `methods: ["DELETE"]`. Root admin still passes through normal permission helpers, but extension code must not rely on root-only assumptions.',
350
- '- **Extension permission UX:** if the current user can read a page but cannot perform an action, hide the action by default. If hiding would confuse the workflow, render a disabled state with a short reason. Never let the button render active and depend only on the server rejection; server permissions are the final boundary, not the UI contract.',
351
- '- **Flow schedule UI:** schedule trigger editors must keep the server contract as `triggerConfig.cron` and `triggerConfig.timezone`, but the UI should not be a bare cron field plus giant timezone dropdown. Provide common cadence presets, readable current-schedule summary, searchable access to all IANA timezones, suggested timezone shortcuts, and a custom cron escape hatch so operators can configure recurring checks without remembering cron syntax.',
352
- '- **Admin operation UI:** use Enfyra admin `CommonModal` for compact create, disable, delete, and multi-field confirmation workflows. Use `CommonDrawer` for longer setup workflows such as multi-step create/edit forms and provider/package selection. Open the modal/drawer immediately on click, then render loading/error/content inside it; do not wait for async fetches to finish before showing the shell.',
353
- '- **FormEditor is preferred for table-record forms:** when an extension creates or edits a concrete table record such as `report`, `order`, or another table-backed entity, use `FormEditor`/`FormEditorLazy` inside the modal/page when the form is a direct table edit. Customize layout with `sections`, `includes`, and `field-map`; reserve custom inputs only for workflow-specific fields that are not table columns.',
354
- '- **Modal form layout:** inside `CommonModal`, stack each form control vertically with label text above a full-width input/control. Use a small grid/space stack such as `grid gap-4`, `p.text-sm.font-medium`, then `UInput class="mt-2 w-full"`. Do not place modal labels and inputs side by side unless the user explicitly asks for a dense horizontal form.',
355
- '- **Confirmation modal flow:** destructive/admin confirmation modals must read top-to-bottom as the operator workflow. For server-hash confirmations, render: id input first, then a full-width `Request hash` button, then a disabled hash input that is auto-filled by the server response, then the final destructive action in the footer. The final action stays disabled until the typed id matches and the server hash has been requested. Do not ask operators to manually type or edit the hash.',
356
- '- **Do not downgrade extension code to ES5 to appease tooling.** Enfyra admin extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix Enfyra admin extension checker/runtime contract instead of rewriting generated extension code around the limitation. Vue extension diagnostics should only TypeScript-check `<script setup lang="ts">`; plain `<script setup>` extension code is JavaScript and must not emit TypeScript diagnostics into the form.',
357
- '',
358
- '#### Injected Vue API functions:',
359
- '- Reactivity: `ref`, `reactive`, `computed`, `readonly`, `shallowRef`, `shallowReactive`',
360
- '- Lifecycle: `onMounted`, `onUnmounted`, `onBeforeMount`, `onBeforeUnmount`, `onUpdated`, `onBeforeUpdate`',
361
- '- Watch: `watch`, `watchEffect`',
362
- '- Component: `defineProps`, `defineEmits`, `defineExpose`, `resolveComponent`, `h`, `defineComponent`',
363
- '- Utils: `nextTick`, `toRef`, `toRefs`, `unref`, `isRef`, `isProxy`, `isReactive`, `isReadonly`, `toRaw`, `markRaw`, `effectScope`, `getCurrentScope`, `onScopeDispose`',
364
- '',
365
- '#### Injected Nuxt composables:',
366
- '- Router: `useRoute`, `useRouter`, `navigateTo`',
367
- '- State: `useState`, `useCookie`, `useNuxtApp`, `useRuntimeConfig`',
368
- '- Data: `useFetch`, `useAsyncData`, `useLazyFetch`',
369
- '- SEO: `useHead`, `useSeoMeta`',
370
- '- UI: `useToast`',
371
- '',
372
- '#### Injected Enfyra composables:',
373
- '- **useApi:** API client with auto error handling & toast. Returns `{ data, error, pending, status, execute, refresh }`. Does NOT auto-run — must call `execute()`. Example: `const { data, execute } = useApi(\'/enfyra_user\', { query: { limit: 10 } }); onMounted(() => execute());`',
374
- '- **useAuth:** Authentication. Returns `{ me, login, logout, fetchUser, isLoggedIn, isLoading, oauthLogin }`. `me` is reactive user object with `isRootAdmin`, `role`, `allowedRoutePermissions`.',
375
- '- **usePermissions:** Permission checks. Returns `{ hasPermission(route, method), hasAnyPermission(routes, methods), hasAllPermissions(routes, methods), checkPermissionCondition(condition) }`. Permission conditions use HTTP method names directly, for example `{ or: [{ route: "/posts", methods: ["GET"] }] }`.',
376
- '- **useSchema:** Schema management. Returns `{ schemas, schema, fetchSchema, schemaLoading, definition, fieldMap, getField(key), editableFields, generateEmptyForm(), validate(record), getIncludeFields(), useFormChanges() }`.',
377
- '- **useGlobalState:** Global app state. Returns `{ settings, storageConfigs, aiConfigs, appPackages, sidebarVisible, sidebarCollapsed, routeLoading, toggleSidebar(), setRouteLoading(), fetchAppPackages(), packageCacheState }`.',
378
- '- **useScreen:** Responsive helpers. Returns `{ width, height, isMobile, isTablet, isDesktop, isLargeDesktop, screenType }`.',
379
- '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
380
- '- **useHeaderActionRegistry:** Register header actions. Use `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id, label, onClick, color, icon, order, side, global, permission }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component, permission }`; admin actions should set `permission` by default.',
381
- '- **useSubHeaderActionRegistry:** Same as header but for sub-header: destructure `register` first, then call it with one action or an array.',
382
- '- **useAccountPanelRegistry:** Register rows in the sidebar account panel. Destructure `const { register } = useAccountPanelRegistry()` and call `register(itemOrItems)`. Prefer data-driven items: `{ id, order, label, icon, description, badge, badgeColor, trailingIcon, expanded, onToggle, contentComponent, contentProps }`. Enfyra admin UI renders the row chrome and lifecycle-unregisters the caller automatically. Use `contentComponent` only for expanded inner content, and use raw `component` only when the entire row truly cannot use the shell contract.',
383
- '- **usePageHeaderRegistry:** Page title strip. `{ registerPageHeader, clearPageHeader, pageHeader, hasPageHeader }`. Config: `title`, optional `description`, `stats`, `variant`, `gradient` (`purple`|`blue`|`cyan`|`none` — horizontal strip + leading icon tint), `leadingIcon` (icon name), `hideLeadingIcon`. Call `registerPageHeader` again when title/stats must update (plain object snapshot, not refs inside the config).',
384
- '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
385
- '- **useMenuApi:** Low-level menu API.',
386
- '- **useAdminSocket:** Shared admin Socket.IO client. Returns `{ adminSocket, activeReloads, isReloading, showReloadBanner, runtimeMetricsByInstance, runtimeMetricsUpdatedAt, redisAdminOverview, redisAdminOverviewUpdatedAt, redisAdminKeyChange, loadRedisOverview, loadRedisKeys, loadRedisKey }`. Listen with `adminSocket.on(event, handler)` and remove listeners in `onUnmounted` with `adminSocket.off(event, handler)`.',
387
- '- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
388
- '- **useDatabase:** Database helpers (returns `{ getId, getIdFieldName }`).',
389
- '- **useRoutes:** Route management.',
390
- '- **useHighlight:** Code highlighting.',
391
- '- **useMounted:** Mount state helper.',
392
- '',
393
- '#### Injected UI Components (auto-resolved):',
394
- '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `CommonModal`, `CommonDrawer`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
395
- '- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
396
- '- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
397
- '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
398
- '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
399
- '- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
400
- '- **Tabs:** `UTabs` is available in current Enfyra admin extension runtime. Use it for page-level sections when a page would otherwise become too long.',
401
- '- **Extension:** `Widget` — embed widget extension records by numeric database id, e.g. `<Widget :id="123" />`',
402
- '- **WebSocket:** `WebSocketManager`',
403
- '- **Permission:** `PermissionGate`, `PermissionManager`',
404
- '',
405
- '#### Global objects:',
406
- '- `fetch`, `console`, `window`, `document`',
407
- '- `$ctx` — runtime context',
408
- '',
409
- '#### Extension types:',
410
- '- **FormEditor field-map:** Customize fields via `:field-map`. Options: `label`, `description`, `hideLabel`, `hideDescription`, `component`, `componentProps`, `type`, `disabled`, `placeholder`, `permission`, `excludedOptions`/`includedOptions`, `fieldProps` (e.g. grid `class: \'md:col-span-2\'` when `layout=\'grid\'`), `booleanWrapperClass`, `fieldWrapperClass`. Optional `:sections` — array of `{ id, title?, hideHeading?, headingClass?, class?, rootClass?, fields: string[] }`; field order follows `fields`; unlisted columns render after. Custom input component: `modelValue` / `update:modelValue`.',
411
- '- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `enfyra_menu`), find by path/label, then create extension with `menu: { id: menuId }`. `enfyra_menu` uses **label** not name — filter by `label` or `path`. Sensitive/admin menus should pass a `permission` JSON object string to `create_menu` so visibility is permission-gated from creation.',
412
- '- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
413
- '- **type "global":** Global shell extension. No menu required and not embedded manually; the Enfyra admin UI fetches all enabled global extensions during layout init and mounts them invisibly with normal Vue lifecycle. Use for global notification/account-panel/realtime registration code, not for route content. Keep `<template></template>` empty or minimal hidden markup, and keep visible UI in registered components or existing shell slots.',
414
- '- **Widget composition:** split large page extensions into widget extensions for bulky or reusable sections such as operation panels, status cards, timelines, tables, and sidebars. Keep tiny one-off markup in the page. Create/update the widget `enfyra_extension` rows first, then embed their ids from the page extension.',
415
- '- **Widget props/events:** the `Widget` wrapper forwards non-`id` attrs and listeners to the rendered widget component. Widget extensions can declare `defineProps` and `defineEmits`; parent prop changes are reactive like normal Vue component props. Use normal Vue syntax such as `<Widget :id="123" :project-id="projectId" :history-rows="historyRows" @refresh="refreshAll" />`; kebab-case attributes map to camelCase props.',
416
- '- **Widget state boundary:** do not mutate props inside widgets. Derive display state with `computed`, or mirror a prop into local mutable state with `watch(() => props.value, ...)` when the widget owns an editor draft. Prefer emitted events for child-to-parent actions; use callback function props only for parent-owned modal/drawer openers or imperative actions, and guard with `typeof action === "function"` inside the widget.',
417
- '- **Widget ownership:** keep route parsing, page-level API loading/mutation state, and modal submit flows in the page extension unless a widget intentionally owns the whole workflow. Use widgets for focused render sections or operation panels that receive safe data/actions from the page.',
418
- '- **Widget permissions:** sensitive action controls inside widgets must still use `PermissionGate` or `usePermissions()` and must keep `type="button"` plus `@click.stop.prevent` for modal/drawer triggers. Server routes remain the final permission boundary.',
419
- '- **Widget loading performance:** Enfyra admin UI batches widget metadata fetches requested in the same tick and caches loaded widgets. Do not manually fetch widget code from page extensions; render `<Widget>` components together so the runtime can batch-load them.',
420
- '- **Existing pages:** if a menu already has a page extension, update that `enfyra_extension` record instead of creating a duplicate menu/extension. For example `/dashboard` is menu-driven and may already have an extension attached.',
421
- '',
422
- '#### NPM packages (install via MCP):',
423
- '- **Use `install_package` tool** — just pass the package name and type. The tool auto-fetches version from NPM, checks if already installed, and creates the record.',
424
- '- Example: `install_package({ name: "node-ssh", type: "Server" })` — that is all. Tool handles everything.',
425
- '- **Search first with `search_npm`** if unsure of exact package name.',
426
- '- **Server** packages → available as `$ctx.$pkgs.packageName` in handlers/hooks.',
427
- '- **App** packages → available via `getPackages([\'dayjs\'])` in extensions (call in `onMounted`).',
428
- '- **Extension package imports:** Do not write static imports like `import { CalendarDate } from "@internationalized/date"` inside `enfyra_extension.code`; the extension builder does not resolve app packages that way. Install the package as type `App`, then load it inside the extension with `const pkgs = await getPackages(["@internationalized/date"]); const { CalendarDate } = pkgs["@internationalized/date"];`.',
429
- '- **Do NOT use `create_record` on `enfyra_package` directly** — use `install_package` instead.',
430
- '',
431
- '#### Important patterns:',
432
- '- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
433
- '- **Header actions:** `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id: \'back\', label: \'Hosts\', icon: \'lucide:arrow-left\', color: \'neutral\', variant: \'ghost\', order: 0, onClick: goBack }, { id: \'refresh\', label: \'Refresh\', icon: \'lucide:refresh-cw\', color: \'primary\', variant: \'solid\', order: 1, onClick: refresh }])`',
434
- '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
435
- '- **Permissions:** Use `checkPermissionCondition({ or: [{ route: \'/posts\', methods: [\'GET\'] }] })` for complex rules. In templates, wrap sensitive controls with `<PermissionGate :condition="{ and: [{ route: \'/admin/action\', methods: [\'POST\'] }] }">...</PermissionGate>` instead of only disabling them visually.',
436
- '- **After menu/extension create/update:** open Enfyra admin tabs should update through the `$system:reload` contract. Do not tell the user to press F5 unless you have verified the natural reload event failed or the server/Enfyra admin UI version does not support menu/extension reload yet.',
437
- '',
438
- '#### Minimal example:',
439
- '`<template><div class="p-6"><h1 class="text-2xl font-bold">{{ title }}</h1><UButton @click="handleClick">Click</UButton></div></template><script setup>const title = ref(\'My Extension\'); const toast = useToast(); const handleClick = () => toast.add({ title: \'Clicked\', color: \'green\' });</script>`',
440
- '',
441
- '### API Testing',
442
- '- API testing is available at `/settings/api-tester` in the app UI.',
443
- '',
444
- '### MCP tool → HTTP',
445
- '- **Routes:** `create_route` / `create_handler` / `create_pre_hook` / `create_post_hook` persist to `enfyra_route`, `enfyra_route_handler`, etc. (REST CRUD on those tables). Prefer **`create_route`** for new paths — not `create_table`.',
446
- `- \`get_all_metadata\` → GET \`${base}/metadata\``,
447
- `- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
448
- `- \`discover_runtime_context\` → GET metadata/routes/method/runtime-backed tables and infer live primary key/backend context`,
449
- `- \`discover_query_capabilities\` → GET metadata/routes and summarize Query DSL/deep/table-specific query contracts`,
450
- `- \`discover_script_contexts\` → static runtime macro/context map for handlers/hooks/flows/websocket/GraphQL/extensions`,
451
- `- \`trace_metadata_usage\` → scans live metadata and script-backed records for a table/route/keyword before editing`,
452
- `- \`get_script_source\` → GET one script-backed record via filter+limit=1 and returns full sourceCode plus sha256`,
453
- `- \`patch_script_source\` → preview or apply exact sourceCode search/replace; apply validates with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
454
- `- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args, including filter/sort/page/limit/fields plus optional meta/deep/aggregate)`,
455
- `- \`count_records\` → GET \`${base}/<tableName>?fields=id&limit=1&meta=totalCount|filterCount\``,
456
- `- \`find_one_record\` (by id) → GET \`${base}/<tableName>?filter=…&limit=1\``,
457
- `- \`create_record\` → POST \`${base}/<tableName>\` (optional tool queryParams append URL query)`,
458
- `- \`update_record\` → PATCH \`${base}/<tableName>/<id>\` (optional tool queryParams append URL query)`,
459
- `- \`update_script_source\` → validates sourceCode with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
460
- `- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\` after preview + confirm=true (optional tool queryParams append URL query)`,
461
- `- \`create_extension\` → POST \`${base}/enfyra_extension\` (Vue SFC only; for \`type="page"\` pass menuId, for \`type="widget"\` or \`type="global"\` omit menuId). \`update_record\` on enfyra_extension to change code.`,
462
- `- Flow tables: \`${base}/enfyra_flow\`, \`${base}/enfyra_flow_step\`, \`${base}/enfyra_flow_execution\` — use standard CRUD tools.`,
463
- `- \`run_admin_test\` → POST \`${base}/admin/test/run\``,
464
- `- \`test_flow_step\` → POST \`${base}/admin/test/run\` with \`kind:"flow_step"\``,
465
- `- \`trigger_flow\` → POST \`${base}/admin/flow/trigger/<flowIdOrName>\``,
466
- `- Other: \`${base}/enfyra_menu\`, \`${base}/enfyra_websocket\`, \`${base}/admin/reload\`, etc.`,
467
- '',
468
- 'When asked which endpoint the API calls, respond with **HTTP method + full URL** using this base. Call `get_enfyra_api_context` to confirm the resolved base if needed.',
21
+ '## Enfyra MCP',
22
+ '',
23
+ `API base for this session: \`${base}\`.`,
24
+ `GraphQL endpoints: \`${graphqlHttpUrl}\` and \`${graphqlSchemaUrl}\`.`,
25
+ '',
26
+ '### Work Flow',
27
+ '- Discover before deciding. For architecture/capability questions call `discover_enfyra_system`; for DB/pk/runtime/cache context call `discover_runtime_context`; for filters/deep/sort/relation query shape call `discover_query_capabilities`. Run broad discovery tools sequentially, not in parallel.',
28
+ '- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
29
+ '- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category.',
30
+ '- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
31
+ '- For existing script-backed records, use `trace_metadata_usage` then `get_script_source`; edit with `patch_script_source` or `update_script_source` so source is hash-checked and validated.',
32
+ '- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
33
+ '',
34
+ '### Core Contracts',
35
+ '- `query_table` and `get_all_routes` require explicit intent: pass `limit` for bounded reads or `all: true` for a complete list. Do not invent arbitrary limits such as 30 or 50.',
36
+ '- Read tools are minimal by default. Pass explicit `fields`; use metadata inspection before guessing field/relation names. Field exclusion mode exists: `fields=-compiledCode`, and `fields=id,-compiledCode` still means all readable fields except `compiledCode`.',
37
+ '- Mutations return ids/status by default. Re-read with `find_one_record` or `query_table` and explicit `fields` when the saved row matters.',
38
+ '- Use `enfyra_user` as the user table. Model record links as real relations using relation `propertyName` values, not physical FK fields like `userId`, `conversationId`, `senderId`, or `memberId` in generated DB code.',
39
+ '- Do not call internal/no-route system tables such as `enfyra_column` or `enfyra_session` through generic CRUD. Use table/column/relation tools and route-backed tables discovered from metadata.',
40
+ '- Custom API paths use `create_route` without `mainTableId`; `create_table` is only for new persisted data.',
41
+ '- For canonical table reads and RLS, preserve client-controlled query shape: do not override `@QUERY.fields`, `@QUERY.deep`, `@QUERY.sort`, `@QUERY.limit`, `@QUERY.page`, `@QUERY.meta`, `@QUERY.aggregate`, or `debugMode`. Merge only security filters into `@QUERY.filter`.',
42
+ '- Script source is `sourceCode`; `compiledCode` is generated and may differ textually because macros expand. Do not warn about source/compiled mismatch unless validation or runtime behavior proves the compiled artifact is stale.',
43
+ '- For intentional user/domain errors in scripts use `@THROW400`-style helpers or `$ctx.$throw[...]`, not `throw new Error(...)`.',
44
+ '- Destructive operations are preview-first. Do not pass `confirm=true` until the user explicitly approves.',
45
+ '- Operator posture: act from these contracts plus live metadata. Do not turn expected implementation details into speculative warnings; ask only for new product/design decisions or genuine ambiguity.',
46
+ '',
47
+ '### App Connection Defaults',
48
+ '- Generated Nuxt/Next/SSR apps should use a same-origin proxy such as `/enfyra/**` to the Enfyra API. Browser code calls `/enfyra/login`, `/enfyra/me`, `/enfyra/logout`, and `/enfyra/<table>`; it should not store JWTs.',
49
+ '- OAuth starts through the same proxy prefix with `redirect=<absoluteReturnUrl>` and `cookieBridgePrefix=/enfyra`. OAuth setup details live in `get_enfyra_examples({ category: "ssr-app-auth" })`.',
50
+ '- Socket.IO browser clients connect to the gateway namespace, e.g. `io("/chat", { path: "/socket.io", withCredentials: true })`, while the app proxies `/socket.io/**` to Enfyra `/ws/socket.io/**`.',
51
+ '',
52
+ '### Dynamic Script Surface',
53
+ '- Prefer macros when available: `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REQ`, `@RES`, `@REPOS`, `@CACHE`, `@HELPERS`, `@FETCH`, `@STORAGE`, `@UPLOADED_FILE`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, `@STATUS`, `@ENV`, `@PKGS`, `@LOGS`, `@SHARE`, `@API`, `@THROW*`, `@FLOW*`, and `#table_name`. Call `discover_script_contexts` for exact per-surface availability.',
54
+ '- `@SOCKET.roomSize(room)` is available in the server socket helper. Bound websocket contexts also have `reply`, `join`, `leave`, `disconnect`, `emitToCurrentRoom`, and `broadcastToRoom`; HTTP/flow contexts only have global emit helpers plus `roomSize`.',
55
+ '',
56
+ '### Direct HTTP Mapping',
57
+ '- Route-backed table CRUD is REST: `GET /<table>?...`, `POST /<table>`, `PATCH /<table>/<id>`, `DELETE /<table>/<id>`. There is no `GET /<table>/<id>`; use a filtered list with `limit=1` or `find_one_record`.',
58
+ '- REST public access is controlled by route `publicMethods`; otherwise direct HTTP needs Bearer JWT plus route permissions. GraphQL requires Bearer auth and table GraphQL enablement.',
59
+ '',
60
+ 'When the user asks for details, fetch only the relevant live context or example category instead of relying on broad memorized rules.',
469
61
  ].join('\n');
470
62
  }
@@ -353,6 +353,13 @@ async function fetchAll(path) {
353
353
  return unwrapData(await fetchAPI(ENFYRA_API_URL, path));
354
354
  }
355
355
 
356
+ function targetInstance() {
357
+ return {
358
+ apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
359
+ source: 'ENFYRA_API_URL environment variable used by this MCP server process',
360
+ };
361
+ }
362
+
356
363
  async function getMetadataTables() {
357
364
  const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
358
365
  return {
@@ -617,6 +624,7 @@ server.tool(
617
624
  [
618
625
  'Call this first when you need to understand the live Enfyra instance.',
619
626
  'Returns a concise capability map from live metadata/routes/method rows, including schema management, REST route behavior, GraphQL enablement, and relation handling.',
627
+ 'Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
620
628
  ].join(' '),
621
629
  {},
622
630
  async () => {
@@ -637,6 +645,7 @@ server.tool(
637
645
  const routeTableList = [...routeTables].sort();
638
646
 
639
647
  const payload = {
648
+ targetInstance: targetInstance(),
640
649
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
641
650
  counts: {
642
651
  tables: tableNames.length,
@@ -696,7 +705,7 @@ server.tool(
696
705
  'discover_runtime_context',
697
706
  [
698
707
  'Discover live runtime context that affects how an LLM should use Enfyra.',
699
- 'Reports inferred primary key/backend family, route/cache/admin surfaces, active metadata-backed runtime areas, and what is not exposed by the backend API.',
708
+ 'Reports inferred primary key/backend family, route/cache/admin surfaces, active metadata-backed runtime areas, and what is not exposed by the backend API. Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
700
709
  ].join(' '),
701
710
  {},
702
711
  async () => {
@@ -728,6 +737,7 @@ server.tool(
728
737
  const publicRoutes = routes.filter((route) => route.publicMethods?.length);
729
738
 
730
739
  const payload = {
740
+ targetInstance: targetInstance(),
731
741
  apiBase: ENFYRA_API_URL.replace(/\/$/, ''),
732
742
  authenticatedUser: Array.isArray(meResult?.data) ? meResult.data[0] || null : meResult?.data || null,
733
743
  database: getMetadataDatabaseContext(metadata, tables),
@@ -779,7 +789,7 @@ server.tool(
779
789
  'discover_query_capabilities',
780
790
  [
781
791
  'Discover Enfyra query/filter/deep-fetch capabilities for the live instance.',
782
- 'Optionally pass tableName to include columns, relations, primary key, route paths, and examples for that table.',
792
+ 'Prefer passing tableName. Without tableName this returns only generic query rules. Run broad discovery tools sequentially; do not call multiple broad discovery tools in parallel.',
783
793
  ].join(' '),
784
794
  {
785
795
  tableName: z.string().optional().describe('Optional table name to summarize query fields and relation/deep capabilities.'),
@@ -796,6 +806,7 @@ server.tool(
796
806
  : [];
797
807
 
798
808
  const payload = {
809
+ targetInstance: targetInstance(),
799
810
  operators: {
800
811
  filter: FILTER_OPERATORS,
801
812
  fieldPermissionConditions: FIELD_PERMISSION_CONDITION_OPERATORS,
@@ -859,11 +870,12 @@ server.tool(
859
870
  'discover_script_contexts',
860
871
  [
861
872
  'Discover runtime script contexts and macro availability for handlers, hooks, flows, websocket scripts, GraphQL, packages, and extensions.',
862
- 'Use before writing dynamic JavaScript logic so the model does not mix context variables across surfaces.',
873
+ 'Use before writing dynamic JavaScript logic so the model does not mix context variables across surfaces. This tool is static and safe to call alone; avoid running it in parallel with other broad discovery calls.',
863
874
  ].join(' '),
864
875
  {},
865
876
  async () => {
866
877
  const payload = {
878
+ targetInstance: targetInstance(),
867
879
  transformer: {
868
880
  rule: 'Dynamic server scripts are transformed before sandbox execution. Macros expand to $ctx paths; comments are not transformed.',
869
881
  preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use macros such as @BODY/@QUERY/@PARAMS/@USER/@REQ/@RES/@REPOS/@CACHE/@HELPERS/@FETCH/@STORAGE/@UPLOADED_FILE/@SOCKET/@TRIGGER/@DATA/@ERROR/@STATUS/@ENV/@PKGS/@LOGS/@SHARE/@API/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
@@ -1844,14 +1856,20 @@ server.tool(
1844
1856
  'inspect_feature',
1845
1857
  [
1846
1858
  'Search live REST/system metadata for a feature name, route path, table, handler, hook, guard, or permission.',
1847
- 'Use when the user mentions a capability and you need to find where it lives before editing.',
1859
+ 'Use when the user mentions a capability and you need to find where it lives before editing. Keep the query specific; broad searches return bounded summaries.',
1848
1860
  ].join(' '),
1849
1861
  {
1850
1862
  query: z.string().describe('Feature keyword, table name, route path, handler text, hook name, or guard name'),
1863
+ limit: z.number().int().positive().max(25).optional().default(8).describe('Maximum matches returned per section. Default 8 to keep output small.'),
1851
1864
  },
1852
- async ({ query }) => {
1865
+ async ({ query, limit }) => {
1866
+ const rawQuery = String(query || '').trim();
1867
+ if (rawQuery.length < 2) {
1868
+ throw new Error('inspect_feature query must be at least 2 characters. Use a table name, route path, event name, or specific feature keyword.');
1869
+ }
1870
+ const max = Math.max(1, Math.min(Number(limit || 8), 25));
1853
1871
  const state = await collectRestDefinitionState();
1854
- const q = query.toLowerCase();
1872
+ const q = rawQuery.toLowerCase();
1855
1873
  const matchesText = (value) => JSON.stringify(value ?? '').toLowerCase().includes(q);
1856
1874
  const tableMatches = state.tables.filter((table) => matchesText({
1857
1875
  name: table.name,
@@ -1871,7 +1889,9 @@ server.tool(
1871
1889
  ];
1872
1890
 
1873
1891
  const payload = {
1874
- query,
1892
+ targetInstance: targetInstance(),
1893
+ query: rawQuery,
1894
+ limit: max,
1875
1895
  counts: {
1876
1896
  tables: tableMatches.length,
1877
1897
  routes: routeMatches.length,
@@ -1881,13 +1901,14 @@ server.tool(
1881
1901
  guards: guardMatches.length,
1882
1902
  permissions: permissionMatches.length,
1883
1903
  },
1884
- tables: tableMatches.map(summarizeTable).slice(0, 20),
1885
- routes: routeMatches.map((route) => enrichRoute(route, state)).slice(0, 20),
1886
- handlers: handlerMatches.slice(0, 20),
1887
- preHooks: preHookMatches.slice(0, 20),
1888
- postHooks: postHookMatches.slice(0, 20),
1889
- guards: guardMatches.slice(0, 20),
1890
- permissions: permissionMatches.slice(0, 20),
1904
+ tables: tableMatches.slice(0, max).map(summarizeTable),
1905
+ routes: routeMatches.slice(0, max).map((route) => enrichRoute(route, state)),
1906
+ handlers: handlerMatches.slice(0, max),
1907
+ preHooks: preHookMatches.slice(0, max),
1908
+ postHooks: postHookMatches.slice(0, max),
1909
+ guards: guardMatches.slice(0, max),
1910
+ permissions: permissionMatches.slice(0, max),
1911
+ detailHint: 'For a specific match, call inspect_table, inspect_route, trace_metadata_usage, or get_script_source instead of broadening this search.',
1891
1912
  };
1892
1913
 
1893
1914
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };