@enfyra/mcp-server 0.0.50 → 0.0.52

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.50",
3
+ "version": "0.0.52",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -184,6 +184,32 @@ window.location.href = url.toString()`,
184
184
  'For chat-list UX, default to a boolean unread dot instead of exact counts.',
185
185
  ],
186
186
  },
187
+ {
188
+ name: 'Add server-owned user verification fields',
189
+ code: `create_column({
190
+ tableId: "<user_definition_table_id>",
191
+ name: "emailVerifiedAt",
192
+ type: "datetime",
193
+ isNullable: true,
194
+ isPublished: true,
195
+ description: "When the user's email address was verified."
196
+ })
197
+
198
+ create_column({
199
+ tableId: "<user_definition_table_id>",
200
+ name: "emailVerificationStatus",
201
+ type: "varchar",
202
+ isNullable: false,
203
+ defaultValue: "pending",
204
+ isPublished: true,
205
+ description: "Email verification state controlled by server hooks."
206
+ })`,
207
+ notes: [
208
+ 'Run schema-changing calls sequentially. Do not parallelize create_column calls.',
209
+ 'create_column fetches table_definition and patches only real persisted columns with id/_id; generated metadata projections such as createdAt, updatedAt, or relation FK display fields are skipped.',
210
+ 'Use hooks or field permissions to prevent clients from updating server-owned fields.',
211
+ ],
212
+ },
187
213
  ],
188
214
  },
189
215
  'queries-deep': {
@@ -289,19 +315,49 @@ const scope = {
289
315
  },
290
316
  {
291
317
  name: 'Pre-hook encrypted field normalization',
292
- code: `const value = @BODY.api_token_encrypted
318
+ code: `create_pre_hook({
319
+ routeId: "<route_id>",
320
+ name: "encrypt_api_token",
321
+ methods: ["POST", "PATCH"],
322
+ priority: 0,
323
+ code: \`const value = @BODY.api_token_encrypted
293
324
  if (value && value.slice(0, 7) !== "enc:v1:") {
294
325
  @BODY.api_token_encrypted = @HELPERS.$encrypt.encrypt(value)
295
- }`,
326
+ }\`
327
+ })`,
296
328
  notes: [
329
+ 'MCP create_pre_hook accepts code as the tool argument, then persists it to Enfyra as sourceCode with scriptLanguage.',
330
+ 'Do not call raw create_record with a code field for pre_hook_definition or post_hook_definition; backend CRUD rejects code.',
297
331
  'Use Enfyra pre-hooks for request-body normalization before canonical CRUD persists the record.',
298
332
  'Do not implement encrypted field normalization as a Knex/database hook.',
299
333
  'Use $encrypt for encryption and $ssh.generateKeyPair for SSH key generation; do not use $secrets.',
300
334
  ],
301
335
  },
