@enfyra/mcp-server 0.0.63 → 0.0.65
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 +1 -1
- package/src/lib/mcp-examples.js +182 -0
- package/src/lib/mcp-instructions.js +14 -2
- package/src/mcp-server-entry.mjs +2 -1
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -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)
|
|
@@ -356,6 +445,39 @@ GET /enfyra/post?filter={"<primaryKeyFromMetadata>":{"_eq":123}}&limit=1`,
|
|
|
356
445
|
'Do not invent deep keys like members unless members is a relation on that table.',
|
|
357
446
|
],
|
|
358
447
|
},
|
|
448
|
+
{
|
|
449
|
+
name: 'Sort parent rows by child relation aggregates',
|
|
450
|
+
code: `query_table({
|
|
451
|
+
tableName: "cloud_support_tickets",
|
|
452
|
+
fields: [
|
|
453
|
+
"id",
|
|
454
|
+
"subject",
|
|
455
|
+
"status",
|
|
456
|
+
"project.id",
|
|
457
|
+
"project.name"
|
|
458
|
+
],
|
|
459
|
+
sort: "-_max(messages.createdAt),-createdAt",
|
|
460
|
+
limit: 25,
|
|
461
|
+
deep: JSON.stringify({
|
|
462
|
+
messages: {
|
|
463
|
+
fields: "id,authorKind,body,createdAt",
|
|
464
|
+
sort: "-createdAt",
|
|
465
|
+
limit: 3
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Other parent aggregate sorts:
|
|
471
|
+
// sort=-_count(messages)
|
|
472
|
+
// sort=_min(messages.createdAt)`,
|
|
473
|
+
notes: [
|
|
474
|
+
'Use _max(relation.field) for latest-child ordering, _min(relation.field) for earliest-child ordering, and _count(relation) for child-count ordering.',
|
|
475
|
+
'Aggregate sort helpers only work on direct one-to-many or many-to-many list relations.',
|
|
476
|
+
'The aggregate field must be a real non-encrypted scalar field on the related table.',
|
|
477
|
+
'Do not use raw sort=-messages.createdAt for parent ordering; it is ambiguous and rejected.',
|
|
478
|
+
'deep.messages.sort only orders the loaded message rows inside each ticket, so keep parent sort and child pagination as separate concerns.',
|
|
479
|
+
],
|
|
480
|
+
},
|
|
359
481
|
{
|
|
360
482
|
name: 'Encrypted fields are not lookup fields',
|
|
361
483
|
code: `// Bad: api_token is isEncrypted=true, so filter/sort cannot use it.
|
|
@@ -893,6 +1015,66 @@ create_extension({
|
|
|
893
1015
|
'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
1016
|
],
|
|
895
1017
|
},
|
|
1018
|
+
{
|
|
1019
|
+
name: 'Compose page extensions from widgets',
|
|
1020
|
+
code: `// Create reusable/bulky sections as widget extension records first.
|
|
1021
|
+
const reportStatusWidgetCode = \`
|
|
1022
|
+
<template>
|
|
1023
|
+
<section class="rounded-xl border border-default bg-default p-4">
|
|
1024
|
+
<div class="flex items-start justify-between gap-3">
|
|
1025
|
+
<div>
|
|
1026
|
+
<p class="text-sm text-muted">Total reports</p>
|
|
1027
|
+
<p class="mt-2 text-2xl font-semibold">{{ total }}</p>
|
|
1028
|
+
<p class="mt-1 text-xs text-muted">{{ latestLabel }}</p>
|
|
1029
|
+
</div>
|
|
1030
|
+
<UButton type="button" color="neutral" variant="outline" @click.stop.prevent="emit('refresh')">Refresh</UButton>
|
|
1031
|
+
</div>
|
|
1032
|
+
<UButton v-if="hasLatest" type="button" class="mt-3" color="primary" variant="soft" @click.stop.prevent="openLatest">Open latest</UButton>
|
|
1033
|
+
</section>
|
|
1034
|
+
</template>
|
|
1035
|
+
|
|
1036
|
+
<script setup>
|
|
1037
|
+
const props = defineProps({
|
|
1038
|
+
total: { type: Number, default: 0 },
|
|
1039
|
+
rows: { type: Array, default: () => [] },
|
|
1040
|
+
openDetails: { type: Function, default: null }
|
|
1041
|
+
})
|
|
1042
|
+
const emit = defineEmits(['refresh'])
|
|
1043
|
+
const hasLatest = computed(() => props.rows.length > 0)
|
|
1044
|
+
const latestLabel = computed(() => hasLatest.value ? 'Latest: ' + (props.rows[0]?.title || props.rows[0]?.id || 'Untitled') : 'No reports yet')
|
|
1045
|
+
function openLatest() {
|
|
1046
|
+
if (typeof props.openDetails === 'function' && props.rows[0]) props.openDetails(props.rows[0])
|
|
1047
|
+
}
|
|
1048
|
+
</script>
|
|
1049
|
+
\`
|
|
1050
|
+
|
|
1051
|
+
create_extension({
|
|
1052
|
+
type: "widget",
|
|
1053
|
+
name: "ReportStatusWidget",
|
|
1054
|
+
description: "Report status summary cards",
|
|
1055
|
+
code: reportStatusWidgetCode,
|
|
1056
|
+
isEnabled: true
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
// Read the created widget record id, then embed it from the page extension.
|
|
1060
|
+
create_extension({
|
|
1061
|
+
type: "page",
|
|
1062
|
+
name: "ReportsPage",
|
|
1063
|
+
menuId: "<reports-menu-id>",
|
|
1064
|
+
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>",
|
|
1065
|
+
isEnabled: true
|
|
1066
|
+
})`,
|
|
1067
|
+
notes: [
|
|
1068
|
+
'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
|
|
1069
|
+
'Embed widgets by their numeric extension_definition id, not by extensionId/name.',
|
|
1070
|
+
'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
|
|
1071
|
+
'Use kebab-case in the parent template for camelCase widget props, for example :open-details maps to openDetails.',
|
|
1072
|
+
'Do not mutate widget props. Use computed for derived display state, and use watch only when mirroring a prop into local editable draft state.',
|
|
1073
|
+
'Prefer defineEmits for child-to-parent requests such as refresh. Use callback props only for parent-owned modal/drawer openers or imperative navigation.',
|
|
1074
|
+
'Keep PermissionGate and type="button" plus @click.stop.prevent inside action widgets; server permissions still enforce the real boundary.',
|
|
1075
|
+
'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.',
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
896
1078
|
{
|
|
897
1079
|
name: 'Page header and action button variants',
|
|
898
1080
|
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.',
|
|
@@ -239,6 +241,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
239
241
|
'- Use **`discover_query_capabilities`** before building non-trivial filters/deep queries. It returns supported filter operators, field-permission condition operators, deep shape/rules, table columns/relations, table primary key, route paths, and examples.',
|
|
240
242
|
'- Full filter operators: `_eq`, `_neq`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_not_in`, `_nin`, `_contains`, `_starts_with`, `_ends_with`, `_between`, `_is_null`, `_is_not_null`, `_and`, `_or`, `_not`.',
|
|
241
243
|
'- Field permission condition DSL is narrower and does not support `_contains`, `_starts_with`, `_ends_with`, or `_between`.',
|
|
244
|
+
'- Root `sort` accepts local fields such as `-createdAt` plus direct list-relation aggregate helpers: `_count(relationName)`, `_max(relationName.fieldName)`, and `_min(relationName.fieldName)`. Use `-_max(messages.createdAt)` to order parent rows by the latest child row. The relation must be direct `one-to-many` or `many-to-many`, and the aggregate field must be a non-encrypted scalar field on the related table.',
|
|
245
|
+
'- Raw dotted to-many sort such as `messages.createdAt` is invalid for parent ordering. `deep: { messages: { sort: "-createdAt" } }` sorts the loaded child rows inside each parent only; it does not sort the parent list.',
|
|
242
246
|
'- Deep shape: `{ relationName: { fields?, filter?, sort?, limit?, page?, deep? } }`. Relation keys are relation `propertyName`, not physical FK columns.',
|
|
243
247
|
'- Use dotted relation fields such as `owner.email` or `lastMessage.text` when the caller only needs basic related record fields. Use `deep` when relation loading needs query options such as `filter`, `sort`, `limit`, `page`, or nested `deep`. Do not use `deep` for simple relation-id filters, one-row lookup, counts, or large child collections that should be loaded separately with pagination.',
|
|
244
248
|
'- Deep validation rejects unknown relation keys, unknown subkeys, `limit` on many-to-one/one-to-one, and invalid dotted sort through many-side relations.',
|
|
@@ -316,6 +320,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
316
320
|
'- **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
321
|
'- **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
322
|
'- **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.*`.',
|
|
323
|
+
'- **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
324
|
'- **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
325
|
'- **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
326
|
'- **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 +360,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
355
360
|
'- **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
361
|
'- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
|
|
357
362
|
'- **useMenuApi:** Low-level menu API.',
|
|
363
|
+
'- **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
364
|
'- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
|
|
359
365
|
'- **useDatabase:** Database helpers (returns `{ getId, getIdFieldName }`).',
|
|
360
366
|
'- **useRoutes:** Route management.',
|
|
@@ -369,7 +375,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
369
375
|
'- **Menu:** `MenuRenderer`, `MenuItemEditor`',
|
|
370
376
|
'- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
|
|
371
377
|
'- **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
|
|
378
|
+
'- **Extension:** `Widget` — embed widget extension records by numeric database id, e.g. `<Widget :id="123" />`',
|
|
373
379
|
'- **WebSocket:** `WebSocketManager`',
|
|
374
380
|
'- **Permission:** `PermissionGate`, `PermissionManager`',
|
|
375
381
|
'',
|
|
@@ -380,7 +386,13 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
380
386
|
'#### Extension types:',
|
|
381
387
|
'- **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
388
|
'- **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="
|
|
389
|
+
'- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
|
|
390
|
+
'- **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.',
|
|
391
|
+
'- **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.',
|
|
392
|
+
'- **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.',
|
|
393
|
+
'- **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.',
|
|
394
|
+
'- **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.',
|
|
395
|
+
'- **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
396
|
'- **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
397
|
'',
|
|
386
398
|
'#### NPM packages (install via MCP):',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -654,7 +654,7 @@ server.tool(
|
|
|
654
654
|
queryParams: {
|
|
655
655
|
fields: 'Comma-separated scalar/relation fields. Relations use relation propertyName, not physical FK column names.',
|
|
656
656
|
filter: 'JSON object using operators above. Relation filters use nested relation propertyName objects.',
|
|
657
|
-
sort: '
|
|
657
|
+
sort: 'Local field or -field. For direct one-to-many/many-to-many parent ordering, use _count(relation), _max(relation.field), or _min(relation.field); raw dotted to-many sort is invalid.',
|
|
658
658
|
page: '1-based page.',
|
|
659
659
|
limit: 'Page size.',
|
|
660
660
|
meta: 'Request metadata/counts where supported.',
|
|
@@ -668,6 +668,7 @@ server.tool(
|
|
|
668
668
|
'Unknown deep entry keys are invalid.',
|
|
669
669
|
'limit on many-to-one/one-to-one relations is invalid.',
|
|
670
670
|
'Dotted sort through one-to-many/many-to-many is invalid.',
|
|
671
|
+
'Deep sort orders rows inside the related collection only; use root aggregate sort helpers when parent rows must be ordered by child values.',
|
|
671
672
|
'Nested deep is recursively validated.',
|
|
672
673
|
'Field permissions may rewrite filters/sorts and sanitize post-query results.',
|
|
673
674
|
],
|