@enfyra/mcp-server 0.0.70 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.70",
3
+ "version": "0.0.71",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.',
@@ -1101,19 +1102,28 @@ create_extension({
1101
1102
 
1102
1103
  <script setup>
1103
1104
  const unread = ref(0)
1105
+ const expanded = ref(false)
1104
1106
 
1105
- const NotificationBell = defineComponent({
1106
- name: 'NotificationBell',
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',
1107
1116
  setup() {
1108
- const openNotifications = () => navigateTo('/notifications')
1109
- return () => h(UButton, {
1110
- type: 'button',
1111
- icon: unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell',
1112
- color: unread.value > 0 ? 'primary' : 'neutral',
1113
- variant: unread.value > 0 ? 'solid' : 'ghost',
1114
- label: unread.value > 0 ? String(unread.value) : undefined,
1115
- onClick: openNotifications,
1116
- })
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
+ ])
1117
1127
  },
1118
1128
  })
1119
1129
 
@@ -1121,7 +1131,16 @@ const { register } = useAccountPanelRegistry()
1121
1131
  register({
1122
1132
  id: 'notifications',
1123
1133
  order: 20,
1124
- component: NotificationBell,
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,
1125
1144
  })
1126
1145
 
1127
1146
  const { adminSocket } = useAdminSocket()
@@ -1146,38 +1165,27 @@ create_extension({
1146
1165
  'Global extensions are mounted invisibly by eApp during layout init; do not create a menu and do not embed them with Widget.',
1147
1166
  'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
1148
1167
  'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
1149
- 'For account-panel UI, keep the control compact: icon left, label/microcopy center, count/status badge right, one click target, and no floating card around it.',
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.',
1150
1170
  'Destructure registry functions and register stable ids so reloads replace the same shell item predictably.',
1151
1171
  'Remove socket or DOM listeners in onUnmounted; eApp unmounts old global components when extension cache reloads or the extension is disabled.',
1152
1172
  ],
1153
1173
  },
1154
1174
  {
1155
- name: 'Design a global account-panel item with proper shell UI',
1175
+ name: 'Register a data-driven account-panel item',
1156
1176
  code: `<script setup>
1157
1177
  const unread = ref(3)
1178
+ const expanded = ref(false)
1158
1179
 
1159
- const NotificationPanelItem = defineComponent({
1160
- name: 'NotificationPanelItem',
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',
1161
1187
  setup() {
1162
- const openNotifications = () => navigateTo('/notifications')
1163
- return () => h('button', {
1164
- type: 'button',
1165
- class: 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition hover:bg-muted',
1166
- onClick: openNotifications,
1167
- }, [
1168
- h('span', {
1169
- class: 'flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary',
1170
- }, [
1171
- h(UIcon, { name: unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell', class: 'h-5 w-5' }),
1172
- ]),
1173
- h('span', { class: 'min-w-0 flex-1' }, [
1174
- h('span', { class: 'block truncate text-sm font-medium text-highlighted' }, 'Notifications'),
1175
- h('span', { class: 'mt-0.5 block truncate text-xs text-muted' }, unread.value > 0 ? 'Needs review' : 'All caught up'),
1176
- ]),
1177
- unread.value > 0
1178
- ? h(UBadge, { color: 'primary', variant: 'soft', size: 'sm' }, () => String(unread.value))
1179
- : h(UIcon, { name: 'lucide:chevron-right', class: 'h-4 w-4 text-muted' }),
1180
- ])
1188
+ return () => h('div', { class: 'px-2 py-1 text-xs text-[var(--text-tertiary)]' }, 'Recent unread notifications can render here.')
1181
1189
  },
1182
1190
  })
1183
1191
 
@@ -1185,14 +1193,23 @@ const { register } = useAccountPanelRegistry()
1185
1193
  register({
1186
1194
  id: 'notifications',
1187
1195
  order: 20,
1188
- component: NotificationPanelItem,
1196
+ label,
1197
+ icon,
1198
+ description,
1199
+ badge,
1200
+ badgeColor: 'error',
1201
+ expanded,
1202
+ onToggle: () => {
1203
+ expanded.value = !expanded.value
1204
+ },
1205
+ contentComponent: NotificationPanelContent,
1189
1206
  })
1190
1207
  </script>`,
1191
1208
  notes: [
1192
- 'Use this shape for shell/account-panel items: one row, aligned icon, readable label, short secondary text, and a compact trailing badge or chevron.',
1193
- 'Do not use page-scale cards, hero headings, large whitespace, or a separate modal just to expose a shell entry.',
1194
- 'Use `rounded-lg` or smaller radii, controlled padding, and the shell tokens/classes (`bg-muted`, `text-muted`, `text-highlighted`) so the item matches eApp.',
1195
- 'Keep the entire row as one button with `type="button"`; avoid nested buttons inside account-panel rows.',
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.',
1196
1213
  'Register the component from a `type="global"` extension, not from a page extension, when it must appear everywhere.',
1197
1214
  ],
1198
1215
  },
@@ -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` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
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.',
@@ -323,7 +323,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
323
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.',
324
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.',
325
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.*`.',
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.',
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.',
327
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.',
328
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.',
329
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.',
@@ -361,7 +361,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
361
361
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
362
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.',
363
363
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header: destructure `register` first, then call it with one action or an array.',
364
- '- **useAccountPanelRegistry:** Register rows in the sidebar account panel. Use `const { register } = useAccountPanelRegistry(); register({ id, order, component, props })` or pass an array. The caller lifecycle unregisters rows automatically, so global extensions can register notification/account controls safely.',
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.',
365
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).',
366
366
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
367
367
  '- **useMenuApi:** Low-level menu API.',
@@ -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 {id}.',
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 or {"id": <id>}. 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"}]'),
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 userRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
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 };