@enfyra/mcp-server 0.1.2 → 0.1.4

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.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1694,7 +1694,7 @@ register({
1694
1694
  label: 'Notifications',
1695
1695
  icon: notificationIcon,
1696
1696
  description: notificationDescription,
1697
- badge: notificationBadge,
1697
+ count: notificationBadge,
1698
1698
  badgeColor: 'error',
1699
1699
  expanded,
1700
1700
  onToggle: () => {
@@ -1703,6 +1703,21 @@ register({
1703
1703
  contentComponent: NotificationList,
1704
1704
  })
1705
1705
 
1706
+ const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
1707
+ watchEffect(() => {
1708
+ if (notificationBadge.value) {
1709
+ registerMenuNotification({
1710
+ id: 'notifications-menu-unread',
1711
+ target: { path: '/notifications' },
1712
+ value: notificationBadge.value,
1713
+ color: 'error',
1714
+ title: notificationDescription.value,
1715
+ })
1716
+ } else {
1717
+ unregisterMenuNotification('notifications-menu-unread')
1718
+ }
1719
+ })
1720
+
1706
1721
  const { adminSocket } = useAdminSocket()
1707
1722
  const handleNotification = (payload) => {
1708
1723
  if (payload?.unread != null) unread.value = payload.unread
@@ -1710,6 +1725,7 @@ const handleNotification = (payload) => {
1710
1725
  adminSocket.on('notification:summary', handleNotification)
1711
1726
  onUnmounted(() => {
1712
1727
  adminSocket.off('notification:summary', handleNotification)
1728
+ unregisterMenuNotification('notifications-menu-unread')
1713
1729
  })
1714
1730
  </script>
1715
1731
  \`
@@ -1724,6 +1740,9 @@ ensure_global_extension({
1724
1740
  notes: [
1725
1741
  'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
1726
1742
  'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
1743
+ 'Use useMenuNotificationRegistry for sidebar menu counts/dots when notification state should be visible in the menu as well as the notification center.',
1744
+ 'Choose value only when the signal source already owns an exact count. Omit value for a dot when realtime only proves that something new exists.',
1745
+ 'Do not fetch the destination domain list just to decorate a menu. A mail page fetches mail; a support page fetches tickets; the shell should use notification or summary signals.',
1727
1746
  'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
1728
1747
  'For account-panel UI, register data-driven row fields so Enfyra admin UI owns icon size, row spacing, badge placement, hover state, and expanded chrome.',
1729
1748
  'Use contentComponent only for expanded inner content; use raw component only as an escape hatch when the row cannot fit the shell contract.',
@@ -1731,6 +1750,159 @@ ensure_global_extension({
1731
1750
  'Remove socket or DOM listeners in onUnmounted; The Enfyra admin UI unmounts old global components when extension cache reloads or the extension is disabled.',
1732
1751
  ],
1733
1752
  },
1753
+ {
1754
+ name: 'Signal menu attention without polling destination lists',
1755
+ code: `const signalBridgeCode = \`
1756
+ <template></template>
1757
+
1758
+ <script setup>
1759
+ const attentionRows = ref([])
1760
+ const notificationSignal = ref(false)
1761
+ const route = useRoute()
1762
+
1763
+ const notificationApi = useApi('/cloud_admin_notifications', {
1764
+ query: {
1765
+ filter: JSON.stringify({ readAt: { _is_null: true } }),
1766
+ fields: 'id,kind,targetPath,readAt',
1767
+ sort: '-createdAt,-id',
1768
+ limit: 10,
1769
+ },
1770
+ })
1771
+
1772
+ const hasNewEmail = computed(() =>
1773
+ attentionRows.value.some((row) => row.kind === 'email_inbound' && !row.readAt)
1774
+ )
1775
+ const hasNewSupport = computed(() =>
1776
+ attentionRows.value.some((row) => row.kind === 'support' && !row.readAt)
1777
+ )
1778
+ const accountBadge = computed(() => notificationSignal.value ? 'New' : null)
1779
+ const accountDescription = computed(() => notificationSignal.value ? 'New admin attention' : 'All caught up')
1780
+
1781
+ function syncFromNotificationRows() {
1782
+ const value = notificationApi.data?.value
1783
+ const rows = Array.isArray(value?.data)
1784
+ ? value.data
1785
+ : Array.isArray(value?.data?.data)
1786
+ ? value.data.data
1787
+ : []
1788
+ attentionRows.value = rows
1789
+ notificationSignal.value = rows.some((row) => !row.readAt)
1790
+ }
1791
+
1792
+ async function refreshNotificationSignals() {
1793
+ await notificationApi.execute()
1794
+ syncFromNotificationRows()
1795
+ }
1796
+
1797
+ const { register: registerAccountPanel } = useAccountPanelRegistry()
1798
+ registerAccountPanel({
1799
+ id: 'admin-attention',
1800
+ order: 20,
1801
+ label: 'Notifications',
1802
+ icon: computed(() => notificationSignal.value ? 'lucide:bell-ring' : 'lucide:bell'),
1803
+ description: accountDescription,
1804
+ count: accountBadge,
1805
+ badgeColor: 'info',
1806
+ onClick: () => navigateTo('/data/cloud_admin_notifications'),
1807
+ })
1808
+
1809
+ const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
1810
+ watchEffect(() => {
1811
+ if (hasNewEmail.value) {
1812
+ registerMenuNotification({
1813
+ id: 'attention-email',
1814
+ target: { path: '/email/messages' },
1815
+ color: 'info',
1816
+ title: 'New inbound email',
1817
+ })
1818
+ } else {
1819
+ unregisterMenuNotification('attention-email')
1820
+ }
1821
+
1822
+ if (hasNewSupport.value) {
1823
+ registerMenuNotification({
1824
+ id: 'attention-support',
1825
+ target: { path: '/cloud/support' },
1826
+ color: 'info',
1827
+ title: 'New support activity',
1828
+ })
1829
+ } else {
1830
+ unregisterMenuNotification('attention-support')
1831
+ }
1832
+ })
1833
+
1834
+ watch(() => route.path, (path) => {
1835
+ if (path.startsWith('/email/messages')) {
1836
+ attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'email_inbound')
1837
+ }
1838
+ if (path.startsWith('/cloud/support')) {
1839
+ attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'support')
1840
+ }
1841
+ notificationSignal.value = attentionRows.value.some((row) => !row.readAt)
1842
+ })
1843
+
1844
+ const { adminSocket } = useAdminSocket()
1845
+ function handleAdminNotification(payload) {
1846
+ refreshNotificationSignals()
1847
+ if (payload?.kind === 'email_inbound') {
1848
+ registerMenuNotification({ id: 'attention-email', target: { path: '/email/messages' }, color: 'info', title: 'New inbound email' })
1849
+ }
1850
+ if (payload?.kind === 'support') {
1851
+ registerMenuNotification({ id: 'attention-support', target: { path: '/cloud/support' }, color: 'info', title: 'New support activity' })
1852
+ }
1853
+ }
1854
+
1855
+ function getAdminSocket() {
1856
+ return adminSocket && adminSocket.value !== undefined ? adminSocket.value : adminSocket
1857
+ }
1858
+
1859
+ function bindAdminSocket(socket) {
1860
+ if (socket && typeof socket.on === 'function') {
1861
+ socket.on('admin:notification-created', handleAdminNotification)
1862
+ }
1863
+ }
1864
+
1865
+ function unbindAdminSocket(socket) {
1866
+ if (socket && typeof socket.off === 'function') {
1867
+ socket.off('admin:notification-created', handleAdminNotification)
1868
+ }
1869
+ }
1870
+
1871
+ if (adminSocket && adminSocket.value !== undefined) {
1872
+ watch(adminSocket, (nextSocket, previousSocket) => {
1873
+ unbindAdminSocket(previousSocket)
1874
+ bindAdminSocket(nextSocket)
1875
+ })
1876
+ }
1877
+
1878
+ onMounted(() => {
1879
+ refreshNotificationSignals()
1880
+ bindAdminSocket(getAdminSocket())
1881
+ })
1882
+ onUnmounted(() => {
1883
+ unbindAdminSocket(getAdminSocket())
1884
+ unregisterMenuNotification('attention-email')
1885
+ unregisterMenuNotification('attention-support')
1886
+ })
1887
+ </script>
1888
+ \`
1889
+
1890
+ ensure_global_extension({
1891
+ name: "AdminAttentionSignalBridge",
1892
+ description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
1893
+ code: signalBridgeCode,
1894
+ isEnabled: true,
1895
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1896
+ })`,
1897
+ notes: [
1898
+ 'Use this pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
1899
+ 'This example fetches only the notification source of truth, not the email, support, order, or job tables. Substitute your own notification or summary endpoint when available.',
1900
+ 'Omitting value on registerMenuNotification renders a dot. That is the right promise when the shell knows "new work exists" but not an exact count.',
1901
+ 'If a backend summary event already includes an exact unread count, use value for a count chip. If the event only says one record changed, use a dot and let the page fetch details.',
1902
+ 'Map notification kinds to menu targets by product meaning, not by copying these paths. For example, approval_required could target /reviews, failed_job could target /operations/jobs, and quota_warning could target /billing.',
1903
+ 'Clear local dot signals when the user enters the destination route or when the notification center marks the underlying notification as read.',
1904
+ ],
1905
+ },
1734
1906
  {
1735
1907
  name: 'Register a data-driven account-panel item',
1736
1908
  code: `<script setup>
@@ -1739,7 +1911,7 @@ const expanded = ref(false)
1739
1911
 
1740
1912
  const label = 'Notifications'
1741
1913
  const icon = computed(() => unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell')
1742
- const badge = computed(() => unread.value > 0 ? String(unread.value) : null)
1914
+ const count = computed(() => unread.value > 0 ? String(unread.value) : null)
1743
1915
  const description = computed(() => unread.value > 0 ? 'Needs review' : 'All caught up')
1744
1916
 
1745
1917
  const NotificationPanelContent = defineComponent({
@@ -1756,7 +1928,7 @@ register({
1756
1928
  label,
1757
1929
  icon,
1758
1930
  description,
1759
- badge,
1931
+ count,
1760
1932
  badgeColor: 'error',
1761
1933
  expanded,
1762
1934
  onToggle: () => {
@@ -1767,6 +1939,7 @@ register({
1767
1939
  </script>`,
1768
1940
  notes: [
1769
1941
  'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
1942
+ 'Use count for the primary visible badge value. badge remains supported as a legacy alias, but count is what the account trigger aggregates.',
1770
1943
  '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.',
1771
1944
  'Let the Enfyra admin UI handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
1772
1945
  'Keep contentComponent compact; it is rendered inside account-panel chrome and should not create another large card around itself.',
@@ -304,6 +304,9 @@ function getExtensionThemeContract() {
304
304
  'The extension is already mounted inside the Enfyra app shell. Do not add a duplicate page header, centered page wrapper, or root-level page padding.',
305
305
  'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
306
306
  'Use usePageHeaderRegistry for the shell title and useHeaderActionRegistry/useSubHeaderActionRegistry for page actions.',
307
+ 'Use useMenuNotificationRegistry from global extensions to register sidebar menu notification counts or dots. Register stable ids, target menus by id/path/route, use value for counts, omit value for a dot, and choose color from primary/success/warning/error/info/neutral.',
308
+ 'For shell menu notifications, first decide the signal source. Use a count only when the source already owns an exact count, such as a notification summary endpoint or bounded unread-notification query. Use a dot when a realtime event only proves that something new exists. Do not poll a domain list such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
309
+ 'Use useAccountPanelRegistry for account panel rows. AccountPanelItem supports count as the preferred numeric/text badge value, badge as a legacy alias, and badgeColor primary/neutral/info/error/warning/success.',
307
310
  'For detail/form workflows that should stay left-aligned with empty space on the right, wrap the body in eapp-page-constrained; use eapp-page-constrained-wide only when the workflow genuinely needs more width.',
308
311
  'Card/list grids inside the default shell must account for the 280px desktop sidebar. Do not switch general card grids to three columns at lg; use md:grid-cols-2 xl:grid-cols-3 unless a local container proves three columns have enough width.',
309
312
  ],
@@ -359,6 +362,7 @@ function getExtensionThemeContract() {
359
362
  'Use CommonDrawer for side-panel editing. Open drawers immediately on user action and render loading/error/content inside the drawer instead of waiting for fetch before opening.',
360
363
  'Use UTabs for page sections and large grouped forms instead of custom tab bars; the app-level Nuxt UI override owns active and inactive indicators, focus rings, spacing, and theme contrast.',
361
364
  'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
365
+ 'Use shell registries for shell badges: useAccountPanelRegistry for the account panel and useMenuNotificationRegistry for sidebar menus. Do not draw detached fixed-position badges over the app shell.',
362
366
  ],
363
367
  loadingAndLists: [
364
368
  'For first load of card/list pages, render calm skeleton cards with a slow pulse. Use USkeleton or shared loading components so the app-owned skeleton theme controls contrast and accent matching. For subsequent pagination/filter refreshes, keep the card shells mounted and skeletonize card content until the new list is ready.',
@@ -409,6 +413,12 @@ function getExtensionThemeContract() {
409
413
  },
410
414
  ],
411
415
  compactExample: '<template><section class="min-h-full w-full space-y-4"><article class="eapp-surface-card p-4"><div class="flex items-start justify-between gap-3"><div><p class="text-sm eapp-text-tertiary">Neutral KPI</p><p class="mt-2 text-2xl font-semibold eapp-text-primary">24</p></div><span class="eapp-primary-soft eapp-icon-tile"><UIcon name="lucide:square-stack" class="size-5 eapp-primary-text" /></span></div><div class="mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted"><div class="eapp-primary-solid h-full w-1/2"></div></div></article><section class="eapp-surface-card p-4"><div class="flex items-center justify-between gap-3"><p class="font-semibold eapp-text-primary">Status block stays neutral</p><UBadge color="success" variant="soft">Healthy</UBadge></div></section></section></template>',
416
+ shellNotificationContract: {
417
+ menu: 'useMenuNotificationRegistry().register({ id, target: { id?, path?, route? }, value?, color?, title?, order? }). value renders a count/chip; omitting value renders a dot. Parent menus sum numeric child values.',
418
+ accountPanel: 'useAccountPanelRegistry().register({ id, label, description, icon, count?, badge?, badgeColor?, expanded?, onToggle?, contentComponent? }). count is preferred over badge and the account trigger sums numeric visible item counts, capped at 99+.',
419
+ lifecycle: 'Register from global extensions for app-wide notification state; stable ids replace previous registrations and component-owned registrations are removed on unmount.',
420
+ reasoning: 'Counts and dots are different promises. A count says the shell knows an exact or bounded number from an appropriate notification/summary source. A dot says the shell only knows that new attention exists. Avoid fetching the destination domain list just to make a menu badge more precise.',
421
+ },
412
422
  contractAuthority: [
413
423
  'This is the authoritative Enfyra theme & color contract. Source of truth: documents/app/theme-color-contract.md. The app owns color through app/utils/primary-colors.ts (Material You seed-to-role generation), app/assets/css/theme.css (semantic variables and Nuxt UI ramps), app/assets/css/main.css (extension-safe semantic utilities), and app/app.config.ts (Nuxt UI component mapping). Pages and extensions only CONSUME classes/Nuxt UI props; they never define colors.',
414
424
  'Every color flows from two base layers: --md-* (Material You, runtime primary picker) and --st-* (status). Runtime primary roles are generated with SchemeTonalSpot. Success/warning/info stay fixed status quarts; error follows the generated Material error role through the single --danger-* lane. All Nuxt UI semantic colors (primary/secondary/success/warning/error/info/neutral) are re-pointed to these, so Nuxt UI is used per its docs but colors are decided by Enfyra. This applies to the shell, system pages, and compiled dynamic extensions.',
@@ -109,6 +109,7 @@ export function buildRequiredKnowledgePayload() {
109
109
  'Prefer FormEditor/FormEditorLazy for direct table-backed forms when the form maps to metadata fields.',
110
110
  'For long admin setup workflows, open CommonDrawer immediately and show loading/error/content inside it.',
111
111
  'Use Widget with numeric enfyra_extension widget ids; pass safe reactive props/events and keep page-level mutation ownership in the page unless the widget intentionally owns the workflow.',
112
+ 'Use useMenuNotificationRegistry for sidebar menu counts/dots and useAccountPanelRegistry count/badge fields for account panel notifications; register these from global extensions when they should update across the shell.',
112
113
  ],
113
114
  },
114
115
  ],