@enfyra/mcp-server 0.0.85 → 0.0.87
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 +1 -1
- package/src/lib/mcp-examples.js +122 -47
- package/src/lib/mcp-instructions.js +74 -64
- package/src/lib/mutation-guards.js +18 -18
- package/src/lib/route-permission-tools.js +2 -2
- package/src/lib/table-tools.js +23 -23
- package/src/mcp-server-entry.mjs +148 -154
|
@@ -42,16 +42,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
42
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
43
|
'',
|
|
44
44
|
'### Capability map (current Enfyra system)',
|
|
45
|
-
'- **Schema/metadata:** `
|
|
46
|
-
'- **Dynamic REST API:** `
|
|
47
|
-
'- **Auth/OAuth/session:** `
|
|
48
|
-
'- **Guards/permissions/validation:** `
|
|
49
|
-
'- **GraphQL:** `
|
|
50
|
-
'- **Files/storage/assets:** `
|
|
51
|
-
'- **WebSocket:** `
|
|
52
|
-
'- **Flows:** `
|
|
53
|
-
'- **Extensions/packages/menus:** `
|
|
54
|
-
'- **Platform config:** `
|
|
45
|
+
'- **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.',
|
|
46
|
+
'- **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.',
|
|
47
|
+
'- **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.',
|
|
48
|
+
'- **Guards/permissions/validation:** `enfyra_guard`, `enfyra_guard_rule`, `enfyra_field_permission`, and `enfyra_column_rule` control route guards, field access, and request body validation.',
|
|
49
|
+
'- **GraphQL:** `enfyra_graphql` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
|
|
50
|
+
'- **Files/storage/assets:** `enfyra_file`, `enfyra_file_permission`, `enfyra_folder`, `enfyra_storage_config` plus upload/assets routes and file helpers.',
|
|
51
|
+
'- **WebSocket:** `enfyra_websocket` and `enfyra_websocket_event` define Socket.IO gateways/events. Use `run_admin_test` for websocket scripts.',
|
|
52
|
+
'- **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.',
|
|
53
|
+
'- **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`.',
|
|
54
|
+
'- **Platform config:** `enfyra_setting`, `enfyra_cors_origin`, reload endpoints, logs, and metadata endpoints.',
|
|
55
55
|
'',
|
|
56
56
|
'### ENFYRA_API_URL (MCP must use the app proxy)',
|
|
57
57
|
'- **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`).',
|
|
@@ -82,16 +82,26 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
82
82
|
'- **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.',
|
|
83
83
|
'- **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.',
|
|
84
84
|
'',
|
|
85
|
+
'### Guards (request gates and rate limits)',
|
|
86
|
+
'- 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.',
|
|
87
|
+
'- 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.',
|
|
88
|
+
'- 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.',
|
|
89
|
+
'- 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.',
|
|
90
|
+
'- 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.',
|
|
91
|
+
'- `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.',
|
|
92
|
+
'- `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.',
|
|
93
|
+
'- 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.',
|
|
94
|
+
'',
|
|
85
95
|
'### After a new table is created',
|
|
86
96
|
'- 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.',
|
|
87
97
|
'- 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.',
|
|
88
98
|
'- 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.',
|
|
89
99
|
'- 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.',
|
|
90
100
|
'- 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.',
|
|
91
|
-
'- 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 `"
|
|
92
|
-
'- **Use `
|
|
93
|
-
'- When modeling features that involve users, relate domain tables directly to `
|
|
94
|
-
'- **Do not create reverse relations on `
|
|
101
|
+
'- 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.',
|
|
102
|
+
'- **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`.',
|
|
103
|
+
'- 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.',
|
|
104
|
+
'- **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.',
|
|
95
105
|
'- **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.',
|
|
96
106
|
'- 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.',
|
|
97
107
|
'- 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.',
|
|
@@ -120,8 +130,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
120
130
|
'- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
|
|
121
131
|
' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
|
|
122
132
|
' - **One-to-many / many-to-many:** `"publicMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
|
|
123
|
-
'- **Method IDs** are instance data, not a stable contract. Query `
|
|
124
|
-
'- `
|
|
133
|
+
'- **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.',
|
|
134
|
+
'- `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.',
|
|
125
135
|
'- **Wrong:** `"publicMethods": ["GET"]` or `"publicMethods": [{"method": "GET"}]` — rejected or silently ignored.',
|
|
126
136
|
'- **Right:** first query method records, then pass their ids, for example `"publicMethods": [{"id": <GET_METHOD_ID>}]`. Multiple methods use multiple id objects.',
|
|
127
137
|
'- **To unset:** pass empty array `"publicMethods": []`.',
|
|
@@ -162,10 +172,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
162
172
|
'- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
|
|
163
173
|
'',
|
|
164
174
|
'### Chat / realtime app rules learned from implementation review',
|
|
165
|
-
'- Before generating a chat app, inspect live metadata for `
|
|
175
|
+
'- 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.',
|
|
166
176
|
'- 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.',
|
|
167
177
|
'- 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.',
|
|
168
|
-
'- On an authenticated gateway, Enfyra loads `
|
|
178
|
+
'- 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 should not ask the client to send `senderId`, and `chat:join` does not need to join `user_<userId>` again.',
|
|
169
179
|
'- 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.',
|
|
170
180
|
'- `chat:message` should 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.',
|
|
171
181
|
'- 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.',
|
|
@@ -189,7 +199,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
189
199
|
'**Redirect URI must match everywhere (critical):**',
|
|
190
200
|
'- 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`).',
|
|
191
201
|
'- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
|
|
192
|
-
'- **Enfyra** (`
|
|
202
|
+
'- **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.',
|
|
193
203
|
'- **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.',
|
|
194
204
|
'- **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.',
|
|
195
205
|
'',
|
|
@@ -210,31 +220,31 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
210
220
|
'',
|
|
211
221
|
'### System tables — which have REST routes',
|
|
212
222
|
'- **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.',
|
|
213
|
-
'- **`
|
|
214
|
-
'- Do not invent
|
|
223
|
+
'- **`enfyra_column` and `enfyra_session` have NO route** — do NOT call `query_table("enfyra_column", …)` or `query_table("enfyra_session", …)`. They will 404.',
|
|
224
|
+
'- 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.',
|
|
215
225
|
'- 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.',
|
|
216
|
-
'- **Tables confirmed to have REST routes (system):** `
|
|
217
|
-
'- **Tables without REST routes (internal/system only):** `
|
|
218
|
-
'- 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 `
|
|
219
|
-
'- 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 `
|
|
226
|
+
'- **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`.',
|
|
227
|
+
'- **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.',
|
|
228
|
+
'- 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.',
|
|
229
|
+
'- 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`.',
|
|
220
230
|
'- 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.',
|
|
221
231
|
'',
|
|
222
232
|
'### Body validation & column rules',
|
|
223
|
-
'- Each `
|
|
233
|
+
'- 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.',
|
|
224
234
|
'- 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"`).',
|
|
225
|
-
'- **`
|
|
235
|
+
'- **`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.',
|
|
226
236
|
'- **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`.',
|
|
227
237
|
'- 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).',
|
|
228
238
|
'- 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"`).',
|
|
229
|
-
'- 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 `
|
|
230
|
-
'- 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 `
|
|
239
|
+
'- 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`.',
|
|
240
|
+
'- 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 }`.',
|
|
231
241
|
'',
|
|
232
242
|
'### Schema / table migration (sequential only)',
|
|
233
243
|
'- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
|
|
234
244
|
'- 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.',
|
|
235
245
|
'',
|
|
236
246
|
'### Resolving the real REST path',
|
|
237
|
-
'- Do **not** assume `
|
|
247
|
+
'- Do **not** assume `enfyra_route.path` always equals `enfyra_table.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
|
|
238
248
|
'- 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.',
|
|
239
249
|
'',
|
|
240
250
|
'### MongoDB vs SQL primary key',
|
|
@@ -257,16 +267,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
257
267
|
'### GraphQL (same prefix as REST / ENFYRA_API_URL)',
|
|
258
268
|
`- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With the required app proxy base: e.g. \`http://localhost:3000/api/graphql\`.`,
|
|
259
269
|
`- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
|
|
260
|
-
'- A table appears in the schema when `
|
|
261
|
-
'- **Query** field = same string as `
|
|
270
|
+
'- 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.',
|
|
271
|
+
'- **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).',
|
|
262
272
|
'- **Auth:** GraphQL currently requires `Authorization: Bearer <accessToken>`. REST route `publicMethods` does not make GraphQL anonymous.',
|
|
263
|
-
'- **Management workflow:** use `update_table` with `graphqlEnabled: true|false`, or create/update `
|
|
273
|
+
'- **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.',
|
|
264
274
|
'- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
|
|
265
275
|
'',
|
|
266
276
|
'### WebSocket (Socket.IO)',
|
|
267
|
-
'- Enfyra uses **Socket.IO**. Gateways and events are stored in **`
|
|
268
|
-
'- **Gateway** (`
|
|
269
|
-
'- **Event** (`
|
|
277
|
+
'- 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).',
|
|
278
|
+
'- **Gateway** (`enfyra_websocket`): `path` = namespace (e.g. `/chat`), `requireAuth` (JWT in `auth.token`), `connectionHandlerScript` (runs on connect), `connectionHandlerTimeout`, `isEnabled`.',
|
|
279
|
+
'- **Event** (`enfyra_websocket_event`): `gateway` → gateway id, `eventName` (client emits), `handlerScript`, `timeout`, `isEnabled`.',
|
|
270
280
|
'- **@SOCKET in scripts (prefer template `@SOCKET.method()` over `$ctx.$socket.method()`):**',
|
|
271
281
|
'- `@SOCKET.reply(event, data)` — send to this client only (WS context only).',
|
|
272
282
|
'- `@SOCKET.join(room)` — join a room (WS context only).',
|
|
@@ -282,34 +292,34 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
282
292
|
'- **Context**: Connection — `@BODY` = {id, ip, headers}, `@USER` if auth. Event — `@BODY` = payload, `@USER` if auth. Both have `@SOCKET`.',
|
|
283
293
|
'- **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`.',
|
|
284
294
|
'- **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.',
|
|
285
|
-
'- **Workflow**: Create gateway → `create_record` on `
|
|
295
|
+
'- **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.',
|
|
286
296
|
'- **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 }`.',
|
|
287
297
|
'- MCP wrapper: use **`run_admin_test`** with `kind:"websocket_event"` or `kind:"websocket_connection"` instead of hand-building the HTTP call.',
|
|
288
298
|
'- Before writing websocket scripts, call **`discover_script_contexts`** to confirm which `@SOCKET` methods are bound in websocket vs HTTP/flow contexts.',
|
|
289
299
|
'',
|
|
290
300
|
'### Flows (Automated Workflows)',
|
|
291
|
-
'- Enfyra supports automated workflows via **`
|
|
292
|
-
'- **Flow** (`
|
|
293
|
-
'- **Step** (`
|
|
294
|
-
'- **Execution history** (`
|
|
301
|
+
'- Enfyra supports automated workflows via **`enfyra_flow`**, **`enfyra_flow_step`**, and **`enfyra_flow_execution`** tables.',
|
|
302
|
+
'- **Flow** (`enfyra_flow`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
|
|
303
|
+
'- **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).',
|
|
304
|
+
'- **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.',
|
|
295
305
|
'- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
|
|
296
|
-
'- **Step config examples**: script: `{"code":"return #
|
|
306
|
+
'- **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}`.',
|
|
297
307
|
'- **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.',
|
|
298
308
|
'- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
|
|
299
309
|
'- **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.',
|
|
300
|
-
'- **Flow refactor rule:** when changing an existing oversized flow step, prefer adding or extracting adjacent focused `
|
|
310
|
+
'- **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.',
|
|
301
311
|
'- **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)`.',
|
|
302
312
|
'- **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.',
|
|
303
313
|
'- **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.',
|
|
304
|
-
'- **Workflow**: Create flow → `create_record` on `
|
|
305
|
-
'- **Flow source sanity:** after creating or patching a multi-step flow, refetch saved `
|
|
314
|
+
'- **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}`.',
|
|
315
|
+
'- **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.',
|
|
306
316
|
'- **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}`.',
|
|
307
317
|
'- MCP wrappers: use **`test_flow_step`** for one flow step, **`run_admin_test`** for flow/websocket tests, and **`trigger_flow`** for saved flows.',
|
|
308
318
|
'- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
|
|
309
319
|
'- Before writing flow scripts, call **`discover_script_contexts`** to confirm `@FLOW`, `@FLOW_PAYLOAD`, `@FLOW_LAST`, `#table_name`, `$ctx.$trigger`, and `$socket` behavior.',
|
|
310
320
|
'',
|
|
311
321
|
'### Extension (Vue SFC only — NOT React)',
|
|
312
|
-
'- **CRITICAL:** MUST call `create_record` or `update_record` on `
|
|
322
|
+
'- **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.',
|
|
313
323
|
'- **Code format:** Vue SFC only. Structure: `<template>...</template>` + `<script setup>...</script>`. Server auto-compiles; if compile fails, fix and retry.',
|
|
314
324
|
'- **NO import statements.** All APIs are injected globally (see full list below).',
|
|
315
325
|
'- **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.',
|
|
@@ -319,26 +329,26 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
319
329
|
'- **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.',
|
|
320
330
|
'- **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.',
|
|
321
331
|
'- **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.',
|
|
322
|
-
'- **Menu/extension realtime reload contract:** `
|
|
332
|
+
'- **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.',
|
|
323
333
|
'- **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.',
|
|
324
334
|
'- **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.',
|
|
325
335
|
'- **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.',
|
|
326
336
|
'- **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.',
|
|
327
|
-
'- **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: "/
|
|
337
|
+
'- **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.',
|
|
328
338
|
'- **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.',
|
|
329
|
-
'- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `
|
|
339
|
+
'- **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.',
|
|
330
340
|
'- **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.',
|
|
331
|
-
'- **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/
|
|
332
|
-
'- **Global extension lifecycle:** `
|
|
341
|
+
'- **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.*`.',
|
|
342
|
+
'- **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.',
|
|
333
343
|
'- **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.',
|
|
334
344
|
'- **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.',
|
|
335
|
-
'- **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/
|
|
336
|
-
'- **Admin menu visibility is permission-driven, not RLS:** admin menu entries are sensitive and must set `
|
|
345
|
+
'- **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.',
|
|
346
|
+
'- **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"] }] }`.',
|
|
337
347
|
'- **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.',
|
|
338
348
|
'- **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.',
|
|
339
349
|
'- **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.',
|
|
340
350
|
'- **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.',
|
|
341
|
-
'- **FormEditor is preferred for table-record forms:** when an extension creates or edits a concrete table record such as `
|
|
351
|
+
'- **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.',
|
|
342
352
|
'- **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.',
|
|
343
353
|
'- **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.',
|
|
344
354
|
'- **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.',
|
|
@@ -358,7 +368,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
358
368
|
'- UI: `useToast`',
|
|
359
369
|
'',
|
|
360
370
|
'#### Injected Enfyra composables:',
|
|
361
|
-
'- **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(\'/
|
|
371
|
+
'- **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());`',
|
|
362
372
|
'- **useAuth:** Authentication. Returns `{ me, login, logout, fetchUser, isLoggedIn, isLoading, oauthLogin }`. `me` is reactive user object with `isRootAdmin`, `role`, `allowedRoutePermissions`.',
|
|
363
373
|
'- **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"] }] }`.',
|
|
364
374
|
'- **useSchema:** Schema management. Returns `{ schemas, schema, fetchSchema, schemaLoading, definition, fieldMap, getField(key), editableFields, generateEmptyForm(), validate(record), getIncludeFields(), useFormChanges() }`.',
|
|
@@ -396,16 +406,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
396
406
|
'',
|
|
397
407
|
'#### Extension types:',
|
|
398
408
|
'- **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`.',
|
|
399
|
-
'- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `
|
|
409
|
+
'- **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.',
|
|
400
410
|
'- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
|
|
401
411
|
'- **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.',
|
|
402
|
-
'- **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 `
|
|
412
|
+
'- **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.',
|
|
403
413
|
'- **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.',
|
|
404
414
|
'- **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.',
|
|
405
415
|
'- **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.',
|
|
406
416
|
'- **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.',
|
|
407
417
|
'- **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.',
|
|
408
|
-
'- **Existing pages:** if a menu already has a page extension, update that `
|
|
418
|
+
'- **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.',
|
|
409
419
|
'',
|
|
410
420
|
'#### NPM packages (install via MCP):',
|
|
411
421
|
'- **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.',
|
|
@@ -413,8 +423,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
413
423
|
'- **Search first with `search_npm`** if unsure of exact package name.',
|
|
414
424
|
'- **Server** packages → available as `$ctx.$pkgs.packageName` in handlers/hooks.',
|
|
415
425
|
'- **App** packages → available via `getPackages([\'dayjs\'])` in extensions (call in `onMounted`).',
|
|
416
|
-
'- **Extension package imports:** Do not write static imports like `import { CalendarDate } from "@internationalized/date"` inside `
|
|
417
|
-
'- **Do NOT use `create_record` on `
|
|
426
|
+
'- **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"];`.',
|
|
427
|
+
'- **Do NOT use `create_record` on `enfyra_package` directly** — use `install_package` instead.',
|
|
418
428
|
'',
|
|
419
429
|
'#### Important patterns:',
|
|
420
430
|
'- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
|
|
@@ -430,7 +440,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
430
440
|
'- API testing is available at `/settings/api-tester` in the app UI.',
|
|
431
441
|
'',
|
|
432
442
|
'### MCP tool → HTTP',
|
|
433
|
-
'- **Routes:** `create_route` / `create_handler` / `create_pre_hook` / `create_post_hook` persist to `
|
|
443
|
+
'- **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`.',
|
|
434
444
|
`- \`get_all_metadata\` → GET \`${base}/metadata\``,
|
|
435
445
|
`- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
|
|
436
446
|
`- \`discover_runtime_context\` → GET metadata/routes/method/runtime-backed tables and infer live primary key/backend context`,
|
|
@@ -446,12 +456,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
446
456
|
`- \`update_record\` → PATCH \`${base}/<tableName>/<id>\` (optional tool queryParams append URL query)`,
|
|
447
457
|
`- \`update_script_source\` → validates sourceCode with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
|
|
448
458
|
`- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\` after preview + confirm=true (optional tool queryParams append URL query)`,
|
|
449
|
-
`- \`create_extension\` → POST \`${base}/
|
|
450
|
-
`- Flow tables: \`${base}/
|
|
459
|
+
`- \`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.`,
|
|
460
|
+
`- Flow tables: \`${base}/enfyra_flow\`, \`${base}/enfyra_flow_step\`, \`${base}/enfyra_flow_execution\` — use standard CRUD tools.`,
|
|
451
461
|
`- \`run_admin_test\` → POST \`${base}/admin/test/run\``,
|
|
452
462
|
`- \`test_flow_step\` → POST \`${base}/admin/test/run\` with \`kind:"flow_step"\``,
|
|
453
463
|
`- \`trigger_flow\` → POST \`${base}/admin/flow/trigger/<flowIdOrName>\``,
|
|
454
|
-
`- Other: \`${base}/
|
|
464
|
+
`- Other: \`${base}/enfyra_menu\`, \`${base}/enfyra_websocket\`, \`${base}/admin/reload\`, etc.`,
|
|
455
465
|
'',
|
|
456
466
|
'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.',
|
|
457
467
|
].join('\n');
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
const SCRIPT_TABLES = new Set([
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
2
|
+
'enfyra_route_handler',
|
|
3
|
+
'enfyra_pre_hook',
|
|
4
|
+
'enfyra_post_hook',
|
|
5
|
+
'enfyra_flow_step',
|
|
6
|
+
'enfyra_websocket_event',
|
|
7
|
+
'enfyra_websocket',
|
|
8
|
+
'enfyra_graphql',
|
|
9
|
+
'enfyra_bootstrap_script',
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
12
|
const CODE_ALIAS_FORBIDDEN_TABLES = new Set([
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
13
|
+
'enfyra_route_handler',
|
|
14
|
+
'enfyra_pre_hook',
|
|
15
|
+
'enfyra_post_hook',
|
|
16
|
+
'enfyra_flow_step',
|
|
17
|
+
'enfyra_websocket_event',
|
|
18
|
+
'enfyra_websocket',
|
|
19
|
+
'enfyra_graphql',
|
|
20
|
+
'enfyra_bootstrap_script',
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
const FORBIDDEN_RELATION_DEFINITION_KEYS = new Set([
|
|
@@ -67,11 +67,11 @@ export function rejectUnsafeScriptPayload(tableName, payload) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function rejectUnsafeRelationDefinitionPayload(tableName, payload) {
|
|
70
|
-
if (tableName !== '
|
|
70
|
+
if (tableName !== 'enfyra_relation') return;
|
|
71
71
|
const forbidden = Object.keys(payload).filter((key) => FORBIDDEN_RELATION_DEFINITION_KEYS.has(key));
|
|
72
72
|
if (forbidden.length > 0) {
|
|
73
73
|
throw new Error(
|
|
74
|
-
`Do not send physical FK/junction fields to
|
|
74
|
+
`Do not send physical FK/junction fields to enfyra_relation: ${forbidden.join(', ')}. ` +
|
|
75
75
|
'Use create_relation/add_relation with targetTable/type/propertyName; Enfyra derives physical columns.'
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -36,7 +36,7 @@ export function resolveRoleByNameOrId(roles, { roleId, roleName } = {}) {
|
|
|
36
36
|
|
|
37
37
|
export function methodNamesFromRecords(methods, methodIdNameMap = {}) {
|
|
38
38
|
return normalizeMethodNames((methods || []).map((method) => (
|
|
39
|
-
method?.name ||
|
|
39
|
+
method?.name || methodIdNameMap[String(getRecordId(method))] || method
|
|
40
40
|
)));
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -57,7 +57,7 @@ export function validateMethodsForRoute(route, methods, methodMap, methodIdNameM
|
|
|
57
57
|
const knownMethods = new Set(Object.keys(methodMap || {}).map(normalizeMethodName));
|
|
58
58
|
const unknown = normalizedMethods.filter((method) => !knownMethods.has(method));
|
|
59
59
|
if (unknown.length) {
|
|
60
|
-
throw new Error(`Unknown
|
|
60
|
+
throw new Error(`Unknown enfyra_method.name values: ${unknown.join(', ')}`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const availableMethods = routeAvailableMethodNames(route, methodIdNameMap);
|