@enfyra/mcp-server 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,6 +54,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
54
54
  '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
55
55
  '- MCP tools that use `fetchAPI` authenticate with the configured admin credentials; explain to users that **direct HTTP** calls need a token unless the route/method is published.',
56
56
  '',
57
+ '### Post-hooks (REST)',
58
+ '- **post-hooks always run** after the handler, including when the handler or a pre-hook throws — then `@ERROR` / `$ctx.$error` is set and `@DATA` is null.',
59
+ '- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
60
+ '',
57
61
  '### OAuth login (browser / frontend — not the MCP `login` tool)',
58
62
  '- **MCP `login`** uses **email + password** → `POST {base}/auth/login`. It cannot complete OAuth (no browser redirect).',
59
63
  '- **Supported providers (server):** `google`, `facebook`, `github` only.',
@@ -83,6 +87,16 @@ export function buildMcpServerInstructions(apiBaseUrl) {
83
87
  '- **Tables confirmed to have REST routes (system):** `table_definition`, `route_definition`, `user_definition`, `setting_definition`, `ai_config_definition`, `role_definition`, `menu_definition`, `extension_definition`, `folder_definition`, `file_definition`, `file_permission_definition`, `package_definition`, `bootstrap_script_definition`, `storage_config_definition`, `ai_conversation_definition`, `ai_message_definition`, `websocket_definition`, `websocket_event_definition`, `oauth_config_definition`, `oauth_account_definition`, `method_definition`, `pre_hook_definition`, `post_hook_definition`, `route_handler_definition`, `route_permission_definition`, `flow_definition`, `flow_step_definition`, `flow_execution_definition`.',
84
88
  '- **Tables without REST routes (internal/system only):** `column_definition`, `relation_definition` — these are managed indirectly via cascade on `table_definition` (PATCH /table_definition/{id} with columns/relations array). The `create_column` MCP tool handles this automatically.',
85
89
  '',
90
+ '### Body validation & column rules',
91
+ '- Each `table_definition` 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.',
92
+ '- 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"`).',
93
+ '- **`column_rule_definition`** stores per-column rules: fields are `column` (FK to `column_definition`), `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 `/column_rule_definition` 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.',
94
+ '- **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`.',
95
+ '- 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).',
96
+ '- 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"`).',
97
+ '- 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 `table_definition`.',
98
+ '- 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 `table_definition` with `{ validateBody: false }`.',
99
+ '',
86
100
  '### Schema / table migration (sequential only)',
87
101
  '- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
88
102
  '- Do NOT batch multiple schema changes (e.g. create 3 tables in parallel, or create table + add columns simultaneously). Execute each `create_table`, `create_column`, sync, or drop sequentially; wait for completion before the next.',
@@ -98,7 +112,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
98
112
  `- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With Nuxt base: e.g. \`http://localhost:3000/api/graphql\`. With direct Nest: e.g. \`http://localhost:1105/graphql\`.`,
99
113
  `- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
100
114
  '- A table appears in the schema if some **enabled route** for that table has **both** `GQL_QUERY` and `GQL_MUTATION` in `availableMethods` and a **`mainTable`** pointing at the table. The route **`path` does not need to be** `/<table_name>` (custom paths still qualify).',
101
- '- **Query** field = same string as `table_definition.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`. No mutations if no non-PK columns for input.',
115
+ '- **Query** field = same string as `table_definition.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).',
102
116
  '- **Auth:** `publishedMethods` may include `GQL_QUERY` and/or `GQL_MUTATION` **separately** — each controls anonymous access for queries vs mutations. Otherwise Bearer JWT + `routePermissions` must list the same method key (`GQL_QUERY` / `GQL_MUTATION`).',
103
117
  '- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
104
118
  '',
