@enfyra/mcp-server 0.0.42 → 0.0.44

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.42",
3
+ "version": "0.0.44",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -287,6 +287,18 @@ const scope = {
287
287
  'Mutate @QUERY.filter before canonical CRUD runs.',
288
288
  ],
289
289
  },
290
+ {
291
+ name: 'Pre-hook encrypted field normalization',
292
+ code: `const value = @BODY.api_token_encrypted
293
+ if (value && value.slice(0, 7) !== "enc:v1:") {
294
+ @BODY.api_token_encrypted = @HELPERS.$encrypt.encrypt(value)
295
+ }`,
296
+ notes: [
297
+ 'Use Enfyra pre-hooks for request-body normalization before canonical CRUD persists the record.',
298
+ 'Do not implement encrypted field normalization as a Knex/database hook.',
299
+ 'Use $encrypt for encryption and $ssh.generateKeyPair for SSH key generation; do not use $secrets.',
300
+ ],
301
+ },
290
302
  {
291
303
  name: 'Post-hook response shaping',
292
304
  code: `if (@ERROR) {
@@ -355,6 +367,75 @@ return @DATA`,
355
367
  'Use route/pre-hook filters for row-level access.',
356
368
  ],
357
369
  },
370
+ {
371
+ name: 'Admin menu and extension permission gates',
372
+ code: `<template>
373
+ <section class="space-y-4">
374
+ <PermissionGate :condition="canReadCloudProjects">
375
+ <template #default>
376
+ <div class="flex items-center justify-between gap-3">
377
+ <h2 class="text-lg font-semibold">Cloud projects</h2>
378
+
379
+ <PermissionGate :condition="canCreateCloudProject">
380
+ <UButton icon="i-lucide-plus" label="Create project" @click="openCreate = true" />
381
+ </PermissionGate>
382
+ </div>
383
+
384
+ <div v-for="project in projects" :key="project.id" class="rounded-lg border p-4">
385
+ <NuxtLink :to="\`/cloud/projects/\${project.id}\`" class="font-medium">
386
+ {{ project.name }}
387
+ </NuxtLink>
388
+
389
+ <div class="mt-3 flex gap-2">
390
+ <PermissionGate :condition="canUpdateCloudProject">
391
+ <UButton icon="i-lucide-pause" variant="soft" label="Disable" @click="openDisable(project)" />
392
+ </PermissionGate>
393
+
394
+ <PermissionGate :condition="canDeleteCloudProject">
395
+ <UButton icon="i-lucide-trash-2" color="error" variant="soft" label="Delete" @click="openDelete(project)" />
396
+ </PermissionGate>
397
+ </div>
398
+ </div>
399
+ </template>
400
+
401
+ <template #fallback>
402
+ <EmptyState title="No access" description="You do not have permission to view Cloud projects." />
403
+ </template>
404
+ </PermissionGate>
405
+ </section>
406
+ </template>
407
+
408
+ <script setup>
409
+ const { checkPermissionCondition } = usePermissions()
410
+
411
+ const canReadCloudProjects = computed(() => checkPermissionCondition({
412
+ or: [
413
+ { route: '/cloud/projects', methods: ['GET'] },
414
+ { route: '/cloud_projects', methods: ['GET'] }
415
+ ]
416
+ }))
417
+
418
+ const canCreateCloudProject = computed(() => checkPermissionCondition({
419
+ or: [{ route: '/cloud_projects', methods: ['POST'] }]
420
+ }))
421
+
422
+ const canUpdateCloudProject = computed(() => checkPermissionCondition({
423
+ or: [{ route: '/cloud_projects', methods: ['PATCH'] }]
424
+ }))
425
+
426
+ const canDeleteCloudProject = computed(() => checkPermissionCondition({
427
+ or: [{ route: '/cloud_projects', methods: ['DELETE'] }]
428
+ }))
429
+ </script>`,
430
+ notes: [
431
+ 'This is menu/extension visibility, not row-level RLS.',
432
+ 'Set menu_definition.permission on every sensitive admin menu. Example for /cloud/hosts: { or: [{ route: "/cloud/admin/hosts", methods: ["GET"] }, { route: "/cloud_servers", methods: ["GET"] }] }.',
433
+ 'Admin pages are sensitive. Use permission gates by default, not as an optional polish step.',
434
+ 'Menus should only be visible when the user has at least GET permission for the page route or backing data route.',
435
+ 'Inside the extension, gate each action by its own route/method: GET for page visibility, POST for create/flow-trigger buttons, PATCH for normal record edits, DELETE for native delete routes.',
436
+ 'Server route permissions remain mandatory; UI gates are for clear operator UX and least-privilege surfaces.',
437
+ ],
438
+ },
358
439
  ],
