@enfyra/mcp-server 0.0.67 → 0.0.68

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.67",
3
+ "version": "0.0.68",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1076,6 +1076,108 @@ create_extension({
1076
1076
  'eApp batch-fetches widget metadata requested in the same tick and caches loaded widgets, so render Widget components directly instead of manually fetching widget code.',
1077
1077
  ],
1078
1078
  },
1079
+ {
1080
+ name: 'Create a global shell extension for app-wide notifications',
1081
+ code: `const notificationBellCode = \`
1082
+ <template></template>
1083
+
1084
+ <script setup>
1085
+ const unread = ref(0)
1086
+
1087
+ const NotificationBell = defineComponent({
1088
+ name: 'NotificationBell',
1089
+ setup() {
1090
+ const openNotifications = () => navigateTo('/notifications')
1091
+ return () => h(UButton, {
1092
+ type: 'button',
1093
+ icon: unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell',
1094
+ color: unread.value > 0 ? 'primary' : 'neutral',
1095
+ variant: unread.value > 0 ? 'solid' : 'ghost',
1096
+ label: unread.value > 0 ? String(unread.value) : undefined,
1097
+ onClick: openNotifications,
1098
+ })
1099
+ },
1100
+ })
1101
+
1102
+ const { register } = useAccountPanelRegistry()
1103
+ register({
1104
+ id: 'notifications',
1105
+ order: 20,
1106
+ component: NotificationBell,
1107
+ })
1108
+
1109
+ const { adminSocket } = useAdminSocket()
1110
+ const handleNotification = (payload) => {
1111
+ if (payload?.unread != null) unread.value = payload.unread
1112
+ }
1113
+ adminSocket.on('notification:summary', handleNotification)
1114
+ onUnmounted(() => {
1115
+ adminSocket.off('notification:summary', handleNotification)
1116
+ })
1117
+ </script>
1118
+ \`
1119
+
1120
+ create_extension({
1121
+ type: "global",
1122
+ name: "NotificationBellGlobal",
1123
+ description: "Registers the app-wide notification bell in the account panel",
1124
+ code: notificationBellCode,
1125
+ isEnabled: true
1126
+ })`,
1127
+ notes: [
1128
+ 'Global extensions are mounted invisibly by eApp during layout init; do not create a menu and do not embed them with Widget.',
1129
+ 'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
1130
+ '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, keep the control compact: icon left, label/microcopy center, count/status badge right, one click target, and no floating card around it.',
1132
+ 'Destructure registry functions and register stable ids so reloads replace the same shell item predictably.',
1133
+ 'Remove socket or DOM listeners in onUnmounted; eApp unmounts old global components when extension cache reloads or the extension is disabled.',
1134
+ ],
1135
+ },
1136
+ {
1137
+ name: 'Design a global account-panel item with proper shell UI',
1138
+ code: `<script setup>
1139
+ const unread = ref(3)
1140
+
1141
+ const NotificationPanelItem = defineComponent({
1142
+ name: 'NotificationPanelItem',
1143
+ setup() {
1144
+ const openNotifications = () => navigateTo('/notifications')
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
+ ])
1163
+ },
1164
+ })
1165
+
1166
+ const { register } = useAccountPanelRegistry()
1167
+ register({
1168
+ id: 'notifications',
1169
+ order: 20,
1170
+ component: NotificationPanelItem,
1171
+ })
1172
+ </script>`,
1173
+ notes: [
1174
+ 'Use this shape for shell/account-panel items: one row, aligned icon, readable label, short secondary text, and a compact trailing badge or chevron.',
1175
+ 'Do not use page-scale cards, hero headings, large whitespace, or a separate modal just to expose a shell entry.',
1176
+ 'Use `rounded-lg` or smaller radii, controlled padding, and the shell tokens/classes (`bg-muted`, `text-muted`, `text-highlighted`) so the item matches eApp.',
1177
+ 'Keep the entire row as one button with `type="button"`; avoid nested buttons inside account-panel rows.',
1178
+ 'Register the component from a `type="global"` extension, not from a page extension, when it must appear everywhere.',
1179
+ ],
1180
+ },
1079
1181
  {
1080
1182
  name: 'Page header and action button variants',
1081
1183
  code: `<script setup>
@@ -1143,8 +1245,9 @@ registerHeaderActions([
1143
1245
  // await fetch menus
1144
1246
  // rebuild menu registry with reset: true
1145
1247
  // invalidate dynamic extension cache too, because route-to-extension mapping may change
1146
- // if reload target is extension/menu or extension/global:
1248
+ // if reload target is extension or menu:
1147
1249
  // clear dynamic extension component/meta cache
1250
+ // reload enabled type="global" shell extensions
1148
1251
 
1149
1252
  // Verification pattern:
1150
1253
  // 1. Save the menu or extension record.
@@ -311,7 +311,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
311
311
  '- **Aggregate numeric rule:** `sum` and `avg` require a numeric field in ESV. Do not aggregate money stored as varchar/text. Use a numeric money field such as `amount_usd` with type `float`, `amount_cents`, or `amount` for revenue stats, or build a dedicated stats route that normalizes legacy values explicitly. If metadata says `float` but SQL aggregate still fails with `sum(character varying)`, the Enfyra Server physical schema is stale or missing the SQL float DDL mapping and must be redeployed/healed before relying on aggregate.',
312
312
  '- **Snapshot migrations:** backend metadata/physical schema renames belong in `data/snapshot-migration.json` via table-driven `columnsToModify` entries. The server migration/self-heal path should read table name plus `oldName`/`newName` dynamically; do not hard-code one-off table repairs when the snapshot migration contract can express the change.',
313
313
  '- **Partial reload default:** ESV/ASV automatically triggers partial reloads for metadata, routes, menus, extensions, flows, handlers, and related caches after successful writes. Do not reflexively call `/admin/reload`, `/admin/reload/metadata`, or `/admin/reload/routes` after each change. Verify naturally first; use manual reload only when verification shows stale behavior, a reload event failed, or a concrete error indicates the partial reload did not apply.',
314
- '- **Menu/extension realtime reload contract:** `menu_definition` and `extension_definition` writes are runtime UI changes, not plain CRUD. The server cache orchestrator must emit `$system:reload` through the admin Socket.IO channel with identifiers that eApp handles; eApp must refetch menus/rebuild the menu registry for menu reloads and invalidate dynamic extension caches for extension reloads. Menu reloads can change route-to-extension mapping, so they should also invalidate extension cache. If an open admin tab does not reflect menu/extension changes, debug this two-sided reload contract before telling the user to refresh.',
314
+ '- **Menu/extension realtime reload contract:** `menu_definition` and `extension_definition` writes are runtime UI changes, not plain CRUD. The server cache orchestrator must emit `$system:reload` through the admin Socket.IO channel with identifiers that eApp handles; eApp must refetch menus/rebuild the menu registry for menu reloads, invalidate dynamic extension caches for extension reloads, and reload enabled `type="global"` shell extensions when extension/menu metadata changes. Menu reloads can change route-to-extension mapping, so they should also invalidate extension cache. If an open admin tab does not reflect menu/extension changes, debug this two-sided reload contract before telling the user to refresh.',
315
315
  '- **Dashboard stats:** time range buttons must change the query filter and reload stats. Dashboards should summarize actionable errors and high-level activity; successful/no-error background runs usually do not need a standalone page unless there is a real workflow to manage.',
316
316
  '- **Page layout default:** page extensions should render full-bleed inside the app shell by default. The extension root is already inside the eApp page `<main>`, so do not add root-level page padding such as `p-4 sm:p-6 xl:p-8`; use spacing between internal sections only. Do not wrap the entire page in a centered card/container unless explicitly requested. Use responsive grids/stacks from the first version so the page works on desktop, tablet, and mobile.',
317
317
  '- **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.',
@@ -321,6 +321,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
321
321
  '- **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
322
  '- **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
323
  '- **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.',
324
325
  '- **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.',
325
326
  '- **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.',
326
327
  '- **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.',
@@ -358,6 +359,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
358
359
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
359
360
  '- **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.',
360
361
  '- **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. 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.',
361
363
  '- **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).',
362
364
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
363
365
  '- **useMenuApi:** Low-level menu API.',
@@ -388,6 +390,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
388
390
  '- **FormEditor field-map:** Customize fields via `:field-map`. Options: `label`, `description`, `hideLabel`, `hideDescription`, `component`, `componentProps`, `type`, `disabled`, `placeholder`, `permission`, `excludedOptions`/`includedOptions`, `fieldProps` (e.g. grid `class: \'md:col-span-2\'` when `layout=\'grid\'`), `booleanWrapperClass`, `fieldWrapperClass`. Optional `:sections` — array of `{ id, title?, hideHeading?, headingClass?, class?, rootClass?, fields: string[] }`; field order follows `fields`; unlisted columns render after. Custom input component: `modelValue` / `update:modelValue`.',
389
391
  '- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `menu_definition`), find by path/label, then create extension with `menu: { id: menuId }`. `menu_definition` uses **label** not name — filter by `label` or `path`. Sensitive/admin menus should pass a `permission` JSON object string to `create_menu` so visibility is permission-gated from creation.',
390
392
  '- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
393
+ '- **type "global":** Global shell extension. No menu required and not embedded manually; eApp fetches all enabled global extensions during layout init and mounts them invisibly with normal Vue lifecycle. Use for global notification/account-panel/realtime registration code, not for route content. Keep `<template></template>` empty or minimal hidden markup, and keep visible UI in registered components or existing shell slots.',
391
394
  '- **Widget composition:** split large page extensions into widget extensions for bulky or reusable sections such as operation panels, status cards, timelines, tables, and sidebars. Keep tiny one-off markup in the page. Create/update the widget `extension_definition` rows first, then embed their ids from the page extension.',
392
395
  '- **Widget props/events:** the `Widget` wrapper forwards non-`id` attrs and listeners to the rendered widget component. Widget extensions can declare `defineProps` and `defineEmits`; parent prop changes are reactive like normal Vue component props. Use normal Vue syntax such as `<Widget :id="123" :project-id="projectId" :history-rows="historyRows" @refresh="refreshAll" />`; kebab-case attributes map to camelCase props.',
393
396
  '- **Widget state boundary:** do not mutate props inside widgets. Derive display state with `computed`, or mirror a prop into local mutable state with `watch(() => props.value, ...)` when the widget owns an editor draft. Prefer emitted events for child-to-parent actions; use callback function props only for parent-owned modal/drawer openers or imperative actions, and guard with `typeof action === "function"` inside the widget.',
@@ -432,7 +435,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
432
435
  `- \`update_record\` → PATCH \`${base}/<tableName>/<id>\` (optional tool queryParams append URL query)`,
433
436
  `- \`update_script_source\` → validates sourceCode with \`${base}/admin/script/validate\`, then PATCHes \`${base}/<script_table>/<id>\``,
434
437
  `- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\` after preview + confirm=true (optional tool queryParams append URL query)`,
435
- `- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for page pass menuId). \`update_record\` on extension_definition to change code.`,
438
+ `- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for \`type="page"\` pass menuId, for \`type="widget"\` or \`type="global"\` omit menuId). \`update_record\` on extension_definition to change code.`,
436
439
  `- Flow tables: \`${base}/flow_definition\`, \`${base}/flow_step_definition\`, \`${base}/flow_execution_definition\` — use standard CRUD tools.`,
437
440
  `- \`run_admin_test\` → POST \`${base}/admin/test/run\``,
438
441
  `- \`test_flow_step\` → POST \`${base}/admin/flow/test-step\``,
@@ -223,15 +223,18 @@ export function buildColumnDefinition({
223
223
  description,
224
224
  options,
225
225
  }) {
226
- const column = { name, type };
227
- if (isNullable !== undefined) column.isNullable = isNullable;
226
+ const column = {
227
+ name,
228
+ type,
229
+ isNullable: isNullable ?? true,
230
+ isPrimary: isPrimary ?? false,
231
+ isGenerated: isGenerated ?? false,
232
+ isSystem: isSystem ?? false,
233
+ isPublished: isPublished ?? true,
234
+ isUpdatable: isUpdatable ?? true,
235
+ isEncrypted: isEncrypted ?? false,
236
+ };
228
237
  if (isUnique !== undefined) column.isUnique = isUnique;
229
- if (isPublished !== undefined) column.isPublished = isPublished;
230
- if (isUpdatable !== undefined) column.isUpdatable = isUpdatable;
231
- if (isEncrypted !== undefined) column.isEncrypted = isEncrypted;
232
- if (isPrimary !== undefined) column.isPrimary = isPrimary;
233
- if (isGenerated !== undefined) column.isGenerated = isGenerated;
234
- if (isSystem !== undefined) column.isSystem = isSystem;
235
238
  if (defaultValue !== undefined) column.defaultValue = defaultValue;
236
239
  if (description !== undefined) column.description = description;
237
240
  if (options !== undefined) column.options = JSON.parse(options);
@@ -2404,20 +2404,26 @@ server.tool('create_menu', 'Create a menu item in the navigation. Use permission
2404
2404
  server.tool(
2405
2405
  'create_extension',
2406
2406
  [
2407
- 'Create an extension (Vue SFC page or widget). Code must be Vue SFC: <template>...</template> + <script setup>...</script> — NO imports, use globals (ref, useToast, useApi, UButton, etc).',
2408
- 'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget no menu needed. Server auto-compiles and should emit realtime reload to open eApp tabs. See extension rules in MCP instructions.',
2407
+ 'Create an extension (Vue SFC page, widget, or global shell extension). Code must be Vue SFC: <template>...</template> + <script setup>...</script> — NO imports, use globals (ref, useToast, useApi, UButton, etc).',
2408
+ 'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget: no menu, embed via <Widget>. For type=global: no menu, eApp mounts it invisibly at shell level for registries/realtime. Server auto-compiles and should emit realtime reload to open eApp tabs. See extension rules in MCP instructions.',
2409
2409
  ].join(' '),
2410
2410
  {
2411
2411
  name: z.string().describe('Extension name (unique)'),
2412
- type: z.enum(['page', 'widget']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component'),
2412
+ type: z.enum(['page', 'widget', 'global']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component; global = shell-level lifecycle component'),
2413
2413
  code: z.string().describe('Vue SFC string — <template> + <script setup>, NO import statements'),
2414
- menuId: z.string().optional().describe('Required for type=page — menu_definition id from create_menu. Omit for widget'),
2414
+ menuId: z.string().optional().describe('Required for type=page — menu_definition id from create_menu. Omit for widget/global'),
2415
2415
  isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
2416
2416
  description: z.string().optional().describe('Extension description'),
2417
2417
  version: z.string().optional().default('1.0.0').describe('Extension version'),
2418
2418
  },
2419
2419
  async (data) => {
2420
2420
  const body = { ...data };
2421
+ if (body.type === 'page' && !body.menuId) {
2422
+ throw new Error('menuId is required for type=page. Create or find a menu_definition record first.');
2423
+ }
2424
+ if (body.type !== 'page' && body.menuId) {
2425
+ throw new Error('menuId is only valid for type=page. Omit menuId for widget/global extensions.');
2426
+ }
2421
2427
  if (body.menuId) {
2422
2428
  body.menu = { id: body.menuId };
2423
2429
  delete body.menuId;