@enfyra/mcp-server 0.0.16 → 0.0.17

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.17",
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.',
@@ -98,7 +102,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
98
102
  `- **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
103
  `- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
100
104
  '- 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.',
105
+ '- **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
106
  '- **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
107
  '- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
104
108
  '',
@@ -130,7 +134,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
130
134
  '- **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
135
  '- **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
136
  '- **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)`.',
137
+ '- **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`, `@THROW400`–`@THROW503` / `@THROW` → `$ctx.$throw[...]`. Trigger other flows in handlers via `@DISPATCH.trigger(name, payload)` or `$ctx.$dispatch.trigger(name, payload)`.',
134
138
  '- **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
139
  '- **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.',
136
140
  '- **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}`.',
@@ -166,6 +170,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
166
170
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
167
171
  '- **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
172
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
173
+ '- **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
174
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
170
175
  '- **useMenuApi:** Low-level menu API.',
171
176
  '- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
@@ -177,7 +182,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
177
182
  '#### Injected UI Components (auto-resolved):',
178
183
  '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
179
184
  '- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
180
- '- **Form:** `FormEditor`, `FilterEditor`, `FilterHistory`, `FieldSelector`',
185
+ '- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
181
186
  '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
182
187
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
183
188
  '- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
@@ -190,7 +195,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
190
195
  '- `$ctx` — runtime context',
191
196
  '',
192
197
  '#### 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`.',
198
+ '- **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
199
  '- **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
200
  '- **type "widget":** Widget extension. No menu required. Embed via `<Widget :id="extensionId" />` in other extensions or pages.',
196
201
  '',
@@ -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'),