336
+ {
337
+ name: 'Pre-hook strips protected body fields silently',
338
+ code: `create_pre_hook({
339
+ routeId: "<user_definition_patch_route_id>",
340
+ name: "strip_email_verification_fields",
341
+ methods: ["PATCH"],
342
+ priority: -10,
343
+ code: \`delete @BODY.emailVerifiedAt
344
+ delete @BODY.emailVerificationStatus
345
+ delete @BODY.emailVerificationSentAt\`
346
+ })`,
347
+ notes: [
348
+ 'Use this pattern when clients may send protected user fields through /me or user_definition PATCH.',
349
+ 'Strip fields instead of throwing when the product wants a permissive client contract with server-owned fields.',
350
+ 'Use native macros such as @BODY instead of raw $ctx when a macro exists.',
351
+ ],
352
+ },
302
353
  {
303
354
  name: 'Post-hook response shaping',
304
- code: `if (@ERROR) {
355
+ code: `create_post_hook({
356
+ routeId: "<route_id>",
357
+ name: "shape_display_title",
358
+ methods: ["GET"],
359
+ priority: 0,
360
+ code: \`if (@ERROR) {
305
361
  @LOGS("Request failed", @ERROR.message)
306
362
  return
307
363
  }
@@ -311,8 +367,10 @@ if (row) {
311
367
  row.displayTitle = row.title || row.email || String(row.id)
312
368
  }
313
369
 
314
- return @DATA`,
370
+ return @DATA\`
371
+ })`,
315
372
  notes: [
373
+ 'MCP create_post_hook accepts code as the tool argument, then persists sourceCode/scriptLanguage to Enfyra.',
316
374
  'Post-hooks run after success and error paths.',
317
375
  'Return non-undefined only when replacing the response body.',
318
376
  ],
@@ -650,6 +708,58 @@ create_extension({
650
708
  'After saving, open eApp tabs should update through the server/eApp realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
651
709
  ],
652
710
  },
711
+ {
712
+ name: 'Page header and action button variants',
713
+ code: `<script setup>
714
+ const { registerPageHeader } = usePageHeaderRegistry()
715
+
716
+ registerPageHeader({
717
+ title: 'Host detail',
718
+ description: 'Provider state, capacity, projects, and reconciliation status.',
719
+ leadingIcon: 'lucide:server',
720
+ gradient: 'cyan',
721
+ variant: 'minimal'
722
+ })
723
+
724
+ useHeaderActionRegistry([
725
+ {
726
+ id: 'back-to-hosts',
727
+ label: 'Hosts',
728
+ icon: 'lucide:arrow-left',
729
+ color: 'neutral',
730
+ variant: 'ghost',
731
+ order: 0,
732
+ onClick: () => navigateTo('/cloud/hosts')
733
+ },
734
+ {
735
+ id: 'run-host-check',
736
+ label: 'Run check',
737
+ icon: 'lucide:scan-search',
738
+ color: 'neutral',
739
+ variant: 'outline',
740
+ order: 1,
741
+ permission: { or: [{ route: '/cloud/admin/hosts/reconcile', methods: ['POST'] }] },
742
+ onClick: runCheck
743
+ },
744
+ {
745
+ id: 'refresh-host',
746
+ label: 'Refresh',
747
+ icon: 'lucide:refresh-cw',
748
+ color: 'primary',
749
+ variant: 'solid',
750
+ order: 2,
751
+ onClick: refresh
752
+ }
753
+ ])
754
+ </script>`,
755
+ notes: [
756
+ 'Use PageHeader for the title strip; do not render a duplicate header inside extension body.',
757
+ 'Back/navigation actions should be neutral ghost so they read as navigation, not a primary operation.',
758
+ 'Visible secondary operations should be neutral outline; soft is only for low-emphasis chrome actions.',
759
+ 'The main page action should be primary solid.',
760
+ 'Do not choose soft only because it looks acceptable in dark mode; light mode must remain clear too.',
761
+ ],
762
+ },
653
763
  {
654
764
  name: 'Debug menu or extension changes that do not appear in open eApp tabs',
655
765
  code: `// Server side: menu_definition and extension_definition are runtime UI definitions.
@@ -134,6 +134,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
134
134
  '- For encrypted persisted fields such as `*_encrypted`, use an Enfyra route pre-hook, not a Knex/database hook. Mutate the body before persistence: `const value = @BODY.field_encrypted; if (value && value.slice(0, 7) !== "enc:v1:") @BODY.field_encrypted = @HELPERS.$encrypt.encrypt(value);`.',
135
135
  '- ASV exposes `$helpers.$encrypt.encrypt/decrypt` for encrypted strings and `$helpers.$ssh.generateKeyPair` for SSH keys. Do not generate `$helpers.$secrets` usage.',
136
136
  '- Script-backed records use one shared persistence contract: `sourceCode` is the editable source, `scriptLanguage` controls compilation, and `compiledCode` is generated by the server from `sourceCode`. Do not hand-edit or send stale `compiledCode` from generated tools; save `sourceCode`/`scriptLanguage` through `PATCH /<script_table>/<id>` and let the server persist generated `compiledCode` internally. Public metadata may mark `compiledCode` non-updatable, but the server engine must still preserve the generated value after normalization.',
137
+ '- MCP `create_pre_hook` and `create_post_hook` accept a user-facing `code` argument but persist it as `sourceCode` with `scriptLanguage`. Do not call raw `create_record` with a `code` field for hook tables; backend request validation rejects `code` on REST CRUD.',
137
138
  '- Enfyra Cloud host provisioning with PgBouncer must preserve tenant database isolation. PgBouncer should use per-tenant `DATABASE_URLS` entries with each tenant `db_user`, `db_password`, and `db_name`, and PostgreSQL 16/SCRAM hosts need `AUTH_TYPE=plain`. Do not route all tenants through PostgreSQL `postgres` just to make PgBouncer connect; that bypasses tenant DB permissions.',
138
139
  '- Enfyra Cloud Docker health checks must compare exact healthy states. `true healthy` passes, `true starting` keeps waiting, and `true unhealthy` fails. Do not use broad substring logic where `unhealthy` accidentally counts as healthy.',
139
140
  '- Before saving generated script code, validate it with `POST /admin/script/validate` when available. It compiles with the server kernel and parses the executable async body without running side effects. Enfyra App `FormCodeEditor` also exposes a `Validate` action for this endpoint; use it before save/run when editing through the UI. If unavailable, use `run_admin_test`/`test_flow_step` as the closest validation path before saving.',
@@ -194,7 +195,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
194
195
  '- To check which tables are accessible via MCP tools, call `get_all_routes` and look for the route whose `mainTable.id` matches the table you need, or `get_all_metadata` to see all table names.',
195
196
  '- **Tables confirmed to have REST routes (system):** `bootstrap_script_definition`, `column_rule_definition`, `cors_origin_definition`, `extension_definition`, `field_permission_definition`, `file_definition`, `file_permission_definition`, `flow_definition`, `flow_execution_definition`, `flow_step_definition`, `folder_definition`, `gql_definition`, `guard_definition`, `guard_rule_definition`, `menu_definition`, `method_definition`, `oauth_account_definition`, `oauth_config_definition`, `package_definition`, `post_hook_definition`, `pre_hook_definition`, `relation_definition`, `role_definition`, `route_definition`, `route_handler_definition`, `route_permission_definition`, `schema_migration_definition`, `setting_definition`, `storage_config_definition`, `table_definition`, `user_definition`, `websocket_definition`, `websocket_event_definition`.',
196
197
  '- **Tables without REST routes (internal/system only):** `column_definition`, `session_definition`. Columns are managed indirectly via cascade on `table_definition` (POST/PATCH with columns arrays). The `create_table`, `create_column`/`add_column`, `update_column`, and `delete_column`/`remove_column` MCP tools handle this automatically.',
197
- '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isPublished=false` directly when creating secret/internal fields such as `*_encrypted`.',
198
+ '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isPublished=false` directly when creating secret/internal fields such as `*_encrypted`. 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.',
198
199
  '- Prefer `create_relation`/`add_relation` and `delete_relation`/`remove_relation` for relation schema changes because they preserve the full table relation list and handle schema-confirm retry. Direct `create_record` on `relation_definition` only edits metadata and is not the canonical schema migration path.',
199
200
  '',
200
201
  '### Body validation & column rules',
@@ -294,6 +295,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
294
295
  '- **PageHeader is mandatory for page extensions:** eApp 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.',
295
296
  '- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, host capacity, 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.',
296
297
  '- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Sensitive registry actions must include a `permission` condition, for example `{ id: "create", label: "Create project", permission: { and: [{ route: "/cloud_projects", methods: ["POST"] }] }, onClick }`.',
298
+ '- **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.',
297
299
  '- **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.',
298
300
  '- **Extension runtime scope:** eApp 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/cloud_projects")`, because Vue compiles template helpers to `_ctx.*`.',
299
301
  '- **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.',
@@ -301,7 +303,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
301
303
  '- **Admin menu visibility is permission-driven, not RLS:** Cloud/admin menu entries are sensitive and must set `menu_definition.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 a Cloud menu merely because an extension exists or because the path is hardcoded. Example: `/cloud/hosts` menu should require `{ or: [{ route: "/cloud/admin/hosts", methods: ["GET"] }, { route: "/cloud_servers", methods: ["GET"] }] }`.',
302
304
  '- **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.',
303
305
  '- **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.',
304
- '- **Cloud project admin operations:** use canonical `cloud_projects` table routes as the single source of truth. Admin manual create uses `POST /cloud_projects` with schema-safe fields `owner: { id }`, `plan: { id }`, `admin_email`, `admin_password`, `name`, `subdomain`, and `status: "creating"`. The UI must show tenant admin credential as an email/password pair and include a generate-password action before create; do not build an email-only credential form. Project detail is the place for destructive lifecycle actions. Disable uses `PATCH /cloud_projects/:id` with body `{ status: "disabled" }` and `confirm_tenant_id`/`confirm_hash` in query params. Enable uses `PATCH /cloud_projects/:id` with body `{ status: "running" }`. Delete uses `DELETE /cloud_projects/:id` with typed `confirm_tenant_id`, returned `requiredConfirmHash`, and the matching hash before triggering `cloud-delete-project`. Do not create separate one-off `/cloud/admin/projects/*` action routes for create/disable/enable/delete when the canonical table route can own the workflow.',
306
+ '- **Cloud project admin operations:** use canonical `cloud_projects` table routes as the single source of truth. Admin manual create uses `POST /cloud_projects` with schema-safe fields `owner: { id }`, `plan: { id }`, `expiredAt`, and `status: "creating"`. The UI searches/selects `user_definition` by email and selects a plan by card; do not ask the operator to type raw user ids, plan ids, project names, subdomains, tenant admin emails, or passwords when the handler can derive them server-side. Project detail is the place for destructive lifecycle actions. Disable uses `PATCH /cloud_projects/:id` with body `{ status: "disabled" }` and `confirm_tenant_id`/`confirm_hash` in query params. Enable uses `PATCH /cloud_projects/:id` with body `{ status: "running" }`. Renew uses `PATCH /cloud_projects/:id` with a future `expiredAt`. Delete uses `DELETE /cloud_projects/:id` with typed `confirm_tenant_id`, returned `requiredConfirmHash`, and the matching hash before triggering `cloud-delete-project`. Do not create separate one-off `/cloud/admin/projects/*` action routes for create/disable/enable/delete when the canonical table route can own the workflow.',
305
307
  '- **Cloud admin terminology:** in Cloud admin UI, call physical tenant workloads "projects" everywhere. Do not label creation or details as "instance" unless the user explicitly asks for that word.',
306
308
  '- **Cloud project create UI:** manual Cloud project creation should use `CommonDrawer`, not a wide modal. Let the operator search/select `user_definition` by email and select a plan with cards; do not expose duplicate free-text `user id` or `plan id` inputs when selectors exist. Prefer sending only the selected owner id, plan id, and required workflow fields such as `expiredAt` when the canonical handler can derive customer email, project name, subdomain, and password. Expiry selection should use quick presets plus a manual calendar (`UCalendar` when available, loaded through `install_package`/`getPackages` if an app package is needed).',
307
309
  '- **Cloud host settings and creation UI:** host settings store only provider selection codes Enfyra controls, currently location and server type. Do not expose or save provider-derived RAM, disk, vCPU, or cost values by hand. Query the provider catalog route, show real package/location cards, support load-more/search when the list is long, and snapshot provider facts onto `cloud_servers` only during host creation.',
@@ -380,7 +382,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
380
382
  '',
381
383
  '#### Important patterns:',
382
384
  '- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
383
- '- **Header actions:** `useHeaderActionRegistry([{ id: \'refresh\', label: \'Refresh\', onClick: fn, color: \'primary\', icon: \'lucide:refresh\', order: 0 }])`',
385
+ '- **Header actions:** `useHeaderActionRegistry([{ id: \'back\', label: \'Hosts\', icon: \'lucide:arrow-left\', color: \'neutral\', variant: \'ghost\', order: 0, onClick: goBack }, { id: \'refresh\', label: \'Refresh\', icon: \'lucide:refresh-cw\', color: \'primary\', variant: \'solid\', order: 1, onClick: refresh }])`',
384
386
  '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
385
387
  '- **Permissions:** Use `checkPermissionCondition({ or: [{ route: \'/posts\', methods: [\'GET\'] }] })` for complex rules. In templates, wrap sensitive controls with `<PermissionGate :condition="{ and: [{ route: \'/admin/action\', methods: [\'POST\'] }] }">...</PermissionGate>` instead of only disabling them visually.',
386
388
  '- **After menu/extension create/update:** open eApp tabs should update through the `$system:reload` contract. Do not tell the user to press F5 unless you have verified the natural reload event failed or the server/eApp version does not support menu/extension reload yet.',
@@ -84,11 +84,21 @@ function normalizeRelationForTablePatch(relation) {
84
84
  return normalized;
85
85
  }
86
86
 
87
+ function getId(record) {
88
+ return record?.id ?? record?._id ?? null;
89
+ }
90
+
87
91
  function normalizeColumnForTablePatch(column) {
88
92
  const { table, ...rest } = column;
89
93
  return rest;
90
94
  }
91
95
 
96
+ function getPatchableColumns(columns) {
97
+ return (columns || [])
98
+ .filter((column) => getId(column) !== null)
99
+ .map(normalizeColumnForTablePatch);
100
+ }
101
+
92
102
  function buildColumnDefinition({
93
103
  name,
94
104
  type,
@@ -127,7 +137,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
127
137
  return { content: [{ type: 'text', text: `Error: Table with ID ${args.tableId} not found.` }] };
128
138
  }
129
139
 
130
- const existingColumns = (tableData.columns || []).map(normalizeColumnForTablePatch);
140
+ const existingColumns = getPatchableColumns(tableData.columns);
131
141
  const newCol = buildColumnDefinition(args);
132
142
  const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, { columns: [...existingColumns, newCol] });
133
143
 
@@ -161,7 +171,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
161
171
  }
162
172
 
163
173
  const columns = (tableData.columns || [])
164
- .filter(col => String(col.id) !== String(columnId))
174
+ .filter((col) => getId(col) !== null)
175
+ .filter(col => String(getId(col)) !== String(columnId))
165
176
  .map(normalizeColumnForTablePatch);
166
177
 
167
178
  const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
@@ -372,7 +383,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
372
383
  [
373
384
  'Add a column to an existing table via PATCH /table_definition/{tableId}.',
374
385
  'Columns are managed through cascade with table_definition — there is NO direct /column_definition endpoint.',
375
- 'This tool fetches existing columns, appends the new one, and PATCHes the table.',
386
+ 'This tool fetches existing columns, keeps only persisted column rows with id/_id, appends the new one, and PATCHes the table.',
387
+ 'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are not valid cascade rows and are skipped.',
376
388
  'Run schema changes sequentially — migration locks DB per operation.',
377
389
  ].join(' '),
378
390
  {
@@ -386,6 +398,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
386
398
  [
387
399
  'Alias for create_column. Add a column to an existing table through the canonical table_definition cascade.',
388
400
  'Use this for schema additions, including hidden secret fields with isPublished=false.',
401
+ 'Skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
389
402
  'Run schema changes sequentially — migration locks DB per operation.',
390
403
  ].join(' '),
391
404
  columnCreateSchema,
@@ -398,7 +411,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
398
411
  'update_column',
399
412
  [
400
413
  'Update an existing column on a table via PATCH /table_definition/{tableId}.',
401
- 'Fetches all columns, modifies the target column, and PATCHes the table.',
414
+ 'Fetches table columns, keeps only persisted rows with id/_id, modifies the target column, and PATCHes the table.',
415
+ 'Generated metadata projections such as createdAt, updatedAt, or relation-derived FK display fields without id are skipped.',
402
416
  'Run schema changes sequentially — migration locks DB per operation.',
403
417
  ].join(' '),
404
418
  {
@@ -418,9 +432,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
418
432
  return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
419
433
  }
420
434
 
421
- const columns = (tableData.columns || []).map(col => {
435
+ const columns = (tableData.columns || []).filter((col) => getId(col) !== null).map(col => {
422
436
  const rest = normalizeColumnForTablePatch(col);
423
- if (String(col.id) === String(columnId)) {
437
+ if (String(getId(col)) === String(columnId)) {
424
438
  if (name !== undefined) rest.name = name;
425
439
  if (type !== undefined) rest.type = type;
426
440
  if (isNullable !== undefined) rest.isNullable = isNullable;
@@ -446,7 +460,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
446
460
  'delete_column',
447
461
  [
448
462
  'Delete a column from a table via PATCH /table_definition/{tableId}.',
449
- 'Fetches all columns, removes the target, and PATCHes the table.',
463
+ 'Fetches table columns, keeps only persisted rows with id/_id, removes the target, and PATCHes the table.',
450
464
  'The physical column is dropped from the database. System columns (id, createdAt, updatedAt) cannot be deleted.',
451
465
  'Run schema changes sequentially — migration locks DB per operation.',
452
466
  ].join(' '),
@@ -461,6 +475,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
461
475
  [
462
476
  'Alias for delete_column. Remove a column through the canonical table_definition cascade.',
463
477
  'This drops the physical column. Confirm destructive schema changes before calling.',
478
+ 'Skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
464
479
  'Run schema changes sequentially — migration locks DB per operation.',
465
480
  ].join(' '),
466
481
  columnDeleteSchema,
@@ -1283,7 +1283,8 @@ server.tool(
1283
1283
 
1284
1284
  await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1285
1285
 
1286
- return { content: [{ type: 'text', text: `Route created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1286
+ const created = firstDataRecord(result);
1287
+ return { content: [{ type: 'text', text: `Route created (ID: ${getId(created)}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1287
1288
  },
1288
1289
  );
1289
1290
 
@@ -1338,7 +1339,7 @@ server.tool(
1338
1339
  {
1339
1340
  routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
1340
1341
  name: z.string().describe('Hook name (unique per route)'),
1341
- code: z.string().describe('Hook JavaScript code'),
1342
+ code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
1342
1343
  methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1343
1344
  .describe('Methods this hook applies to. Default: all REST methods.'),
1344
1345
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
@@ -1353,7 +1354,8 @@ server.tool(
1353
1354
  body: JSON.stringify({
1354
1355
  route: { id: routeId },
1355
1356
  name,
1356
- code,
1357
+ sourceCode: code,
1358
+ scriptLanguage: 'javascript',
1357
1359
  methods: resolveMethodIds(methodMap, methodNames),
1358
1360
  priority,
1359
1361
  isEnabled,
@@ -1362,7 +1364,8 @@ server.tool(
1362
1364
 
1363
1365
  await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1364
1366
 
1365
- return { content: [{ type: 'text', text: `Pre-hook "${name}" created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1367
+ const created = firstDataRecord(result);
1368
+ return { content: [{ type: 'text', text: `Pre-hook "${name}" created (ID: ${getId(created)}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1366
1369
  },
1367
1370
  );
1368
1371
 
@@ -1377,7 +1380,7 @@ server.tool(
1377
1380
  {
1378
1381
  routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
1379
1382
  name: z.string().describe('Hook name (unique per route)'),
1380
- code: z.string().describe('Hook JavaScript code'),
1383
+ code: z.string().describe('Hook JavaScript sourceCode. MCP stores it as sourceCode and lets Enfyra compile compiledCode.'),
1381
1384
  methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
1382
1385
  .describe('Methods this hook applies to. Default: all REST methods.'),
1383
1386
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
@@ -1392,7 +1395,8 @@ server.tool(
1392
1395
  body: JSON.stringify({
1393
1396
  route: { id: routeId },
1394
1397
  name,
1395
- code,
1398
+ sourceCode: code,
1399
+ scriptLanguage: 'javascript',
1396
1400
  methods: resolveMethodIds(methodMap, methodNames),
1397
1401
  priority,
1398
1402
  isEnabled,
@@ -1401,7 +1405,8 @@ server.tool(
1401
1405
 
1402
1406
  await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
1403
1407
 
1404
- return { content: [{ type: 'text', text: `Post-hook "${name}" created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1408
+ const created = firstDataRecord(result);
1409
+ return { content: [{ type: 'text', text: `Post-hook "${name}" created (ID: ${getId(created)}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
1405
1410
  },
1406
1411
  );
1407
1412
 
@@ -1691,7 +1696,7 @@ server.tool('get_all_roles', 'Get all role definitions', {}, async () => {
1691
1696
  });
1692
1697
 
1693
1698
  server.tool('login', 'Force authentication to Enfyra and get a new access token', {
1694
- apiToken: z.string().optional().describe('API token; preferred for MCP and automation'),
1699
+ apiToken: z.string().optional().describe('API token for MCP and automation'),
1695
1700
  }, async ({ apiToken }) => {
1696
1701
  const token = apiToken || ENFYRA_API_TOKEN;
1697
1702
  if (token) {
@@ -1825,7 +1830,8 @@ server.tool('create_menu', 'Create a menu item in the navigation', {
1825
1830
  body.path = '/' + body.path;
1826
1831
  }
1827
1832
  const result = await fetchAPI(ENFYRA_API_URL, '/menu_definition', { method: 'POST', body: JSON.stringify(body) });
1828
- return { content: [{ type: 'text', text: `Menu created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
1833
+ const created = firstDataRecord(result);
1834
+ return { content: [{ type: 'text', text: `Menu created (ID: ${getId(created)}):\n${JSON.stringify(result, null, 2)}` }] };
1829
1835
  });
1830
1836
 
1831
1837
  server.tool(
@@ -1850,7 +1856,8 @@ server.tool(
1850
1856
  delete body.menuId;
1851
1857
  }
1852
1858
  const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(body) });
1853
- return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}). Open eApp tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
1859
+ const created = firstDataRecord(result);
1860
+ return { content: [{ type: 'text', text: `Extension created (ID: ${getId(created)}). Open eApp tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
1854
1861
  },
1855
1862
  );
1856
1863