@enfyra/mcp-server 0.0.69 → 0.0.71
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 +75 -40
- package/src/lib/mcp-instructions.js +6 -4
- package/src/lib/table-tools.js +17 -3
- package/src/mcp-server-entry.mjs +1 -1
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -218,6 +218,7 @@ window.location.href = url.toString()`,
|
|
|
218
218
|
})`,
|
|
219
219
|
notes: [
|
|
220
220
|
'Use user_definition as the user table.',
|
|
221
|
+
'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "user_definition" before schema mutation.',
|
|
221
222
|
'Do not add inverse relations on user_definition unless the user explicitly asks.',
|
|
222
223
|
'createdAt, updatedAt, and custom date/datetime/timestamp fields already get auto-generated single-field indexes; add only compound indexes needed by hot filters.',
|
|
223
224
|
'Do not provide physical FK column names; Enfyra derives them.',
|
|
@@ -890,6 +891,24 @@ return order && order.total > 1000`,
|
|
|
890
891
|
'Children run according to branch true/false.',
|
|
891
892
|
],
|
|
892
893
|
},
|
|
894
|
+
{
|
|
895
|
+
name: 'Split a provisioning workflow into focused steps',
|
|
896
|
+
code: `[
|
|
897
|
+
{ "key": "load_project", "stepOrder": 10, "type": "script" },
|
|
898
|
+
{ "key": "reserve_capacity", "stepOrder": 20, "type": "script" },
|
|
899
|
+
{ "key": "create_database_user", "stepOrder": 30, "type": "script" },
|
|
900
|
+
{ "key": "apply_database_guardrails", "stepOrder": 40, "type": "script" },
|
|
901
|
+
{ "key": "start_container", "stepOrder": 50, "type": "script" },
|
|
902
|
+
{ "key": "apply_container_guardrails", "stepOrder": 60, "type": "script" },
|
|
903
|
+
{ "key": "check_health", "stepOrder": 70, "type": "script" },
|
|
904
|
+
{ "key": "finalize_project", "stepOrder": 80, "type": "script" }
|
|
905
|
+
]`,
|
|
906
|
+
notes: [
|
|
907
|
+
'Prefer operation-sized flow steps with clear keys over one large script that performs SSH, Docker, DB, API, email, and finalization work together.',
|
|
908
|
+
'Each step should return only ids, booleans, status keys, or small counters that later steps need.',
|
|
909
|
+
'When refactoring an existing flow, add or extract adjacent focused flow_step_definition rows instead of making an oversized sourceCode block longer.',
|
|
910
|
+
],
|
|
911
|
+
},
|
|
893
912
|
{
|
|
894
913
|
name: 'Flow query step config',
|
|
895
914
|
code: `{
|
|
@@ -1083,19 +1102,28 @@ create_extension({
|
|
|
1083
1102
|
|
|
1084
1103
|
<script setup>
|
|
1085
1104
|
const unread = ref(0)
|
|
1105
|
+
const expanded = ref(false)
|
|
1086
1106
|
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1107
|
+
const notificationDescription = computed(() => {
|
|
1108
|
+
if (unread.value > 0) return unread.value === 1 ? '1 unread' : unread.value + ' unread'
|
|
1109
|
+
return 'All caught up'
|
|
1110
|
+
})
|
|
1111
|
+
const notificationBadge = computed(() => unread.value > 0 ? (unread.value > 99 ? '99+' : unread.value) : null)
|
|
1112
|
+
const notificationIcon = computed(() => unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell')
|
|
1113
|
+
|
|
1114
|
+
const NotificationList = defineComponent({
|
|
1115
|
+
name: 'NotificationList',
|
|
1089
1116
|
setup() {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1117
|
+
return () => h('div', { class: 'p-2 text-sm' }, [
|
|
1118
|
+
h('button', {
|
|
1119
|
+
type: 'button',
|
|
1120
|
+
class: 'flex w-full items-center justify-between rounded px-2 py-2 text-left hover:bg-[var(--surface-muted)]',
|
|
1121
|
+
onClick: () => navigateTo('/notifications'),
|
|
1122
|
+
}, [
|
|
1123
|
+
h('span', 'Open notification center'),
|
|
1124
|
+
h(resolveComponent('UIcon'), { name: 'lucide:arrow-right', class: 'h-4 w-4' }),
|
|
1125
|
+
]),
|
|
1126
|
+
])
|
|
1099
1127
|
},
|
|
1100
1128
|
})
|
|
1101
1129
|
|
|
@@ -1103,7 +1131,16 @@ const { register } = useAccountPanelRegistry()
|
|
|
1103
1131
|
register({
|
|
1104
1132
|
id: 'notifications',
|
|
1105
1133
|
order: 20,
|
|
1106
|
-
|
|
1134
|
+
label: 'Notifications',
|
|
1135
|
+
icon: notificationIcon,
|
|
1136
|
+
description: notificationDescription,
|
|
1137
|
+
badge: notificationBadge,
|
|
1138
|
+
badgeColor: 'error',
|
|
1139
|
+
expanded,
|
|
1140
|
+
onToggle: () => {
|
|
1141
|
+
expanded.value = !expanded.value
|
|
1142
|
+
},
|
|
1143
|
+
contentComponent: NotificationList,
|
|
1107
1144
|
})
|
|
1108
1145
|
|
|
1109
1146
|
const { adminSocket } = useAdminSocket()
|
|
@@ -1128,38 +1165,27 @@ create_extension({
|
|
|
1128
1165
|
'Global extensions are mounted invisibly by eApp during layout init; do not create a menu and do not embed them with Widget.',
|
|
1129
1166
|
'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
|
|
1130
1167
|
'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
|
|
1131
|
-
'For account-panel UI,
|
|
1168
|
+
'For account-panel UI, register data-driven row fields so eApp owns icon size, row spacing, badge placement, hover state, and expanded chrome.',
|
|
1169
|
+
'Use contentComponent only for expanded inner content; use raw component only as an escape hatch when the row cannot fit the shell contract.',
|
|
1132
1170
|
'Destructure registry functions and register stable ids so reloads replace the same shell item predictably.',
|
|
1133
1171
|
'Remove socket or DOM listeners in onUnmounted; eApp unmounts old global components when extension cache reloads or the extension is disabled.',
|
|
1134
1172
|
],
|
|
1135
1173
|
},
|
|
1136
1174
|
{
|
|
1137
|
-
name: '
|
|
1175
|
+
name: 'Register a data-driven account-panel item',
|
|
1138
1176
|
code: `<script setup>
|
|
1139
1177
|
const unread = ref(3)
|
|
1178
|
+
const expanded = ref(false)
|
|
1140
1179
|
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1180
|
+
const label = 'Notifications'
|
|
1181
|
+
const icon = computed(() => unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell')
|
|
1182
|
+
const badge = computed(() => unread.value > 0 ? String(unread.value) : null)
|
|
1183
|
+
const description = computed(() => unread.value > 0 ? 'Needs review' : 'All caught up')
|
|
1184
|
+
|
|
1185
|
+
const NotificationPanelContent = defineComponent({
|
|
1186
|
+
name: 'NotificationPanelContent',
|
|
1143
1187
|
setup() {
|
|
1144
|
-
|
|
1145
|
-
return () => h('button', {
|
|
1146
|
-
type: 'button',
|
|
1147
|
-
class: 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition hover:bg-muted',
|
|
1148
|
-
onClick: openNotifications,
|
|
1149
|
-
}, [
|
|
1150
|
-
h('span', {
|
|
1151
|
-
class: 'flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary',
|
|
1152
|
-
}, [
|
|
1153
|
-
h(UIcon, { name: unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell', class: 'h-5 w-5' }),
|
|
1154
|
-
]),
|
|
1155
|
-
h('span', { class: 'min-w-0 flex-1' }, [
|
|
1156
|
-
h('span', { class: 'block truncate text-sm font-medium text-highlighted' }, 'Notifications'),
|
|
1157
|
-
h('span', { class: 'mt-0.5 block truncate text-xs text-muted' }, unread.value > 0 ? 'Needs review' : 'All caught up'),
|
|
1158
|
-
]),
|
|
1159
|
-
unread.value > 0
|
|
1160
|
-
? h(UBadge, { color: 'primary', variant: 'soft', size: 'sm' }, () => String(unread.value))
|
|
1161
|
-
: h(UIcon, { name: 'lucide:chevron-right', class: 'h-4 w-4 text-muted' }),
|
|
1162
|
-
])
|
|
1188
|
+
return () => h('div', { class: 'px-2 py-1 text-xs text-[var(--text-tertiary)]' }, 'Recent unread notifications can render here.')
|
|
1163
1189
|
},
|
|
1164
1190
|
})
|
|
1165
1191
|
|
|
@@ -1167,14 +1193,23 @@ const { register } = useAccountPanelRegistry()
|
|
|
1167
1193
|
register({
|
|
1168
1194
|
id: 'notifications',
|
|
1169
1195
|
order: 20,
|
|
1170
|
-
|
|
1196
|
+
label,
|
|
1197
|
+
icon,
|
|
1198
|
+
description,
|
|
1199
|
+
badge,
|
|
1200
|
+
badgeColor: 'error',
|
|
1201
|
+
expanded,
|
|
1202
|
+
onToggle: () => {
|
|
1203
|
+
expanded.value = !expanded.value
|
|
1204
|
+
},
|
|
1205
|
+
contentComponent: NotificationPanelContent,
|
|
1171
1206
|
})
|
|
1172
1207
|
</script>`,
|
|
1173
1208
|
notes: [
|
|
1174
|
-
'
|
|
1175
|
-
'Do not
|
|
1176
|
-
'
|
|
1177
|
-
'Keep
|
|
1209
|
+
'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
|
|
1210
|
+
'Do not draw a custom full row with page-scale cards, hero headings, large whitespace, or nested buttons unless the shell contract cannot express the UI.',
|
|
1211
|
+
'Let eApp handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
|
|
1212
|
+
'Keep contentComponent compact; it is rendered inside account-panel chrome and should not create another large card around itself.',
|
|
1178
1213
|
'Register the component from a `type="global"` extension, not from a page extension, when it must appear everywhere.',
|
|
1179
1214
|
],
|
|
1180
1215
|
},
|
|
@@ -48,7 +48,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
48
48
|
'- **GraphQL:** `gql_definition` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
|
|
49
49
|
'- **Files/storage/assets:** `file_definition`, `file_permission_definition`, `folder_definition`, `storage_config_definition` plus upload/assets routes and file helpers.',
|
|
50
50
|
'- **WebSocket:** `websocket_definition` and `websocket_event_definition` define Socket.IO gateways/events. Use `run_admin_test` for websocket scripts.',
|
|
51
|
-
'- **Flows:** `flow_definition`, `flow_step_definition`, `flow_execution_definition`; use `test_flow_step`, `run_admin_test`, and `trigger_flow` for runtime checks.',
|
|
51
|
+
'- **Flows:** `flow_definition`, `flow_step_definition`, `flow_execution_definition`; compose workflows from small operation-sized steps, use `test_flow_step`, `run_admin_test`, and `trigger_flow` for runtime checks.',
|
|
52
52
|
'- **Extensions/packages/menus:** `extension_definition`, `menu_definition`, `package_definition`, `bootstrap_script_definition`; extensions are Vue SFC only, and packages should be installed with `install_package`.',
|
|
53
53
|
'- **Platform config:** `setting_definition`, `cors_origin_definition`, reload endpoints, logs, and metadata endpoints.',
|
|
54
54
|
'',
|
|
@@ -86,7 +86,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
86
86
|
'- MCP **`create_table` and `update_table` support `indexes` and `uniques`** as JSON arrays of logical field groups. Use compound indexes for hot filters and unread/read state, e.g. `indexes: [["member","isRead","conversation"],["conversation","member","isRead"]]` and `uniques: [["message","member"]]`. Relation property names are allowed; Enfyra resolves them to physical FK columns for SQL and Mongo.',
|
|
87
87
|
'- Enfyra auto-generates single-field indexes for `createdAt`, `updatedAt`, and scalar time columns with type `date`, `datetime`, or `timestamp`. SQL appends `id` as the stable tie-breaker, and Mongo appends `_id`. Do not add duplicate single-field indexes for these time fields; add explicit compound indexes only when a hot query combines the time field with other filters such as status, owner, tenant, or relation fields.',
|
|
88
88
|
'- MCP **`create_table` does not accept `alias`**. Do not invent or send alias during table creation; default route/schema behavior is based on `name`. Use `update_table` later only when alias truly needs to change.',
|
|
89
|
-
'- In `create_table.relations`, each relation uses `targetTable
|
|
89
|
+
'- In `create_table.relations`, each relation uses `targetTable`, `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. Prefer table id or `{id}` for `targetTable`; MCP also accepts an exact table name such as `"user_definition"` and resolves it to the live table id before mutation. The target table must already exist.',
|
|
90
90
|
'- **Use `user_definition` as the only user table.** Do not create app-specific user/profile mapping tables such as `chat_profile`, `app_user`, `customer_user`, or tables that only mirror/link Enfyra users. If an app needs extra user fields or user relations, add columns/relations directly on `user_definition`.',
|
|
91
91
|
'- When modeling features that involve users, relate domain tables directly to `user_definition` through real Enfyra relations. Examples: `chat_conversation_member.member` → `user_definition`, `chat_message.sender` → `user_definition`, `order.customer` → `user_definition`. Do not create duplicate scalar columns like `userId` or separate profile records just to point back to a user.',
|
|
92
92
|
'- **Do not create reverse relations on `user_definition` by default.** For domain records that point to users, create only the owning relation on the domain table, e.g. `chat_message.sender -> user_definition`, and omit `inversePropertyName` unless the user explicitly asks for a reverse user field. Reverse fields like `user_definition.chatMessages`, `user_definition.orders`, or `user_definition.memberships` bloat user metadata and make `fields=*` user queries heavy.',
|
|
@@ -288,8 +288,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
288
288
|
'- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. There is no persisted `context` column; failed executions store diagnostics in `error`. Query separately — NOT nested under flow_definition.',
|
|
289
289
|
'- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
|
|
290
290
|
'- **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}`.',
|
|
291
|
+
'- **Flow step sizing:** prefer many small, named steps over one large script. Split workflows at real operation boundaries such as load context, reserve capacity, create remote resource, apply DB/user guardrails, start container, check health, write final state, and send notifications. A script step that mixes unrelated SSH/Docker/DB/API/email/reconciliation work, returns huge objects, or grows beyond roughly 100-150 lines should be split before saving.',
|
|
291
292
|
'- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
|
|
292
293
|
'- **Flow step output discipline:** Script/condition steps must return only the small values that later steps genuinely need, such as ids, booleans, status keys, or counters. Do not return full records, host objects, package objects, DB URLs, SSH keys, API tokens, generated passwords, or other secrets. Later steps should re-query the records they need from ids/payload. Flow execution history already records errors; successful runs do not need full success snapshots.',
|
|
294
|
+
'- **Flow refactor rule:** when changing an existing oversized flow step, prefer adding or extracting adjacent focused `flow_step_definition` rows with clear keys and `stepOrder` instead of making the original `sourceCode` longer. Keep branch `parent`/`branch` relationships explicit, and re-read saved steps after mutation to verify order and executable source.',
|
|
293
295
|
'- **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)`.',
|
|
294
296
|
'- **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.',
|
|
295
297
|
'- **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.',
|
|
@@ -321,7 +323,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
321
323
|
'- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `method_definition`. The backend field is `method_definition.name`, unique per method; do not send `method_definition.method`. The eApp UI for the same records is `/settings/methods`. Method color fields are `buttonColor` for badge background and `textColor` for badge text, both full hex colors. Do not use generic `create_record` on `method_definition` unless the dedicated tool is unavailable.',
|
|
322
324
|
'- **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.',
|
|
323
325
|
'- **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/report_definition")`, because Vue compiles template helpers to `_ctx.*`.',
|
|
324
|
-
'- **Global extension lifecycle:** `extension_definition.type="global"` records are Vue SFC components mounted invisibly once at the eApp shell level. Use them for app-wide registrations such as account panel items, notification bells, admin socket listeners, and background refresh bridges. They do not need a menu, must not render page body UI, and should clean up socket/listener side effects with `onUnmounted`. Prefer registry composables such as `useAccountPanelRegistry`, `useHeaderActionRegistry`, or `useSubHeaderActionRegistry` instead of directly editing shell DOM.',
|
|
326
|
+
'- **Global extension lifecycle:** `extension_definition.type="global"` records are Vue SFC components mounted invisibly once at the eApp shell level. Use them for app-wide registrations such as account panel items, notification bells, admin socket listeners, and background refresh bridges. They do not need a menu, must not render page body UI, and should clean up socket/listener side effects with `onUnmounted`. Prefer registry composables such as `useAccountPanelRegistry`, `useHeaderActionRegistry`, or `useSubHeaderActionRegistry` instead of directly editing shell DOM. For account-panel entries, register data (`label`, `icon`, `description`, `badge`, `badgeColor`, `expanded`, `onToggle`, `contentComponent`) so eApp owns row spacing, icon sizing, badges, and expanded chrome; use a raw row `component` only for a true custom escape hatch.',
|
|
325
327
|
'- **Extension realtime:** admin extensions can use `useAdminSocket()` to listen to the shared admin Socket.IO client instead of creating a separate browser socket. Use it for backend admin events such as operational status changes, debounce refreshes, and unsubscribe with `socket.off(...)` in `onUnmounted`. Guard generated code with `typeof useAdminSocket === "function"` when the extension may run on an older eApp build.',
|
|
326
328
|
'- **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.',
|
|
327
329
|
'- **Admin record links:** when an admin extension links to backend records for management or inspection, point to eApp data routes such as `/data/report_definition` or `/data/order_definition`. Do not use public website paths from record fields unless the explicit intent is previewing the public website.',
|
|
@@ -359,7 +361,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
359
361
|
'- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
|
|
360
362
|
'- **useHeaderActionRegistry:** Register header actions. Use `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id, label, onClick, color, icon, order, side, global, permission }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component, permission }`; admin actions should set `permission` by default.',
|
|
361
363
|
'- **useSubHeaderActionRegistry:** Same as header but for sub-header: destructure `register` first, then call it with one action or an array.',
|
|
362
|
-
'- **useAccountPanelRegistry:** Register rows in the sidebar account panel.
|
|
364
|
+
'- **useAccountPanelRegistry:** Register rows in the sidebar account panel. Destructure `const { register } = useAccountPanelRegistry()` and call `register(itemOrItems)`. Prefer data-driven items: `{ id, order, label, icon, description, badge, badgeColor, trailingIcon, expanded, onToggle, contentComponent, contentProps }`. eApp renders the row chrome and lifecycle-unregisters the caller automatically. Use `contentComponent` only for expanded inner content, and use raw `component` only when the entire row truly cannot use the shell contract.',
|
|
363
365
|
'- **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).',
|
|
364
366
|
'- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
|
|
365
367
|
'- **useMenuApi:** Low-level menu API.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -144,6 +144,16 @@ export function normalizeRelationForTablePatch(relation) {
|
|
|
144
144
|
return normalized;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function resolveRelationTargetsFromMetadata(metadata, relations) {
|
|
148
|
+
return relations.map((relation) => {
|
|
149
|
+
const targetTable = relation.targetTable;
|
|
150
|
+
if (typeof targetTable !== 'string' || !targetTable.trim()) return relation;
|
|
151
|
+
const resolvedTable = resolveTableFromMetadataByName(metadata, targetTable);
|
|
152
|
+
if (!resolvedTable) return relation;
|
|
153
|
+
return { ...relation, targetTable: getId(resolvedTable) };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
function getId(record) {
|
|
148
158
|
return record?.id ?? record?._id ?? null;
|
|
149
159
|
}
|
|
@@ -450,7 +460,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
450
460
|
'**Not** for adding a custom API path or handler only — for that use **`create_route`** without `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
451
461
|
'PREFERRED: pass `columns` and `relations` params as JSON arrays to create a table WITH columns and relations in one call (cascade). Only use create_column/create_relation separately when adding to an existing table later.',
|
|
452
462
|
'Indexes and uniques are first-class table metadata. Use `indexes` for query performance and `uniques` for data integrity. Each entry is a logical field group such as [["member","isRead","conversation"]] or [{"value":["message","member"]}]. Relation property names are allowed; Enfyra resolves them to physical FK columns.',
|
|
453
|
-
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id or
|
|
463
|
+
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id, {id}, or an exact table name that MCP resolves to an id before mutation.',
|
|
454
464
|
'Do NOT provide physical FK/junction columns. Never include fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn. Enfyra derives and hides those physical columns from relation propertyName/table metadata.',
|
|
455
465
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
456
466
|
'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
@@ -466,14 +476,18 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
466
476
|
description: z.string().optional().describe('Description of what this table stores.'),
|
|
467
477
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to table_definition create.'),
|
|
468
478
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, defaultValue?, description?, options? }. Set isEncrypted=true for values encrypted at rest; set isUpdatable=false separately only when the field should be immutable. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"api_key","type":"varchar","isEncrypted":true,"isPublished":false}]'),
|
|
469
|
-
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id
|
|
479
|
+
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
470
480
|
indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
|
|
471
481
|
uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
|
|
472
482
|
},
|
|
473
483
|
async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
|
|
474
484
|
const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
|
|
475
485
|
const userColumns = parseJsonArrayParam('columns', columnsJson);
|
|
476
|
-
const
|
|
486
|
+
const parsedRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
|
|
487
|
+
const metadata = parsedRelations.length ? await fetchAPI(ENFYRA_API_URL, '/metadata') : null;
|
|
488
|
+
const userRelations = metadata
|
|
489
|
+
? resolveRelationTargetsFromMetadata(metadata, parsedRelations)
|
|
490
|
+
: parsedRelations;
|
|
477
491
|
const indexes = normalizeConstraintGroups('indexes', parseJsonArrayParam('indexes', indexesJson));
|
|
478
492
|
const uniques = normalizeConstraintGroups('uniques', parseJsonArrayParam('uniques', uniquesJson));
|
|
479
493
|
const body = { name, description, columns: [idColumn, ...userColumns], relations: userRelations };
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -64,7 +64,7 @@ const CAPABILITY_AREAS = [
|
|
|
64
64
|
{
|
|
65
65
|
area: 'Flows',
|
|
66
66
|
tables: ['flow_definition', 'flow_step_definition', 'flow_execution_definition'],
|
|
67
|
-
workflow: 'Create flows
|
|
67
|
+
workflow: 'Create flows as small operation-sized steps via CRUD, test steps with test_flow_step/run_admin_test, trigger with trigger_flow. Split oversized scripts instead of adding more work to one step.',
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
area: 'Extensions, menus, packages',
|