359
440
  },
360
441
  websocket: {
@@ -553,15 +634,49 @@ create_extension({
553
634
  name: "ReportsPage",
554
635
  description: "Reports dashboard",
555
636
  menuId: "<created-menu-id>",
556
- code: "<template><section class=\\"min-h-full w-full space-y-4\\">Reports</section></template><script setup>useHeaderActionRegistry({ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 })</script>",
637
+ 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>",
557
638
  isEnabled: true
558
639
  })`,
559
640
  notes: [
560
641
  'Menu provides navigation; extension provides content.',
561
642
  'Use menu_definition.label, not title.',
562
643
  'For page extensions, create the menu first and pass menuId to create_extension.',
644
+ 'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
645
+ 'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
646
+ 'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
647
+ 'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry.',
563
648
  'Page extensions should be full-bleed by default and responsive from the first version.',
564
649
  'The extension root is already inside eApp main; do not add root-level page padding.',
650
+ '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.',
651
+ ],
652
+ },
653
+ {
654
+ name: 'Debug menu or extension changes that do not appear in open eApp tabs',
655
+ code: `// Server side: menu_definition and extension_definition are runtime UI definitions.
656
+ // They must participate in partial reload, just like metadata/routes.
657
+ // Expected server contract:
658
+ // - cache orchestrator maps menu_definition -> menu reload
659
+ // - cache orchestrator maps extension_definition -> extension reload
660
+ // - successful writes emit $system:reload to the admin Socket.IO namespace
661
+
662
+ // eApp side expected listener behavior:
663
+ // if reload target is metadata/menu:
664
+ // await fetch menus
665
+ // rebuild menu registry with reset: true
666
+ // invalidate dynamic extension cache too, because route-to-extension mapping may change
667
+ // if reload target is extension/menu or extension/global:
668
+ // clear dynamic extension component/meta cache
669
+
670
+ // Verification pattern:
671
+ // 1. Save the menu or extension record.
672
+ // 2. Watch the open eApp tab for the $system:reload event.
673
+ // 3. Confirm sidebar/menu registry or extension component cache changed.
674
+ // 4. Only use manual reload endpoints or browser refresh after the natural event path is proven stale.`,
675
+ notes: [
676
+ 'Do not treat menu and extension writes as plain CRUD when debugging live admin UI.',
677
+ 'Check both halves: ASV/ESV emits the reload event, and eApp consumes it.',
678
+ 'Menu reload should also invalidate extension cache because menu records attach page extensions to routes.',
679
+ 'Manual reload is a fallback, not the default fix.',
565
680
  ],
566
681
  },
