@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 +1 -1
- package/src/lib/mcp-examples.js +114 -4
- package/src/lib/mcp-instructions.js +5 -3
- package/src/lib/table-tools.js +22 -7
- package/src/mcp-server-entry.mjs +17 -10
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -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: `
|
|
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: `
|
|
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 }`, `
|
|
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: \'
|
|
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.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -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
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -1283,7 +1283,8 @@ server.tool(
|
|
|
1283
1283
|
|
|
1284
1284
|
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
1285
1285
|
|
|
1286
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|