@enfyra/mcp-server 0.0.66 → 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.66",
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",
@@ -999,7 +999,7 @@ create_extension({
999
999
  name: "ReportsPage",
1000
1000
  description: "Reports dashboard",
1001
1001
  menuId: "<created-menu-id>",
1002
- code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-3\\"><UCard><p class=\\"text-sm text-muted\\">Total</p><p class=\\"mt-2 text-2xl font-semibold\\">0</p></UCard></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); useHeaderActionRegistry([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 }])</script>",
1002
+ code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-3\\"><UCard><p class=\\"text-sm text-muted\\">Total</p><p class=\\"mt-2 text-2xl font-semibold\\">0</p></UCard></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); const { register: registerHeaderActions } = useHeaderActionRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); registerHeaderActions([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 }])</script>",
1003
1003
  isEnabled: true
1004
1004
  })`,
1005
1005
  notes: [
@@ -1010,7 +1010,7 @@ create_extension({
1010
1010
  'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
1011
1011
  'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
1012
1012
  'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
1013
- 'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry.',
1013
+ 'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
1014
1014
  'Page extensions should be full-bleed by default and responsive from the first version.',
1015
1015
  'The extension root is already inside eApp main; do not add root-level page padding.',
1016
1016
  '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.',
@@ -1076,10 +1076,113 @@ 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>
1082
1184
  const { registerPageHeader } = usePageHeaderRegistry()
1185
+ const { register: registerHeaderActions } = useHeaderActionRegistry()
1083
1186
 
1084
1187
  registerPageHeader({
1085
1188
  title: 'Report detail',
@@ -1089,7 +1192,7 @@ registerPageHeader({
1089
1192
  variant: 'minimal'
1090
1193
  })
1091
1194
 
1092
- useHeaderActionRegistry([
1195
+ registerHeaderActions([
1093
1196
  {
1094
1197
  id: 'back-to-reports',
1095
1198
  label: 'Reports',
@@ -1142,8 +1245,9 @@ useHeaderActionRegistry([
1142
1245
  // await fetch menus
1143
1246
  // rebuild menu registry with reset: true
1144
1247
  // invalidate dynamic extension cache too, because route-to-extension mapping may change
1145
- // if reload target is extension/menu or extension/global:
1248
+ // if reload target is extension or menu:
1146
1249
  // clear dynamic extension component/meta cache
1250
+ // reload enabled type="global" shell extensions
1147
1251
 
1148
1252
  // Verification pattern:
1149
1253
  // 1. Save the menu or extension record.
@@ -311,16 +311,17 @@ 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.',
318
318
  '- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, capacity totals, 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.',
319
- '- **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 report", permission: { and: [{ route: "/report_definition", methods: ["POST"] }] }, onClick }`.',
319
+ '- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Destructure `register` first, then call it with one action or an array, for example `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id: "create", label: "Create report", permission: { and: [{ route: "/report_definition", methods: ["POST"] }] }, onClick }])`. Sensitive registry actions must include a `permission` condition.',
320
320
  '- **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.',
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.',
@@ -356,8 +357,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
356
357
  '- **useGlobalState:** Global app state. Returns `{ settings, storageConfigs, aiConfigs, appPackages, sidebarVisible, sidebarCollapsed, routeLoading, toggleSidebar(), setRouteLoading(), fetchAppPackages(), packageCacheState }`.',
357
358
  '- **useScreen:** Responsive helpers. Returns `{ width, height, isMobile, isTablet, isDesktop, isLargeDesktop, screenType }`.',
358
359
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
359
- '- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ 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
- '- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
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.',
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.',
@@ -407,7 +410,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
407
410
  '',
408
411
  '#### Important patterns:',
409
412
  '- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
410
- '- **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 }])`',
413
+ '- **Header actions:** `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ 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 }])`',
411
414
  '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
412
415
  '- **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.',
413
416
  '- **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.',
@@ -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;