567
682
  {
@@ -589,6 +704,8 @@ create_menu({
589
704
  'Design the menu/page split before generating dashboard code.',
590
705
  'Keep /dashboard as a summary and distribution page, not a detailed operations table.',
591
706
  'Use focused pages for operational domains.',
707
+ 'Each page extension must use usePageHeaderRegistry for the app-shell title strip and should not render a duplicate top header in the body.',
708
+ 'PageHeader.stats is reserved for deliberate overview headers; operational KPIs belong in body cards/tables.',
592
709
  'Provisioning pages should not show raw history rows as the primary UI; group by project/run and translate step keys into operator-facing labels.',
593
710
  'Operational lists should use pagination plus search/filter controls; do not rely on arbitrary fixed limits such as limit=50.',
594
711
  'UTabs is available in eApp extension runtime for page-level sections.',
@@ -121,6 +121,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
121
121
  '- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
122
122
  '- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
123
123
  '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
124
+ '- Cloud owner-scoped GET handlers must keep normal users filtered to their own `cloud_projects.owner`, but root admin operational views must bypass that owner filter with `@USER.isRootAdmin`. Apply this consistently to `cloud_projects`, `cloud_subscriptions`, `cloud_payment_orders`, `cloud_provisioning_history`, and `/cloud/projects` so admin data routes match admin summary APIs.',
124
125
  '- MCP tools that use `fetchAPI` authenticate with the configured admin credentials; explain to users that **direct HTTP** calls need a token unless the route/method is published.',
125
126
  '',
126
127
  '### Post-hooks (REST)',
@@ -130,6 +131,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
130
131
  '### Dynamic script syntax preference',
131
132
  '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, and `@THROW400`–`@THROW503`.',
132
133
  '- Use Enfyra native throw helpers for intentional errors: `@THROW400("message")`, `@THROW403()`, `@THROW404("resource", id)`, or `$ctx.$throw[400]("message")`. Do not generate `throw new Error(...)` for user/domain errors in handlers, hooks, flows, websocket events, OAuth scripts, or admin-generated scripts.',
134
+ '- For encrypted persisted fields such as `*_encrypted`, use an Enfyra route pre-hook, not a Knex/database hook. Mutate the body before persistence: `const value = @BODY.field_encrypted; if (value && value.slice(0, 7) !== "enc:v1:") @BODY.field_encrypted = @HELPERS.$encrypt.encrypt(value);`.',
135
+ '- ASV exposes `$helpers.$encrypt.encrypt/decrypt` for encrypted strings and `$helpers.$ssh.generateKeyPair` for SSH keys. Do not generate `$helpers.$secrets` usage.',
136
+ '- Script-backed records use one shared persistence contract: `sourceCode` is the editable source, `scriptLanguage` controls compilation, and `compiledCode` is generated by the server from `sourceCode`. Do not hand-edit or send stale `compiledCode` from generated tools; save `sourceCode`/`scriptLanguage` through `PATCH /<script_table>/<id>` and let the server persist generated `compiledCode` internally. Public metadata may mark `compiledCode` non-updatable, but the server engine must still preserve the generated value after normalization.',
137
+ '- Enfyra Cloud host provisioning with PgBouncer must preserve tenant database isolation. PgBouncer should use per-tenant `DATABASE_URLS` entries with each tenant `db_user`, `db_password`, and `db_name`, and PostgreSQL 16/SCRAM hosts need `AUTH_TYPE=plain`. Do not route all tenants through PostgreSQL `postgres` just to make PgBouncer connect; that bypasses tenant DB permissions.',
138
+ '- Enfyra Cloud Docker health checks must compare exact healthy states. `true healthy` passes, `true starting` keeps waiting, and `true unhealthy` fails. Do not use broad substring logic where `unhealthy` accidentally counts as healthy.',
133
139
  '- Before saving generated script code, validate it with `POST /admin/script/validate` when available. It compiles with the server kernel and parses the executable async body without running side effects. Enfyra App `FormCodeEditor` also exposes a `Validate` action for this endpoint; use it before save/run when editing through the UI. If unavailable, use `run_admin_test`/`test_flow_step` as the closest validation path before saving.',
134
140
  '- Do not coerce dynamic script values with `String(...)`, `Number(...)`, or `Boolean(...)`. Enfyra payloads, user ids, record ids, and relation ids should keep their runtime type; validate required values and pass them through directly.',
135
141
  '- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
@@ -256,10 +262,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
256
262
  '- Enfyra supports automated workflows via **`flow_definition`**, **`flow_step_definition`**, and **`flow_execution_definition`** tables.',
257
263
  '- **Flow** (`flow_definition`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
258
264
  '- **Step** (`flow_step_definition`): `flow` → flow id, `key` (unique identifier for data chain), `stepOrder`, `type` (`script`, `condition`, `query`, `create`, `update`, `delete`, `http`, `trigger_flow`, `sleep`, `log`), `config` (JSON), `timeout`, `onError` (`stop`, `skip`, `retry`), `retryAttempts`, `parent` → self-ref to condition step (null = root), `branch` (`true`/`false` — which branch of parent condition).',
259
- '- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `context` (full data chain), `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. Query separately — NOT nested under flow_definition.',
265
+ '- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. There is no persisted `context` column; failed executions store diagnostics in `error`. Query separately — NOT nested under flow_definition.',
260
266
  '- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
261
267
  '- **Step config examples**: script: `{"code":"return #user_definition.find({limit:10})"}`, condition: `{"code":"return @FLOW_LAST?.data?.length > 0"}` (uses JS truthy/falsy: `return user` = truthy if exists, `return null` = falsy), query: `{"table":"user_definition","filter":{"status":{"_eq":"active"}},"limit":10}`, http: `{"url":"https://api.example.com","method":"POST","body":{}}` (auto Content-Type: application/json; **http `url` must be public-safe**—see Safety), sleep: `{"ms":5000}`, trigger_flow: `{"flowId":2}`.',
262
268
  '- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
269
+ '- **Flow step output discipline:** Script/condition steps must return only the small values that later steps genuinely need, such as ids, booleans, status keys, or counters. Do not return full records, host objects, package objects, DB URLs, SSH keys, API tokens, generated passwords, or other secrets. Later steps should re-query the records they need from ids/payload. Flow execution history already records errors; successful runs do not need full success snapshots.',
263
270
  '- **Template syntax (flows)**: `@FLOW_PAYLOAD` → `$ctx.$flow.$payload` (input data), `@FLOW_LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@FLOW_META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@THROW400`–`@THROW503` / `@THROW` → `$ctx.$throw[...]`. Trigger other flows in handlers via `@TRIGGER(name, payload)` or `$ctx.$trigger(name, payload)`.',
264
271
  '- **Condition branching**: Condition step uses JavaScript truthy/falsy evaluation (e.g. `return user` → truthy if exists, falsy if null/0/undefined). Children with matching `parent: {id: conditionStepId}` and `branch: "true"/"false"` execute. Root steps (no parent) always execute sequentially.',
265
272
  '- **Safety**: Max nesting depth 10 (flow triggering flow). Circular flow detection prevents A→B→A loops. HTTP steps: **SSRF hardening** — only `http`/`https`; blocks `localhost`, private IPs, and hostnames resolving to private IPs (use internet-facing URLs like `https://api.example.com`, not internal services, unless server policy changes). Default HTTP timeout 30s (AbortController). `$trigger()` available inside flow steps.',
@@ -279,11 +286,32 @@ export function buildMcpServerInstructions(apiBaseUrl) {
279
286
  '- **Operational list data loading:** do not use arbitrary fixed limits such as `limit=50` as the whole data strategy for admin pages. Use pagination, expose result count when the API supports `meta=filterCount`, and add search/filter controls for natural lookup keys such as project id, name, and subdomain.',
280
287
  '- **ESV aggregate contract:** aggregate query must be an object keyed by a real field or relation, for example `aggregate: { id: { count: true }, status: { count: { _eq: "failed" } }, amount: { sum: true } }`. Results are returned in `response.meta.aggregate`. Time windows and cross-field conditions belong in top-level `filter`, not inside a field aggregate condition. Field aggregate conditions only support operators on that same field; relation aggregates use `countRecords`.',
281
288
  '- **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.',
282
- '- **Partial reload default:** ESV automatically triggers partial reloads for metadata, routes, 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.',
289
+ '- **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.',
290
+ '- **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.',
283
291
  '- **Dashboard stats:** time range buttons must change the query filter and reload stats. Cloud dashboard should summarize flow execution errors from `flow_execution_definition` and billing stats from order/subscription records; successful/no-error flow runs do not need a standalone provisioning menu.',
284
292
  '- **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.',
293
+ '- **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.',
294
+ '- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, host capacity, 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.',
295
+ '- **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 project", permission: { and: [{ route: "/cloud_projects", methods: ["POST"] }] }, onClick }`.',
296
+ '- **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.',
297
+ '- **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/cloud_projects")`, because Vue compiles template helpers to `_ctx.*`.',
298
+ '- **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.',
285
299
  '- **Admin record links:** when an admin extension links to backend records for management or inspection, point to eApp data routes such as `/data/landing_terms`, `/data/cloud_projects`, or `/data/cloud_payment_orders`. Do not use public landing-page paths from record fields such as `/cloud-terms` unless the explicit intent is previewing the public website.',
286
- '- **Do not downgrade extension code to ES5 to appease tooling.** eApp extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix eApp extension checker/runtime contract instead of rewriting generated extension code around the limitation.',
300
+ '- **Admin menu visibility is permission-driven, not RLS:** Cloud/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 a Cloud menu merely because an extension exists or because the path is hardcoded. Example: `/cloud/hosts` menu should require `{ or: [{ route: "/cloud/admin/hosts", methods: ["GET"] }, { route: "/cloud_servers", methods: ["GET"] }] }`.',
301
+ '- **PermissionGate is mandatory inside admin extensions:** every sensitive action button, form, mutation, destructive workflow, and data shortcut must be wrapped in `PermissionGate` or guarded with `usePermissions()` before rendering/enabling. Default gates: list/detail visibility needs `methods: ["GET"]`; create and custom flow-trigger routes usually need `methods: ["POST"]`; native record edits need `methods: ["PATCH"]`; native delete routes need `methods: ["DELETE"]`. Root admin still passes through normal permission helpers, but extension code must not rely on root-only assumptions.',
302
+ '- **Extension permission UX:** if the current user can read a page but cannot perform an action, hide the action by default. If hiding would confuse the workflow, render a disabled state with a short reason. Never let the button render active and depend only on the server rejection; server permissions are the final boundary, not the UI contract.',
303
+ '- **Cloud project admin operations:** use canonical `cloud_projects` table routes as the single source of truth. Admin manual create uses `POST /cloud_projects` with schema-safe fields `owner: { id }`, `plan: { id }`, `admin_email`, `admin_password`, `name`, `subdomain`, and `status: "creating"`. The UI must show tenant admin credential as an email/password pair and include a generate-password action before create; do not build an email-only credential form. Project detail is the place for destructive lifecycle actions. Disable uses `PATCH /cloud_projects/:id` with body `{ status: "disabled" }` and `confirm_tenant_id`/`confirm_hash` in query params. Enable uses `PATCH /cloud_projects/:id` with body `{ status: "running" }`. Delete uses `DELETE /cloud_projects/:id` with typed `confirm_tenant_id`, returned `requiredConfirmHash`, and the matching hash before triggering `cloud-delete-project`. Do not create separate one-off `/cloud/admin/projects/*` action routes for create/disable/enable/delete when the canonical table route can own the workflow.',
304
+ '- **Cloud admin terminology:** in Cloud admin UI, call physical tenant workloads "projects" everywhere. Do not label creation or details as "instance" unless the user explicitly asks for that word.',
305
+ '- **Cloud project create UI:** manual Cloud project creation should use `CommonDrawer`, not a wide modal. Let the operator search/select `user_definition` by email and select a plan with cards; do not expose duplicate free-text `user id` or `plan id` inputs when selectors exist. Prefer sending only the selected owner id, plan id, and required workflow fields such as `expiredAt` when the canonical handler can derive customer email, project name, subdomain, and password. Expiry selection should use quick presets plus a manual calendar (`UCalendar` when available, loaded through `install_package`/`getPackages` if an app package is needed).',
306
+ '- **Cloud host settings and creation UI:** host settings store only provider selection codes Enfyra controls, currently location and server type. Do not expose or save provider-derived RAM, disk, vCPU, or cost values by hand. Query the provider catalog route, show real package/location cards, support load-more/search when the list is long, and snapshot provider facts onto `cloud_servers` only during host creation.',
307
+ '- **Flow schedule UI:** schedule trigger editors must keep the server contract as `triggerConfig.cron` and `triggerConfig.timezone`, but the UI should not be a bare cron field plus giant timezone dropdown. Provide common cadence presets, readable current-schedule summary, searchable access to all IANA timezones, suggested timezone shortcuts, and a custom cron escape hatch so operators can configure recurring checks without remembering cron syntax.',
308
+ '- **Admin operation UI:** use eApp `CommonModal` for compact create, disable, delete, and multi-field confirmation workflows. Use `CommonDrawer` for longer setup workflows such as Cloud host settings, host creation, project creation, and provider/package selection. Open the modal/drawer immediately on click, then render loading/error/content inside it; do not wait for async fetches to finish before showing the shell.',
309
+ '- **FormEditor is preferred for table-record forms:** when an extension creates or edits a concrete table record such as `cloud_host_settings`, `cloud_servers`, or `cloud_projects`, use `FormEditor`/`FormEditorLazy` inside the modal/page when the form is a direct table edit. Customize layout with `sections`, `includes`, and `field-map`; reserve custom inputs only for workflow-specific fields that are not table columns.',
310
+ '- **Modal form layout:** inside `CommonModal`, stack each form control vertically with label text above a full-width input/control. Use a small grid/space stack such as `grid gap-4`, `p.text-sm.font-medium`, then `UInput class="mt-2 w-full"`. Do not place modal labels and inputs side by side unless the user explicitly asks for a dense horizontal form.',
311
+ '- **Confirmation modal flow:** destructive/admin confirmation modals must read top-to-bottom as the operator workflow. For server-hash confirmations, render: tenant/id input first, then a full-width `Request hash` button, then a disabled hash input that is auto-filled by the server response, then the final destructive action in the footer. The final action stays disabled until the typed id matches and the server hash has been requested. Do not ask operators to manually type or edit the hash.',
312
+ '- **Cloud host deletion:** do not delete `cloud_servers` directly from an extension or custom route. Root admin host deletion must be detail-only from `/cloud/hosts/:id`, call `POST /cloud/admin/hosts/delete`, require typed `confirm_host_id`, block when any project is running or attached, return a server-generated `requiredConfirmHash` for an empty host, and require the hash back via `confirmHash` query before triggering `cloud-delete-host`.',
313
+ '- **Cloud host reconciliation is host-rooted:** use `cloud_reconciliation_reports` as the persisted audit source for host/control-plane/runtime mismatch checks, and use `cloud_servers.projects` as the canonical inverse list of physical projects attached to a host. Do not model reconciliation from the project side or make projects manage themselves; the host owns capacity and runtime scan responsibility. `cloud-reconcile-hosts` is a scheduled flow using `triggerConfig.cron`/`timezone`; `POST /cloud/admin/hosts/reconcile` should only trigger that flow for a host and return job metadata. Host list/detail pages should consume `/cloud/admin/hosts`, which includes the latest `reconciliation_report`, instead of querying raw report rows separately.',
314
+ '- **Do not downgrade extension code to ES5 to appease tooling.** eApp extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix eApp extension checker/runtime contract instead of rewriting generated extension code around the limitation. Vue extension diagnostics should only TypeScript-check `<script setup lang="ts">`; plain `<script setup>` extension code is JavaScript and must not emit TypeScript diagnostics into the form.',
287
315
  '',
288
316
  '#### Injected Vue API functions:',
289
317
  '- Reactivity: `ref`, `reactive`, `computed`, `readonly`, `shallowRef`, `shallowReactive`',
@@ -302,12 +330,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
302
330
  '#### Injected Enfyra composables:',
303
331
  '- **useApi:** API client with auto error handling & toast. Returns `{ data, error, pending, status, execute, refresh }`. Does NOT auto-run — must call `execute()`. Example: `const { data, execute } = useApi(\'/user_definition\', { query: { limit: 10 } }); onMounted(() => execute());`',
304
332
  '- **useAuth:** Authentication. Returns `{ me, login, logout, fetchUser, isLoggedIn, isLoading, oauthLogin }`. `me` is reactive user object with `isRootAdmin`, `role`, `allowedRoutePermissions`.',
305
- '- **usePermissions:** Permission checks. Returns `{ hasPermission(route, method), hasAnyPermission(routes, actions), hasAllPermissions(routes, actions), checkPermissionCondition(condition) }`. Actions: `read`, `create`, `update`, `delete`.',
333
+ '- **usePermissions:** Permission checks. Returns `{ hasPermission(route, method), hasAnyPermission(routes, methods), hasAllPermissions(routes, methods), checkPermissionCondition(condition) }`. Permission conditions use HTTP method names directly, for example `{ or: [{ route: "/posts", methods: ["GET"] }] }`.',
306
334
  '- **useSchema:** Schema management. Returns `{ schemas, schema, fetchSchema, schemaLoading, definition, fieldMap, getField(key), editableFields, generateEmptyForm(), validate(record), getIncludeFields(), useFormChanges() }`.',
307
335
  '- **useGlobalState:** Global app state. Returns `{ settings, storageConfigs, aiConfigs, appPackages, sidebarVisible, sidebarCollapsed, routeLoading, toggleSidebar(), setRouteLoading(), fetchAppPackages(), packageCacheState }`.',
308
336
  '- **useScreen:** Responsive helpers. Returns `{ width, height, isMobile, isTablet, isDesktop, isLargeDesktop, screenType }`.',
309
337
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
310
- '- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ id, label, onClick, color, icon, order, side, global }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component }`.',
338
+ '- **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.',
311
339
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
312
340
  '- **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).',
313
341
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
@@ -319,12 +347,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
319
347
  '- **useMounted:** Mount state helper.',
320
348
  '',
321
349
  '#### Injected UI Components (auto-resolved):',
322
- '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
350
+ '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `CommonModal`, `CommonDrawer`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
323
351
  '- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
324
352
  '- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
325
353
  '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
326
354
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
327
- '- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
355
+ '- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
328
356
  '- **Tabs:** `UTabs` is available in current eApp extension runtime. Use it for page-level sections when a page would otherwise become too long.',
329
357
  '- **Extension:** `Widget` — embed widget extension via `<Widget :id="extensionId" />`',
330
358
  '- **WebSocket:** `WebSocketManager`',
@@ -346,14 +374,15 @@ export function buildMcpServerInstructions(apiBaseUrl) {
346
374
  '- **Search first with `search_npm`** if unsure of exact package name.',
347
375
  '- **Server** packages → available as `$ctx.$pkgs.packageName` in handlers/hooks.',
348
376
  '- **App** packages → available via `getPackages([\'dayjs\'])` in extensions (call in `onMounted`).',
377
+ '- **Extension package imports:** Do not write static imports like `import { CalendarDate } from "@internationalized/date"` inside `extension_definition.code`; the extension builder does not resolve app packages that way. Install the package as type `App`, then load it inside the extension with `const pkgs = await getPackages(["@internationalized/date"]); const { CalendarDate } = pkgs["@internationalized/date"];`.',
349
378
  '- **Do NOT use `create_record` on `package_definition` directly** — use `install_package` instead.',
350
379
  '',
351
380
  '#### Important patterns:',
352
381
  '- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
353
382
  '- **Header actions:** `useHeaderActionRegistry([{ id: \'refresh\', label: \'Refresh\', onClick: fn, color: \'primary\', icon: \'lucide:refresh\', order: 0 }])`',
354
383
  '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
355
- '- **Permissions:** Use `checkPermissionCondition({ or: [{ route: \'/posts\', actions: [\'read\'] }] })` for complex rules.',
356
- '- **After create/update:** Tell user to refresh (F5). Changes may not appear until reload.',
384
+ '- **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.',
385
+ '- **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.',
357
386
  '',
358
387
  '#### Minimal example:',
359
388
  '`<template><div class="p-6"><h1 class="text-2xl font-bold">{{ title }}</h1><UButton @click="handleClick">Click</UButton></div></template><script setup>const title = ref(\'My Extension\'); const toast = useToast(); const handleClick = () => toast.add({ title: \'Clicked\', color: \'green\' });</script>`',
@@ -1831,7 +1831,7 @@ server.tool(
1831
1831
  'create_extension',
1832
1832
  [
1833
1833
  '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).',
1834
- 'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget no menu needed. Server auto-compiles; tell user to refresh (F5) after create. See extension rules in MCP instructions.',
1834
+ '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.',
1835
1835
  ].join(' '),
1836
1836
  {
1837
1837
  name: z.string().describe('Extension name (unique)'),
@@ -1849,7 +1849,7 @@ server.tool(
1849
1849
  delete body.menuId;
1850
1850
  }
1851
1851
  const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(body) });
1852
- return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}). Tell user to refresh (F5) to see it.\n${JSON.stringify(result, null, 2)}` }] };
1852
+ return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}). Open eApp tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
1853
1853
  },
1854
1854
  );
1855
1855