@@ -127,15 +141,15 @@ export function buildMcpServerInstructions(apiBaseUrl) {
127
141
  '- **Flow** (`flow_definition`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
128
142
  '- **Step** (`flow_step_definition`): `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).',
129
143
  '- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `context` (full data chain), `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. Query separately — NOT nested under flow_definition.',
130
- '- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@DISPATCH.trigger("flow-name", payload)` instead.',
131
- '- **Step config examples**: script: `{"code":"return #user_definition.find({limit:10})"}`, condition: `{"code":"return @LAST?.data?.length > 0"}` (uses JS truthy/falsy: `return user` = truthy if exists, `return null` = falsy), query: `{"table":"user_definition","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}`.',
132
- '- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@LAST`. Input payload via `@PAYLOAD`. Repos via `#table_name`.',
133
- '- **Template syntax (flows)**: `@PAYLOAD` → `$ctx.$flow.$payload` (input data), `@LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@THROW4xx/5xx` → error helpers. Trigger other flows in handlers via `@DISPATCH.trigger(name, payload)` or `$ctx.$dispatch.trigger(name, payload)`.',
144
+ '- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
145
+ '- **Step config examples**: script: `{"code":"return #user_definition.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":"user_definition","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}`.',
146
+ '- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
147
+ '- **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)`.',
134
148
  '- **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.',
135
- '- **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). `$dispatch.trigger()` available inside flow steps.',
149
+ '- **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.',
136
150
  '- **Workflow**: Create flow → `create_record` on `flow_definition`. Add steps → `create_record` on `flow_step_definition` with `flow: {id}`. For branch steps, set `parent: {id: conditionStepId}` and `branch: "true"` or `"false"`. Trigger manually via `POST /admin/flow/trigger/{flowId}`.',
137
151
  '- **Test step**: `POST /admin/flow/test-step` with body `{type, config, timeout}` — runs a single step without saving, returns `{success, result, error, duration}`.',
138
- '- **In handlers/hooks**: Trigger flows via `$ctx.$dispatch.trigger("flow-name", {payload})` or `$ctx.$dispatch.trigger(flowId, {payload})`.',
152
+ '- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
139
153
  '',
140
154
  '### Extension (Vue SFC only — NOT React)',
141
155
  '- **CRITICAL:** MUST call `create_record` or `update_record` on `extension_definition` — outputting Vue code in chat does NOT save it. User will NOT see it.',
@@ -166,6 +180,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
166
180
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
167
181
  '- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ id, label, onClick, color, icon, order, side, global }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component }`.',
168
182
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
183
+ '- **usePageHeaderRegistry:** Page title strip. `{ registerPageHeader, clearPageHeader, pageHeader, hasPageHeader }`. Config: `title`, optional `description`, `stats`, `variant`, `gradient` (`purple`|`blue`|`cyan`|`none` — horizontal strip + leading icon tint), `leadingIcon` (icon name), `hideLeadingIcon`. Call `registerPageHeader` again when title/stats must update (plain object snapshot, not refs inside the config).',
169
184
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
170
185
  '- **useMenuApi:** Low-level menu API.',
171
186
  '- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
@@ -177,7 +192,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
177
192
  '#### Injected UI Components (auto-resolved):',
178
193
  '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
179
194
  '- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
180
- '- **Form:** `FormEditor`, `FilterEditor`, `FilterHistory`, `FieldSelector`',
195
+ '- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
181
196
  '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
182
197
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
183
198
  '- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
@@ -190,7 +205,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
190
205
  '- `$ctx` — runtime context',
191
206
  '',
192
207
  '#### Extension types:',
193
- '- **FormEditor field-map:** Customize fields via `:field-map` prop. Options: `label` (override label), `description` (override description), `hideLabel`/`hideDescription` (boolean), `component` (custom Vue component replacing field), `componentProps`, `type` (override field type), `disabled`, `placeholder`. Custom component receives `modelValue`/`update:modelValue`.',
208
+ '- **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`.',
194
209
  '- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `menu_definition`), find by path/label, then create extension with `menu: { id: menuId }`. `menu_definition` uses **label** not name — filter by `label` or `path`.',
195
210
  '- **type "widget":** Widget extension. No menu required. Embed via `<Widget :id="extensionId" />` in other extensions or pages.',
196
211
  '',
@@ -208,12 +208,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
208
208
  name: z.string().optional().describe('New column name.'),
209
209
  type: z.string().optional().describe('New column type.'),
210
210
  isNullable: z.boolean().optional().describe('Set nullable.'),
211
- isHidden: z.boolean().optional().describe('Hide column from API responses.'),
211
+ isPublished: z.boolean().optional().describe('Set column visibility baseline. false = unpublished (omitted from response unless allowed by field permission rules).'),
212
212
  defaultValue: z.string().optional().describe('New default value as JSON string.'),
213
213
  description: z.string().optional().describe('New description.'),
214
214
  options: z.string().optional().describe('New options as JSON string.'),
215
215
  },
216
- async ({ tableId, columnId, name, type, isNullable, isHidden, defaultValue, description, options }) => {
216
+ async ({ tableId, columnId, name, type, isNullable, isPublished, defaultValue, description, options }) => {
217
217
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
218
218
  if (!tableData) {
219
219
  return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
@@ -225,7 +225,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
225
225
  if (name !== undefined) rest.name = name;
226
226
  if (type !== undefined) rest.type = type;
227
227
  if (isNullable !== undefined) rest.isNullable = isNullable;
228
- if (isHidden !== undefined) rest.isHidden = isHidden;
228
+ if (isPublished !== undefined) rest.isPublished = isPublished;
229
229
  if (defaultValue !== undefined) rest.defaultValue = defaultValue;
230
230
  if (description !== undefined) rest.description = description;
231
231
  if (options !== undefined) rest.options = JSON.parse(options);
@@ -333,8 +333,8 @@ server.tool(
333
333
  [
334
334
  'Create a post-hook that runs AFTER the handler. Use to transform responses or add metadata.',
335
335
  'Use `routeId` from `create_route` or `get_all_routes` — do not create a new table just to get a route id.',
336
- 'Macros: @DATA (handler result), @STATUS (HTTP status code), @BODY, @QUERY, @USER, @SHARE.',
337
- 'Must return a value that becomes the final response.',
336
+ 'Macros: @DATA, @STATUS, @ERROR, @BODY, @QUERY, @USER, @SHARE, @API (post-hooks always run; on error path @ERROR is set, @DATA is null).',
337
+ 'Mutate @DATA / $ctx.$data in place, or return a value: if the hook returns anything other than undefined, that value replaces $ctx.$data as the response payload.',
338
338
  ].join(' '),
339
339
  {
340
340
  routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),