@enfyra/mcp-server 0.0.63 → 0.0.64

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.63",
3
+ "version": "0.0.64",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,95 @@ const me = await fetch("/enfyra/me", {
64
64
  'Do not read or store JWTs in browser JavaScript in proxy-cookie mode.',
65
65
  ],
66
66
  },
67
+ {
68
+ name: 'Nuxt client plugin for authenticated realtime',
69
+ code: `// composables/useRealtime.ts
70
+ import { io, type Socket } from "socket.io-client"
71
+ import { readonly, ref, shallowRef } from "vue"
72
+
73
+ const socket = shallowRef<Socket | null>(null)
74
+ const isConnected = ref(false)
75
+
76
+ export function useRealtime() {
77
+ function connect() {
78
+ if (import.meta.server) return null
79
+ if (socket.value) return socket.value
80
+
81
+ const nextSocket = io("/chat", {
82
+ path: "/socket.io",
83
+ withCredentials: true,
84
+ reconnection: true,
85
+ reconnectionAttempts: Infinity,
86
+ reconnectionDelay: 2000,
87
+ reconnectionDelayMax: 30000
88
+ })
89
+
90
+ nextSocket.on("connect", () => {
91
+ isConnected.value = true
92
+ })
93
+ nextSocket.on("disconnect", () => {
94
+ isConnected.value = false
95
+ })
96
+
97
+ socket.value = nextSocket
98
+ return nextSocket
99
+ }
100
+
101
+ function disconnect() {
102
+ if (!socket.value) return
103
+ socket.value.disconnect()
104
+ socket.value = null
105
+ isConnected.value = false
106
+ }
107
+
108
+ function onMessage(handler) {
109
+ const activeSocket = socket.value ?? connect()
110
+ if (!activeSocket) return () => {}
111
+ activeSocket.on("chat:message", handler)
112
+ return () => activeSocket.off("chat:message", handler)
113
+ }
114
+
115
+ return { socket, isConnected: readonly(isConnected), connect, disconnect, onMessage }
116
+ }
117
+
118
+ // plugins/realtime.client.ts
119
+ import { watch } from "vue"
120
+
121
+ export default defineNuxtPlugin(() => {
122
+ const { me } = useAuth()
123
+ const realtime = useRealtime()
124
+
125
+ watch(
126
+ me,
127
+ user => {
128
+ if (user) realtime.connect()
129
+ else realtime.disconnect()
130
+ },
131
+ { immediate: true }
132
+ )
133
+ })
134
+
135
+ // pages/chat.vue
136
+ const realtime = useRealtime()
137
+ let stopRealtime = () => {}
138
+
139
+ onMounted(() => {
140
+ stopRealtime = realtime.onMessage(event => {
141
+ // Update local UI state, then debounce REST refresh if full state is needed.
142
+ })
143
+ })
144
+
145
+ onUnmounted(() => {
146
+ stopRealtime()
147
+ })`,
148
+ notes: [
149
+ 'Create the socket once in a client-only plugin after auth has resolved; pages should not own the initial connection lifecycle.',
150
+ 'Use the websocket namespace path from live metadata, such as /chat, and keep the transport path as /socket.io.',
151
+ 'Proxy /socket.io/** to the Enfyra app bridge /ws/socket.io/** so cookies are same-origin.',
152
+ 'Route components add event listeners and remove them on unmount; they can optimistically update local state and debounce REST refreshes.',
153
+ 'Disconnect the singleton socket when the current user/session clears.',
154
+ ],
155
+ },
67
156
  {
68
157
  name: 'Google OAuth button',
69
158
  code: `const redirect = new URL("/chat", window.location.origin)
@@ -893,6 +982,66 @@ create_extension({
893
982
  '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.',
894
983
  ],
895
984
  },
985
+ {
986
+ name: 'Compose page extensions from widgets',
987
+ code: `// Create reusable/bulky sections as widget extension records first.
988
+ const reportStatusWidgetCode = \`
989
+ <template>
990
+ <section class="rounded-xl border border-default bg-default p-4">
991
+ <div class="flex items-start justify-between gap-3">
992
+ <div>
993
+ <p class="text-sm text-muted">Total reports</p>
994
+ <p class="mt-2 text-2xl font-semibold">{{ total }}</p>
995
+ <p class="mt-1 text-xs text-muted">{{ latestLabel }}</p>
996
+ </div>
997
+ <UButton type="button" color="neutral" variant="outline" @click.stop.prevent="emit('refresh')">Refresh</UButton>
998
+ </div>
999
+ <UButton v-if="hasLatest" type="button" class="mt-3" color="primary" variant="soft" @click.stop.prevent="openLatest">Open latest</UButton>
1000
+ </section>
1001
+ </template>
1002
+
1003
+ <script setup>
1004
+ const props = defineProps({
1005
+ total: { type: Number, default: 0 },
1006
+ rows: { type: Array, default: () => [] },
1007
+ openDetails: { type: Function, default: null }
1008
+ })
1009
+ const emit = defineEmits(['refresh'])
1010
+ const hasLatest = computed(() => props.rows.length > 0)
1011
+ const latestLabel = computed(() => hasLatest.value ? 'Latest: ' + (props.rows[0]?.title || props.rows[0]?.id || 'Untitled') : 'No reports yet')
1012
+ function openLatest() {
1013
+ if (typeof props.openDetails === 'function' && props.rows[0]) props.openDetails(props.rows[0])
1014
+ }
1015
+ </script>
1016
+ \`
1017
+
1018
+ create_extension({
1019
+ type: "widget",
1020
+ name: "ReportStatusWidget",
1021
+ description: "Report status summary cards",
1022
+ code: reportStatusWidgetCode,
1023
+ isEnabled: true
1024
+ })
1025
+
1026
+ // Read the created widget record id, then embed it from the page extension.
1027
+ create_extension({
1028
+ type: "page",
1029
+ name: "ReportsPage",
1030
+ menuId: "<reports-menu-id>",
1031
+ code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report_definition?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
1032
+ isEnabled: true
1033
+ })`,
1034
+ notes: [
1035
+ 'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
1036
+ 'Embed widgets by their numeric extension_definition id, not by extensionId/name.',
1037
+ 'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
1038
+ 'Use kebab-case in the parent template for camelCase widget props, for example :open-details maps to openDetails.',
1039
+ 'Do not mutate widget props. Use computed for derived display state, and use watch only when mirroring a prop into local editable draft state.',
1040
+ 'Prefer defineEmits for child-to-parent requests such as refresh. Use callback props only for parent-owned modal/drawer openers or imperative navigation.',
1041
+ 'Keep PermissionGate and type="button" plus @click.stop.prevent inside action widgets; server permissions still enforce the real boundary.',
1042
+ '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.',
1043
+ ],
1044
+ },
896
1045
  {
897
1046
  name: 'Page header and action button variants',
898
1047
  code: `<script setup>
@@ -66,6 +66,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
66
66
  '- Fetch the current user with **`GET /enfyra/me`** and logout with **`POST /enfyra/logout`**. Browser fetches stay same-origin and credentials/cookies flow through the proxy. Do not read JWTs in browser JavaScript for this mode.',
67
67
  '- OAuth starts on the same proxy prefix, e.g. **`GET /enfyra/auth/{provider}?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra`**. `redirect` must be an absolute `http(s)` URL with the app origin. `cookieBridgePrefix` is the third app proxy prefix that forwards to the Enfyra API; Enfyra normalizes it, so `enfyra`, `/enfyra`, and `/enfyra/` all mean `/enfyra`. Use token-query callback handling only when the app intentionally manages tokens itself.',
68
68
  '- Socket.IO uses the app bridge too. Browser clients should connect to the gateway namespace with the Socket.IO transport path on the app origin, e.g. `io("/chat", { path: "/socket.io", withCredentials: true })`, while Nuxt proxies `/socket.io/**` to the Enfyra app bridge `/ws/socket.io/**`. Do not connect browser code directly to the hidden backend Socket.IO endpoint.',
69
+ '- For generated authenticated browser apps, initialize the Socket.IO client once in a client-only app bootstrap/plugin after the current user/session is known. Do not create the first socket lazily inside each page. Pages/components should only add event listeners, react to events, debounce REST refreshes when needed, and remove listeners on unmount.',
69
70
  '- If a project explicitly standardizes on `/api/**` instead of `/enfyra/**`, keep the same proxy behavior under that prefix: proxy to the Enfyra API and avoid generated cookie-management routes unless the user asks for a custom auth boundary.',
70
71
  '- If you are explaining MCP\'s own internal authentication, that is separate: this MCP server exchanges `ENFYRA_API_TOKEN` against `{ENFYRA_API_URL}/auth/token/exchange` before authenticated tool calls. The raw `efy_pat_*` token is never a Bearer token. For normal app work, `ENFYRA_API_URL` must still be the app proxy base such as `{{ nuxtApp }}/api`.',
71
72
  '',
@@ -159,6 +160,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
159
160
  '### Chat / realtime app rules learned from implementation review',
160
161
  '- Before generating a chat app, inspect live metadata for `websocket_definition`, `websocket_event_definition`, `chat_conversation`, `chat_conversation_member`, and `chat_message`. Do not assume table names, reverse relations, route permissions, or physical field casing.',
161
162
  '- For browser SSR apps, REST goes through the app proxy prefix and Socket.IO goes through an app-origin Socket.IO transport proxy. Third apps should connect to the namespace, e.g. `io("/chat", { path: "/socket.io", withCredentials: true })`, and proxy `/socket.io/**` to the Enfyra app bridge `/ws/socket.io/**`. Do not connect app browser code directly to the hidden backend. Do not add custom token cookies when the Enfyra app/proxy already owns cookies.',
163
+ '- For authenticated realtime clients, create the socket as an application singleton from a client-only plugin/bootstrap once auth has resolved. Watch the shared current-user/session state: connect when a user exists, disconnect when the session clears, and let route components subscribe/unsubscribe listeners instead of owning the connection lifecycle.',
162
164
  '- On an authenticated gateway, Enfyra loads `user_definition` once for the socket and event scripts receive `@USER`. The server also joins `user_<userId>` after the connection script succeeds. Event scripts should not ask the client to send `senderId`, and `chat:join` does not need to join `user_<userId>` again.',
163
165
  '- Use `chat:join` only for conversation rooms. Query `chat_conversation` with the current user membership and join `conversation:<conversationId>` rooms. Do not query all membership rows and accidentally join rooms named from member ids.',
164
166
  '- `chat:message` should broadcast to `conversation:<conversationId>` with `@SOCKET.broadcastToRoom`, then persist through `@REPOS` in the same event script. Do not trigger a flow just to save a chat message unless the product needs workflow semantics.',
@@ -316,6 +318,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
316
318
  '- **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.',
317
319
  '- **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.',
318
320
  '- **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.*`.',
321
+ '- **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.',
319
322
  '- **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.',
320
323
  '- **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.',
321
324
  '- **Admin menu visibility is permission-driven, not RLS:** admin menu entries are sensitive and must set `menu_definition.permission` so they are visible only to users who have at least GET permission for the backing route or table. Permission conditions use HTTP `methods`, not CRUD `actions`. Do not show an admin menu merely because an extension exists or because the path is hardcoded. Example: `/reports` menu can require `{ or: [{ route: "/reports", methods: ["GET"] }, { route: "/report_definition", methods: ["GET"] }] }`.',
@@ -355,6 +358,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
355
358
  '- **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).',
356
359
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
357
360
  '- **useMenuApi:** Low-level menu API.',
361
+ '- **useAdminSocket:** Shared admin Socket.IO client. Returns `{ adminSocket, activeReloads, isReloading, showReloadBanner, runtimeMetricsByInstance, runtimeMetricsUpdatedAt, redisAdminOverview, redisAdminOverviewUpdatedAt, redisAdminKeyChange, loadRedisOverview, loadRedisKeys, loadRedisKey }`. Listen with `adminSocket.on(event, handler)` and remove listeners in `onUnmounted` with `adminSocket.off(event, handler)`.',
358
362
  '- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
359
363
  '- **useDatabase:** Database helpers (returns `{ getId, getIdFieldName }`).',
360
364
  '- **useRoutes:** Route management.',
@@ -369,7 +373,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
369
373
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
370
374
  '- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
371
375
  '- **Tabs:** `UTabs` is available in current eApp extension runtime. Use it for page-level sections when a page would otherwise become too long.',
372
- '- **Extension:** `Widget` — embed widget extension via `<Widget :id="extensionId" />`',
376
+ '- **Extension:** `Widget` — embed widget extension records by numeric database id, e.g. `<Widget :id="123" />`',
373
377
  '- **WebSocket:** `WebSocketManager`',
374
378
  '- **Permission:** `PermissionGate`, `PermissionManager`',
375
379
  '',
@@ -380,7 +384,13 @@ export function buildMcpServerInstructions(apiBaseUrl) {
380
384
  '#### Extension types:',
381
385
  '- **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`.',
382
386
  '- **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.',
383
- '- **type "widget":** Widget extension. No menu required. Embed via `<Widget :id="extensionId" />` in other extensions or pages.',
387
+ '- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
388
+ '- **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.',
389
+ '- **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.',
390
+ '- **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.',
391
+ '- **Widget ownership:** keep route parsing, page-level API loading/mutation state, and modal submit flows in the page extension unless a widget intentionally owns the whole workflow. Use widgets for focused render sections or operation panels that receive safe data/actions from the page.',
392
+ '- **Widget permissions:** sensitive action controls inside widgets must still use `PermissionGate` or `usePermissions()` and must keep `type="button"` plus `@click.stop.prevent` for modal/drawer triggers. Server routes remain the final permission boundary.',
393
+ '- **Widget loading performance:** eApp batches widget metadata fetches requested in the same tick and caches loaded widgets. Do not manually fetch widget code from page extensions; render `<Widget>` components together so the runtime can batch-load them.',
384
394
  '- **Existing pages:** if a menu already has a page extension, update that `extension_definition` record instead of creating a duplicate menu/extension. For example `/dashboard` is menu-driven and may already have an extension attached.',
385
395
  '',
386
396
  '#### NPM packages (install via MCP):',