@enfyra/mcp-server 0.0.84 → 0.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/lib/config-local.mjs +2 -4
- package/src/lib/mcp-examples.js +47 -47
- package/src/lib/mcp-instructions.js +64 -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 +143 -151
|
@@ -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`).',
|
|
@@ -88,10 +88,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
88
88
|
'- 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
89
|
'- 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
90
|
'- 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 `
|
|
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 `"enfyra_user"` and resolves it to the live table id before mutation. The target table must already exist.',
|
|
92
|
+
'- **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`.',
|
|
93
|
+
'- 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.',
|
|
94
|
+
'- **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
95
|
'- **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
96
|
'- 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
97
|
'- 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 +120,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
120
120
|
'- **mainTable warning:** do not set `mainTable` on custom routes. It is reserved for canonical table routes only.',
|
|
121
121
|
' - **Many-to-one:** `"someRelation": {"id": 4}` (single object with id)',
|
|
122
122
|
' - **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
|
-
'- `
|
|
123
|
+
'- **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.',
|
|
124
|
+
'- `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
125
|
'- **Wrong:** `"publicMethods": ["GET"]` or `"publicMethods": [{"method": "GET"}]` — rejected or silently ignored.',
|
|
126
126
|
'- **Right:** first query method records, then pass their ids, for example `"publicMethods": [{"id": <GET_METHOD_ID>}]`. Multiple methods use multiple id objects.',
|
|
127
127
|
'- **To unset:** pass empty array `"publicMethods": []`.',
|
|
@@ -162,10 +162,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
162
162
|
'- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
|
|
163
163
|
'',
|
|
164
164
|
'### Chat / realtime app rules learned from implementation review',
|
|
165
|
-
'- Before generating a chat app, inspect live metadata for `
|
|
165
|
+
'- 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
166
|
'- 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
167
|
'- 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 `
|
|
168
|
+
'- 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
169
|
'- 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
170
|
'- `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
171
|
'- 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 +189,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
189
189
|
'**Redirect URI must match everywhere (critical):**',
|
|
190
190
|
'- 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
191
|
'- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
|
|
192
|
-
'- **Enfyra** (`
|
|
192
|
+
'- **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
193
|
'- **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
194
|
'- **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
195
|
'',
|
|
@@ -210,31 +210,31 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
210
210
|
'',
|
|
211
211
|
'### System tables — which have REST routes',
|
|
212
212
|
'- **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
|
|
213
|
+
'- **`enfyra_column` and `enfyra_session` have NO route** — do NOT call `query_table("enfyra_column", …)` or `query_table("enfyra_session", …)`. They will 404.',
|
|
214
|
+
'- 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
215
|
'- 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 `
|
|
216
|
+
'- **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`.',
|
|
217
|
+
'- **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.',
|
|
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 `enfyra_table?fields=columns.*`, because nested relation fields may be paginated/truncated.',
|
|
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 `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
220
|
'- 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
221
|
'',
|
|
222
222
|
'### Body validation & column rules',
|
|
223
|
-
'- Each `
|
|
223
|
+
'- 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
224
|
'- 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
|
-
'- **`
|
|
225
|
+
'- **`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
226
|
'- **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
227
|
'- 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
228
|
'- 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 `
|
|
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 `enfyra_table`.',
|
|
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 `enfyra_table` with `{ validateBody: false }`.',
|
|
231
231
|
'',
|
|
232
232
|
'### Schema / table migration (sequential only)',
|
|
233
233
|
'- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
|
|
234
234
|
'- 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
235
|
'',
|
|
236
236
|
'### Resolving the real REST path',
|
|
237
|
-
'- Do **not** assume `
|
|
237
|
+
'- Do **not** assume `enfyra_route.path` always equals `enfyra_table.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
|
|
238
238
|
'- 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
239
|
'',
|
|
240
240
|
'### MongoDB vs SQL primary key',
|
|
@@ -257,16 +257,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
257
257
|
'### GraphQL (same prefix as REST / ENFYRA_API_URL)',
|
|
258
258
|
`- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With the required app proxy base: e.g. \`http://localhost:3000/api/graphql\`.`,
|
|
259
259
|
`- **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 `
|
|
260
|
+
'- 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.',
|
|
261
|
+
'- **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
262
|
'- **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 `
|
|
263
|
+
'- **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
264
|
'- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
|
|
265
265
|
'',
|
|
266
266
|
'### WebSocket (Socket.IO)',
|
|
267
|
-
'- Enfyra uses **Socket.IO**. Gateways and events are stored in **`
|
|
268
|
-
'- **Gateway** (`
|
|
269
|
-
'- **Event** (`
|
|
267
|
+
'- 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).',
|
|
268
|
+
'- **Gateway** (`enfyra_websocket`): `path` = namespace (e.g. `/chat`), `requireAuth` (JWT in `auth.token`), `connectionHandlerScript` (runs on connect), `connectionHandlerTimeout`, `isEnabled`.',
|
|
269
|
+
'- **Event** (`enfyra_websocket_event`): `gateway` → gateway id, `eventName` (client emits), `handlerScript`, `timeout`, `isEnabled`.',
|
|
270
270
|
'- **@SOCKET in scripts (prefer template `@SOCKET.method()` over `$ctx.$socket.method()`):**',
|
|
271
271
|
'- `@SOCKET.reply(event, data)` — send to this client only (WS context only).',
|
|
272
272
|
'- `@SOCKET.join(room)` — join a room (WS context only).',
|
|
@@ -282,34 +282,34 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
282
282
|
'- **Context**: Connection — `@BODY` = {id, ip, headers}, `@USER` if auth. Event — `@BODY` = payload, `@USER` if auth. Both have `@SOCKET`.',
|
|
283
283
|
'- **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
284
|
'- **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 `
|
|
285
|
+
'- **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
286
|
'- **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
287
|
'- MCP wrapper: use **`run_admin_test`** with `kind:"websocket_event"` or `kind:"websocket_connection"` instead of hand-building the HTTP call.',
|
|
288
288
|
'- Before writing websocket scripts, call **`discover_script_contexts`** to confirm which `@SOCKET` methods are bound in websocket vs HTTP/flow contexts.',
|
|
289
289
|
'',
|
|
290
290
|
'### Flows (Automated Workflows)',
|
|
291
|
-
'- Enfyra supports automated workflows via **`
|
|
292
|
-
'- **Flow** (`
|
|
293
|
-
'- **Step** (`
|
|
294
|
-
'- **Execution history** (`
|
|
291
|
+
'- Enfyra supports automated workflows via **`enfyra_flow`**, **`enfyra_flow_step`**, and **`enfyra_flow_execution`** tables.',
|
|
292
|
+
'- **Flow** (`enfyra_flow`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
|
|
293
|
+
'- **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).',
|
|
294
|
+
'- **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
295
|
'- **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 #
|
|
296
|
+
'- **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
297
|
'- **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
298
|
'- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
|
|
299
299
|
'- **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 `
|
|
300
|
+
'- **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
301
|
'- **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
302
|
'- **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
303
|
'- **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 `
|
|
304
|
+
'- **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}`.',
|
|
305
|
+
'- **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
306
|
'- **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
307
|
'- MCP wrappers: use **`test_flow_step`** for one flow step, **`run_admin_test`** for flow/websocket tests, and **`trigger_flow`** for saved flows.',
|
|
308
308
|
'- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
|
|
309
309
|
'- Before writing flow scripts, call **`discover_script_contexts`** to confirm `@FLOW`, `@FLOW_PAYLOAD`, `@FLOW_LAST`, `#table_name`, `$ctx.$trigger`, and `$socket` behavior.',
|
|
310
310
|
'',
|
|
311
311
|
'### Extension (Vue SFC only — NOT React)',
|
|
312
|
-
'- **CRITICAL:** MUST call `create_record` or `update_record` on `
|
|
312
|
+
'- **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
313
|
'- **Code format:** Vue SFC only. Structure: `<template>...</template>` + `<script setup>...</script>`. Server auto-compiles; if compile fails, fix and retry.',
|
|
314
314
|
'- **NO import statements.** All APIs are injected globally (see full list below).',
|
|
315
315
|
'- **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 +319,26 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
319
319
|
'- **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
320
|
'- **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
321
|
'- **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:** `
|
|
322
|
+
'- **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
323
|
'- **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
324
|
'- **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
325
|
'- **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
326
|
'- **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: "/
|
|
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: "/report", methods: ["POST"] }] }, onClick }])`. Sensitive registry actions must include a `permission` condition.',
|
|
328
328
|
'- **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 `
|
|
329
|
+
'- **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
330
|
'- **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:** `
|
|
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/report")`, because Vue compiles template helpers to `_ctx.*`.',
|
|
332
|
+
'- **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
333
|
'- **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
334
|
'- **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 `
|
|
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/report` or `/data/order`. Do not use public website paths from record fields unless the explicit intent is previewing the public website.',
|
|
336
|
+
'- **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
337
|
'- **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
338
|
'- **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
339
|
'- **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
340
|
'- **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 `
|
|
341
|
+
'- **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
342
|
'- **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
343
|
'- **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
344
|
'- **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 +358,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
358
358
|
'- UI: `useToast`',
|
|
359
359
|
'',
|
|
360
360
|
'#### 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(\'/
|
|
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(\'/enfyra_user\', { query: { limit: 10 } }); onMounted(() => execute());`',
|
|
362
362
|
'- **useAuth:** Authentication. Returns `{ me, login, logout, fetchUser, isLoggedIn, isLoading, oauthLogin }`. `me` is reactive user object with `isRootAdmin`, `role`, `allowedRoutePermissions`.',
|
|
363
363
|
'- **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
364
|
'- **useSchema:** Schema management. Returns `{ schemas, schema, fetchSchema, schemaLoading, definition, fieldMap, getField(key), editableFields, generateEmptyForm(), validate(record), getIncludeFields(), useFormChanges() }`.',
|
|
@@ -396,16 +396,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
396
396
|
'',
|
|
397
397
|
'#### Extension types:',
|
|
398
398
|
'- **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 `
|
|
399
|
+
'- **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
400
|
'- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
|
|
401
401
|
'- **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 `
|
|
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 `enfyra_extension` rows first, then embed their ids from the page extension.',
|
|
403
403
|
'- **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
404
|
'- **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
405
|
'- **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
406
|
'- **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
407
|
'- **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 `
|
|
408
|
+
'- **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
409
|
'',
|
|
410
410
|
'#### NPM packages (install via MCP):',
|
|
411
411
|
'- **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 +413,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
413
413
|
'- **Search first with `search_npm`** if unsure of exact package name.',
|
|
414
414
|
'- **Server** packages → available as `$ctx.$pkgs.packageName` in handlers/hooks.',
|
|
415
415
|
'- **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 `
|
|
416
|
+
'- **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"];`.',
|
|
417
|
+
'- **Do NOT use `create_record` on `enfyra_package` directly** — use `install_package` instead.',
|
|
418
418
|
'',
|
|
419
419
|
'#### Important patterns:',
|
|
420
420
|
'- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
|
|
@@ -430,7 +430,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
430
430
|
'- API testing is available at `/settings/api-tester` in the app UI.',
|
|
431
431
|
'',
|
|
432
432
|
'### MCP tool → HTTP',
|
|
433
|
-
'- **Routes:** `create_route` / `create_handler` / `create_pre_hook` / `create_post_hook` persist to `
|
|
433
|
+
'- **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
434
|
`- \`get_all_metadata\` → GET \`${base}/metadata\``,
|
|
435
435
|
`- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
|
|
436
436
|
`- \`discover_runtime_context\` → GET metadata/routes/method/runtime-backed tables and infer live primary key/backend context`,
|
|
@@ -446,12 +446,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
446
446
|
`- \`update_record\` → PATCH \`${base}/<tableName>/<id>\` (optional tool queryParams append URL query)`,
|
|
447
447
|
`- \`update_script_source\` → validates sourceCode with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
|
|
448
448
|
`- \`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}/
|
|
449
|
+
`- \`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.`,
|
|
450
|
+
`- Flow tables: \`${base}/enfyra_flow\`, \`${base}/enfyra_flow_step\`, \`${base}/enfyra_flow_execution\` — use standard CRUD tools.`,
|
|
451
451
|
`- \`run_admin_test\` → POST \`${base}/admin/test/run\``,
|
|
452
452
|
`- \`test_flow_step\` → POST \`${base}/admin/test/run\` with \`kind:"flow_step"\``,
|
|
453
453
|
`- \`trigger_flow\` → POST \`${base}/admin/flow/trigger/<flowIdOrName>\``,
|
|
454
|
-
`- Other: \`${base}/
|
|
454
|
+
`- Other: \`${base}/enfyra_menu\`, \`${base}/enfyra_websocket\`, \`${base}/admin/reload\`, etc.`,
|
|
455
455
|
'',
|
|
456
456
|
'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
457
|
].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);
|