@enfyra/mcp-server 0.0.79 → 0.0.81

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/README.md CHANGED
@@ -1,66 +1,55 @@
1
1
  # Enfyra MCP Server
2
2
 
3
- MCP server for managing Enfyra instances from **Codex**, **Claude Code**, **Cursor**, and other MCP-compatible clients. All operations go through Enfyra's REST API.
3
+ Manage Enfyra instances from MCP-compatible coding tools such as **Codex**, **Claude Code**, **Cursor**, MCP Inspector, and other STDIO MCP hosts.
4
4
 
5
+ This package is the MCP bridge only. Assistant rules, schema behavior, dynamic script guidance, and examples are served through the MCP server itself from `src/lib/mcp-instructions.js`, `src/lib/mcp-examples.js`, and tool descriptions in `src/mcp-server-entry.mjs`.
5
6
 
6
- **LLM rules (REST, GraphQL, auth, URL, mutation `create_{tableName}`, etc.):** not in this README — see **`src/lib/mcp-instructions.js`** (content sent via MCP `instructions`), **`src/lib/mcp-examples.js`** (concrete examples loaded through `get_enfyra_examples`), and tool descriptions in **`src/mcp-server-entry.mjs`**. This README only covers **MCP installation and configuration** for users/devs.
7
+ ## Quick Start
7
8
 
8
- **Official docs:** [Claude Code MCP](https://docs.anthropic.com/en/docs/claude-code/mcp) · [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings) · [Cursor MCP (`mcp.json`)](https://cursor.com/docs/context/mcp)
9
-
10
- ---
11
-
12
- ## Quick local setup (`config` command)
13
-
14
- From your **Enfyra project root**:
9
+ From your project root:
15
10
 
16
11
  ```bash
17
12
  npx @enfyra/mcp-server config
18
13
  ```
19
14
 
20
- - **Interactive (default in a terminal):** first asks **where** to write config with an arrow-key selector — Claude Code, Cursor, Codex, or all unless you already passed target flags. Then prompts for `ENFYRA_API_URL` and `ENFYRA_API_TOKEN` when missing. Press **Enter** to accept bracketed defaults from env or existing `enfyra` config.
21
- - **Re-run anytime** to update the same files; other entries under `mcpServers` are preserved.
22
- - **Non-interactive** (CI / scripts): `npx @enfyra/mcp-server config --yes` plus optional `-a` / `-t` and/or env vars.
23
- - **One host only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json`. `--cursor` / `--cursor-only` → `./.cursor/mcp.json`. `--codex` / `--codex-only` → `./.codex/config.toml`. Pass multiple target flags to write each selected host.
24
- - **Reconfigure:** `npx @enfyra/mcp-server config --reconfig` prompts for the target host again, uses existing project values as defaults, and replaces the old project `enfyra` entry for that host.
25
- - **Global/user config:** add `--global` only when you intentionally want the selected host config under your home directory instead of this project.
26
- - **Help:** `npx @enfyra/mcp-server -h` or `npx @enfyra/mcp-server config --help`
15
+ The config command can write project config for Codex, Claude Code, Cursor, or all supported clients. It preserves other MCP servers and replaces only the `enfyra` entry.
27
16
 
28
- Equivalent in this repo: `yarn mcp:config` (Yarn v1 reserves `yarn config` for registry settings). Same as `node src/index.mjs config` / `npm run mcp:config`.
17
+ Interactive setup asks for `ENFYRA_APP_URL` first, then asks for `ENFYRA_API_TOKEN`. The MCP API base is inferred as `<app>/api`, and the token page is inferred as `<app>/me`. For example, `https://demo.enfyra.io` becomes API base `https://demo.enfyra.io/api` and token page `https://demo.enfyra.io/me`.
29
18
 
30
- ---
19
+ ```bash
20
+ # Non-interactive, all supported clients
21
+ npx @enfyra/mcp-server config --yes \
22
+ --app-url http://localhost:3000 \
23
+ -t efy_pat_your-token
31
24
 
32
- ## Which coding tool? (switch)
25
+ # One or more clients
26
+ npx @enfyra/mcp-server config --codex
27
+ npx @enfyra/mcp-server config --cursor --claude-code
28
+ ```
33
29
 
34
- Use this table to see **where** each host stores config. The **`mcpServers.enfyra` JSON block** at the bottom of each section is identical; only the **file paths** and **CLI** differ.
30
+ Equivalent in this repo:
35
31
 
36
- | | **Codex** | **Claude Code** | **Cursor** |
37
- |---|-----------|-----------------|------------|
38
- | **Project (repo, default)** | **`.codex/config.toml`** in the project | **`.mcp.json`** at repository root | **`.cursor/mcp.json`** in the project |
39
- | **Global (explicit `--global`)** | `~/.codex/config.toml` | `~/.mcp.json` from this helper, or Claude's `~/.claude.json` via `claude mcp add --scope user` | `~/.cursor/mcp.json` |
40
- | **Typical install** | `npx @enfyra/mcp-server config --codex` | `npx @enfyra/mcp-server config --claude-code` | `npx @enfyra/mcp-server config --cursor` |
41
- | **Precedence / merge** | Project config is merged/replaced for `enfyra` | Project `.mcp.json` is merged/replaced for `enfyra` | Project `.cursor/mcp.json` is merged/replaced for `enfyra` |
42
- | **Gotcha** | Open this folder in a new Codex session after editing config | Do not put MCP server definitions in `.claude/settings.json` | Root **`.mcp.json`** is for Claude Code project scope, not Cursor — use **`.cursor/mcp.json`** for Cursor |
32
+ ```bash
33
+ yarn mcp:config
34
+ ```
43
35
 
44
- Expand **one** block below for step-by-step setup.
36
+ ## Choose A Client
45
37
 
46
- <details open>
47
- <summary><strong>Codex</strong> — setup</summary>
38
+ | Client | Command | Project config |
39
+ |--------|---------|----------------|
40
+ | Codex | `npx @enfyra/mcp-server config --codex` | `.codex/config.toml` |
41
+ | Claude Code | `npx @enfyra/mcp-server config --claude-code` | `.mcp.json` |
42
+ | Cursor | `npx @enfyra/mcp-server config --cursor` | `.cursor/mcp.json` |
43
+ | MCP Inspector / other hosts | Paste the shared STDIO config below | Host-specific `mcpServers` config |
48
44
 
49
- The config command writes project Codex config to `./.codex/config.toml` by default:
45
+ <details>
46
+ <summary><strong>Codex setup</strong></summary>
50
47
 
51
48
  ```bash
52
49
  npx @enfyra/mcp-server config --codex
53
50
  ```
54
51
 
55
- Non-interactive:
56
-
57
- ```bash
58
- npx @enfyra/mcp-server config --codex --yes \
59
- -a http://localhost:3000/api \
60
- -t efy_pat_your-token
61
- ```
62
-
63
- The generated TOML section is:
52
+ Generated project config:
64
53
 
65
54
  ```toml
66
55
  [mcp_servers.enfyra]
@@ -72,92 +61,57 @@ ENFYRA_API_URL = "http://localhost:3000/api"
72
61
  ENFYRA_API_TOKEN = "efy_pat_your-token"
73
62
  ```
74
63
 
75
- The config writer replaces only `[mcp_servers.enfyra]` and `[mcp_servers.enfyra.env]`; other Codex config and other MCP servers are preserved. Open this folder in a new Codex session after updating `./.codex/config.toml`. Use `--global --codex` only when you intentionally want `~/.codex/config.toml`.
76
-
77
- </details>
64
+ The writer replaces only `[mcp_servers.enfyra]` and `[mcp_servers.enfyra.env]`. Other Codex config and other MCP servers are preserved.
78
65
 
79
- <details open>
80
- <summary><strong>Claude Code</strong> — setup</summary>
66
+ After editing config, open the folder in a new Codex session so project config is loaded.
81
67
 
82
- MCP server definitions are **not** placed in `.claude/settings.json`; that folder is for other Claude Code settings.
68
+ Official reference: [Codex config](https://developers.openai.com/codex/config-reference).
83
69
 
84
- ### Choose scope (Claude Code)
85
-
86
- | Goal | Location | Claude Code scope | Typical use |
87
- |------|----------|-------------------|-------------|
88
- | Same Enfyra MCP in **every** project on your machine | **`~/.claude.json`** | **user** (`claude mcp add … --scope user`) | One admin stack you always use |
89
- | MCP only when this **repo is cwd**, private to you, often with secrets | **`~/.claude.json`** | **local** (default: `claude mcp add …` without `--scope project`) | Per-machine URLs or tokens; nothing committed |
90
- | **Team** / reproducible setup; commit config to git | **`.mcp.json`** at the **repository root** | **project** (`claude mcp add … --scope project`) | Shared onboarding; env expansion supported |
70
+ </details>
91
71
 
92
- **Precedence when the same server name exists in more than one place:** **local** → **project** (`.mcp.json`) → **user**. See the [official MCP docs](https://docs.anthropic.com/en/docs/claude-code/mcp).
72
+ <details>
73
+ <summary><strong>Claude Code setup</strong></summary>
93
74
 
94
- **Project `.mcp.json` approval:** Claude Code may prompt before trusting project-scoped servers; use `claude mcp reset-project-choices` to reset.
75
+ ```bash
76
+ npx @enfyra/mcp-server config --claude-code
77
+ ```
95
78
 
96
- ### `claude mcp add` user, local, or project
79
+ Project config is written to `.mcp.json`. MCP server definitions do not belong in `.claude/settings.json`.
97
80
 
98
- Use the CLI (recommended). **User** and **local** configs are stored in **`~/.claude.json`**; **project** (`--scope project`) writes **`./.mcp.json`** at the repo root.
81
+ Claude Code also supports its own CLI:
99
82
 
100
83
  ```bash
101
- # User scope — available in all projects (options before server name per Claude Code docs)
102
- claude mcp add --transport stdio --scope user \
103
- --env ENFYRA_API_URL=http://localhost:3000/api \
104
- --env ENFYRA_API_TOKEN=efy_pat_your-token \
105
- enfyra -- npx -y @enfyra/mcp-server
106
-
107
- # Local scope (default) — only when this repo is cwd; still stored in ~/.claude.json under project path
108
- claude mcp add --transport stdio \
109
- --env ENFYRA_API_URL=http://localhost:3000/api \
110
- --env ENFYRA_API_TOKEN=efy_pat_your-token \
111
- enfyra -- npx -y @enfyra/mcp-server
112
-
113
- # Project scope — writes/updates .mcp.json at repo root (good for teams)
114
84
  claude mcp add --transport stdio --scope project \
115
85
  --env ENFYRA_API_URL=http://localhost:3000/api \
116
86
  --env ENFYRA_API_TOKEN=efy_pat_your-token \
117
87
  enfyra -- npx -y @enfyra/mcp-server
118
88
  ```
119
89
 
120
- On **native Windows** (not WSL), stdio servers using `npx` often need the `cmd /c` wrapper see [Claude Code MCP — Windows](https://docs.anthropic.com/en/docs/claude-code/mcp).
121
-
122
- You can set env vars with **`--env`** (as above), edit **`~/.claude.json`** / **`.mcp.json`**, or use the `/mcp` UI.
123
-
124
- ### Manual JSON (Claude Code)
125
-
126
- Use inside **`.mcp.json`** `mcpServers`, or merge into **`~/.claude.json`** per [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings) for your scope. Reuse the **shared JSON** in the [Shared](#shared-enfyra-mcp-json-and-environment) section below.
90
+ Scope precedence when the same server name exists in multiple places is local, then project, then user. Project-scoped `.mcp.json` may require approval in Claude Code.
127
91
 
128
- ### `.mcp.json` only (Claude Code project, manual)
129
-
130
- If you skip the CLI, add **`mcpServers.enfyra`** to **`.mcp.json`** at the repository root. Official docs support **environment variable expansion** in `.mcp.json`.
131
-
132
- **Local dev (this monorepo):** point `command` / `args` / `cwd` at `node` and `src/index.mjs` inside your clone — see the sample **`.mcp.json`** in this repository (adjust `cwd` or use expansion).
92
+ Official references: [Claude Code MCP](https://docs.anthropic.com/en/docs/claude-code/mcp) and [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings).
133
93
 
134
94
  </details>
135
95
 
136
96
  <details>
137
- <summary><strong>Cursor</strong> setup</summary>
138
-
139
- Cursor reads MCP from **`mcp.json`** in two places ([Cursor docs](https://cursor.com/docs/context/mcp)):
97
+ <summary><strong>Cursor setup</strong></summary>
140
98
 
141
- | Scope | Path |
142
- |-------|------|
143
- | **Global** | `~/.cursor/mcp.json` (macOS/Linux) or `%USERPROFILE%\.cursor\mcp.json` (Windows) |
144
- | **Project** | **`.cursor/mcp.json`** inside the project (directory **`.cursor`** at repo root) |
145
-
146
- Paste the **same** `mcpServers` structure as in the [Shared](#shared-enfyra-mcp-json-and-environment) section. Cursor supports **interpolation**, e.g. `${env:ENFYRA_API_TOKEN}`, `${workspaceFolder}`, for secrets and paths.
99
+ ```bash
100
+ npx @enfyra/mcp-server config --cursor
101
+ ```
147
102
 
148
- Optional **STDIO** fields per Cursor: `type`, `command`, `args`, `env`, `envFile` see [STDIO server configuration](https://cursor.com/docs/context/mcp).
103
+ Cursor project config is written to `.cursor/mcp.json`. Global config is `~/.cursor/mcp.json` on macOS/Linux or `%USERPROFILE%\.cursor\mcp.json` on Windows.
149
104
 
150
- **After edits:** restart Cursor (or toggle the server under **Settings Features → Model Context Protocol**). Use **Output → MCP Logs** if the server fails to start.
105
+ After edits, restart Cursor or reload MCP, then confirm the server under Cursor MCP settings. Use MCP logs if the server fails to start.
151
106
 
152
- **Using both Cursor and Claude Code in one repo:** keep **`.cursor/mcp.json`** for Cursor and **`.mcp.json`** (root) for Claude Code **project** scope if needed — they are different files.
107
+ Official reference: [Cursor MCP](https://cursor.com/docs/context/mcp).
153
108
 
154
109
  </details>
155
110
 
156
- ---
157
-
158
- ## Shared: Enfyra MCP JSON and environment
111
+ <details>
112
+ <summary><strong>Other MCP hosts and MCP Inspector</strong></summary>
159
113
 
160
- Use this block in any host-specific `mcp.json` / `mcpServers` merge (adjust env or use `${env:…}` where your editor supports it).
114
+ Use the shared STDIO config with any host that accepts an `mcpServers` JSON block:
161
115
 
162
116
  ```json
163
117
  {
@@ -174,61 +128,128 @@ Use this block in any host-specific `mcp.json` / `mcpServers` merge (adjust env
174
128
  }
175
129
  ```
176
130
 
177
- - `-y`: auto-confirm `npx` package install without prompting.
178
- - **Restart** the coding tool after manual file edits.
131
+ `ENFYRA_API_TOKEN` is a programmatic token from the Enfyra admin UI `/me`. It is not a JWT; the MCP server exchanges it through `POST {ENFYRA_API_URL}/auth/token/exchange` before calling Enfyra REST APIs.
132
+
133
+ Official reference: [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
134
+
135
+ </details>
136
+
137
+ ## Config Command
138
+
139
+ ```bash
140
+ npx @enfyra/mcp-server config [options]
141
+ ```
142
+
143
+ | Option | Use |
144
+ |--------|-----|
145
+ | `--app-url` | Set the Enfyra app/admin URL; setup writes `<app>/api` to MCP config |
146
+ | `--api-token`, `-t` | Set `ENFYRA_API_TOKEN` |
147
+ | `--yes` | Non-interactive mode for CI/scripts |
148
+ | `--global` | Write global/user config instead of project config |
149
+ | `--reconfig` | Prompt for target clients again and replace the existing `enfyra` entry |
150
+ | `--codex` | Write Codex config |
151
+ | `--claude-code`, `--claude` | Write Claude Code config |
152
+ | `--cursor` | Write Cursor config |
153
+ | `-h`, `--help` | Show CLI help |
154
+
155
+ Without a target flag, interactive mode asks which client to configure. Non-interactive mode defaults to all supported clients.
156
+
157
+ ## Environment
179
158
 
180
159
  | Variable | Description | Default |
181
160
  |----------|-------------|---------|
182
- | `ENFYRA_API_URL` | Base for REST + GraphQL + auth through the Nuxt/app proxy | `http://localhost:3000/api` |
183
- | `ENFYRA_API_TOKEN` | Programmatic token from eApp `/me`. MCP exchanges it through `/auth/token/exchange` for an access token. | — |
161
+ | `ENFYRA_APP_URL` | App/admin URL used by setup to derive `ENFYRA_API_URL=<app>/api` and token page `<app>/me` | `http://localhost:3000` |
162
+ | `ENFYRA_API_URL` | Runtime API base written into MCP client config | Derived from `ENFYRA_APP_URL` |
163
+ | `ENFYRA_API_TOKEN` | Programmatic token from the Enfyra admin UI `/me` | Required |
164
+
165
+ For normal apps and demos, enter the app/admin URL such as `http://localhost:3000` or `https://demo.enfyra.io`. Treat the direct Enfyra backend host as private infrastructure unless you are debugging Enfyra core/server internals.
184
166
 
185
- `ENFYRA_API_TOKEN` is a long-lived programmatic token, not a JWT. MCP must never send it directly as `Authorization: Bearer <token>` to REST tools. The MCP client first calls `POST {ENFYRA_API_URL}/auth/token/exchange` with `{ "apiToken": ENFYRA_API_TOKEN }`, caches the returned `accessToken`, and uses that JWT as the Bearer token for subsequent requests.
167
+ ## Common Examples
186
168
 
187
- Schema and script tools include safety guards for LLM callers: generic record mutations validate request fields against live metadata, script-backed records must validate `sourceCode` before save through `/admin/script/validate` and fail closed if validation is unavailable, relation metadata rejects physical FK/junction inputs, custom routes reject `mainTableId` unless the path is the canonical table route, schema tools serialize table/column/relation changes, and destructive deletes require `confirm=true` after returning a preview.
169
+ Use `get_enfyra_examples` from the MCP tool list when asking an LLM to generate implementation patterns. It returns focused examples for:
188
170
 
189
- Read tools use Enfyra's `fields` parameter directly. Passing explicit includes such as `fields=id,email` returns only those fields, while any `-field` token switches that scope to exclude mode. For example, `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Nested exclusions work with dotted fields and `deep`, such as `fields=-owner.avatar` or `deep.owner.fields=-avatar`.
171
+ - SSR app auth, OAuth, and proxy setup
172
+ - schema, columns, relations, indexes, and validation
173
+ - query filters, sorting, fields, deep relations, and aggregates
174
+ - handlers, hooks, permissions, and RLS
175
+ - websocket gateways and events
176
+ - flows
177
+ - files and storage
178
+ - Enfyra admin extensions
190
179
 
191
- Generated RLS for canonical table reads must keep projection and pagination client-owned. Pre-hooks may merge owner or membership constraints into `@QUERY.filter`, but they must not override `@QUERY.fields`, `@QUERY.deep`, `@QUERY.sort`, `@QUERY.limit`, `@QUERY.page`, `@QUERY.meta`, `@QUERY.aggregate`, or `debugMode`. Fixed projections belong only on clearly custom summary or workflow endpoints with an explicit response contract.
180
+ ## OAuth Setup
192
181
 
193
- Quick checklist for a new LLM using Enfyra MCP: discover the live system first, inspect the specific table/route, load the matching example category, mutate with explicit fields and relation property names, validate or test scripts/routes before relying on them, re-read the saved row when mutation output is summarized, and preview destructive operations before confirming.
182
+ OAuth has three different URLs:
194
183
 
195
- Use `trace_metadata_usage` before changing production features to find related tables, routes, handlers, hooks, flow steps, websocket scripts, GraphQL scripts, and bootstrap scripts. Use `get_script_source` to read full untruncated source plus a SHA-256 hash. Prefer `patch_script_source` for focused exact search/replace edits; it previews by default and, with `apply=true`, validates the patched `sourceCode` through `/admin/script/validate` before saving. Use `update_script_source` when replacing an entire existing script. Use generic `update_record` only for small record patches or patches that include non-script metadata fields.
184
+ | URL | Meaning |
185
+ |-----|---------|
186
+ | Provider callback URL | `{ENFYRA_API_URL}/auth/{provider}/callback` |
187
+ | Enfyra `redirectUri` | Must exactly match the provider callback URL |
188
+ | App `redirect` query | Where Enfyra sends the browser after cookies are set |
196
189
 
197
- For route contracts that intentionally keep workflow fields out of request bodies, generic `create_record`, `update_record`, and `delete_record` accept optional `queryParams` as a JSON object string. For example, a renewal workflow can keep `expires_at=YYYY-MM-DD` in the URL query while `validateBody` remains enabled for the table body.
190
+ Example Google callback when `ENFYRA_API_URL=http://localhost:3000/api`:
198
191
 
199
- ### `ENFYRA_API_URL` — use the app proxy
192
+ ```text
193
+ http://localhost:3000/api/auth/google/callback
194
+ ```
200
195
 
201
- For normal apps and demos, set `ENFYRA_API_URL` to the Nuxt/app proxy:
196
+ Start OAuth from the app proxy:
202
197
 
203
198
  ```text
204
- http://localhost:3000/api
199
+ /enfyra/auth/google?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra
205
200
  ```
206
201
 
207
- The Enfyra backend is private infrastructure. MCP, browser code, SSR routes, GraphQL calls, and generated app code should go through the app origin `/api/**`; do not connect them directly to the backend host/port. Direct backend URLs are only for Enfyra core/server debugging when you intentionally bypass the app proxy.
202
+ `appCallbackUrl` is only for manual-token apps that intentionally read token query parameters. SSR apps should prefer proxy-owned cookies.
203
+
204
+ ## Runtime Safety
205
+
206
+ The MCP server includes safety guards for LLM callers:
207
+
208
+ - Generic record mutations validate fields against live metadata.
209
+ - Script-backed records validate `sourceCode` through `/admin/script/validate` before saving.
210
+ - Relation tools reject physical FK/junction names.
211
+ - Custom route tools reject `mainTableId` unless the route is the canonical table route.
212
+ - Schema changes are serialized.
213
+ - Destructive deletes return a preview before requiring `confirm=true`.
214
+
215
+ ## Query Notes
208
216
 
209
- ### SSR app auth pattern
217
+ Use explicit `fields` in read tools. Include mode is the default, such as `fields=id,email`. Any excluded field switches that scope to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Dotted exclusions such as `fields=-owner.avatar` work for relation fields when the relation exists in metadata.
210
218
 
211
- When an LLM builds a Nuxt, Next, or other SSR frontend for Enfyra, follow the same-origin proxy pattern:
219
+ ## Enfyra URL Pattern
212
220
 
213
- - Browser code calls a same-origin proxy such as `{{ appOrigin }}/enfyra/**`, never the raw Enfyra backend URL.
214
- - Nuxt can proxy it with `routeRules: { "/enfyra/**": { proxy: { to: `${API_URL}/**`, fetchOptions: { redirect: "manual" } } } }`. Keep redirects manual so OAuth set-cookie redirects reach the browser as real HTTP redirects with `Set-Cookie`.
215
- - Generated apps should not create custom login/logout/me routes that manually set `accessToken`, `refreshToken`, or `expTime` cookies when the proxy is enough.
216
- - Password login is `POST /enfyra/login`, not `/enfyra/auth/login`.
217
- - Fetch the current user with `GET /enfyra/me` and logout with `POST /enfyra/logout`.
218
- - OAuth starts through the same proxy prefix, for example `/enfyra/auth/google?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra`. `redirect` must include the app origin, and `cookieBridgePrefix` is the same proxy prefix that reaches Enfyra API routes. Enfyra validates the redirect, exchanges OAuth on its callback, then redirects through `{redirect.origin}{cookieBridgePrefix}/auth/set-cookies` so the third app origin stores the cookies before returning to `redirect`.
219
- - Socket.IO browser clients use a same-origin bridge too. 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/**`. The backend gateway metadata path remains `/chat`.
220
- - Use token-query OAuth callback pages only for non-SSR/manual-token apps.
221
+ Generated apps should use a same-origin proxy:
221
222
 
222
- ---
223
+ ```js
224
+ export default defineNuxtConfig({
225
+ routeRules: {
226
+ "/enfyra/**": {
227
+ proxy: {
228
+ to: `${process.env.ENFYRA_API_URL}/**`,
229
+ fetchOptions: { redirect: "manual" }
230
+ }
231
+ }
232
+ }
233
+ })
234
+ ```
235
+
236
+ Browser code then calls:
237
+
238
+ ```text
239
+ POST /enfyra/login
240
+ GET /enfyra/me
241
+ POST /enfyra/logout
242
+ GET /enfyra/<table>
243
+ ```
223
244
 
224
- ## Tools (summary)
245
+ Do not create custom login/logout/me routes that manually set Enfyra token cookies when the proxy is enough.
225
246
 
226
- Metadata, examples, query/CRUD, method management, route access audit/grant, route/handler/hook, tables/columns, reload cache, logs, user/roles, login, menu/extension, `get_enfyra_api_context`. For full tool list and behavior, see the app after enabling MCP or the source in `src/mcp-server-entry.mjs`.
247
+ ## Tool Summary
227
248
 
228
- Use `get_enfyra_examples` when asking an LLM to generate concrete Enfyra implementation patterns. It returns categorized examples for SSR app auth/OAuth/proxy setup, schema/relations, queries/deep, handlers/hooks, permissions/RLS, websocket, flows, files, and extensions.
249
+ The MCP server exposes tools for metadata discovery, examples, query/CRUD, method management, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, and `get_enfyra_api_context`.
229
250
 
230
- For authenticated route access, use `audit_route_access` before changing permissions and `ensure_route_access` to grant access by `path` plus `roleName`/`roleId` or `allowedUserIds`. `ensure_route_access` resolves route, role, and method ids, validates that requested methods are available on the route, merges existing methods by default, and reloads routes.
251
+ For authenticated route access, use `audit_route_access` before changing permissions and `ensure_route_access` to grant access by route path plus role/user. For production script edits, use `trace_metadata_usage`, `get_script_source`, and `patch_script_source` so changes are targeted, hash-checked, and validated.
231
252
 
232
253
  ## Security
233
254
 
234
- API calls use JWT (MCP auto-refreshes). Permissions are enforced by Enfyra.
255
+ API calls use exchanged JWTs and Enfyra permissions are still enforced server-side. Keep `ENFYRA_API_TOKEN` out of committed config unless the project intentionally uses environment interpolation or another secret-management path.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.79",
4
- "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
3
+ "version": "0.0.81",
4
+ "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "src/index.mjs",
@@ -6,50 +6,103 @@ import { dirname, join } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
 
8
8
  const SERVER_KEY = 'enfyra';
9
+ const forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== '0';
10
+ const canStyle = forceColor || (output.isTTY && process.env.NO_COLOR == null);
11
+ const style = {
12
+ bold: value => canStyle ? `\x1B[1m${value}\x1B[22m` : value,
13
+ dim: value => canStyle ? `\x1B[2m${value}\x1B[22m` : value,
14
+ cyan: value => canStyle ? `\x1B[36m${value}\x1B[39m` : value,
15
+ green: value => canStyle ? `\x1B[32m${value}\x1B[39m` : value,
16
+ magenta: value => canStyle ? `\x1B[35m${value}\x1B[39m` : value,
17
+ blue: value => canStyle ? `\x1B[34m${value}\x1B[39m` : value,
18
+ yellow: value => canStyle ? `\x1B[33m${value}\x1B[39m` : value,
19
+ underline: value => canStyle ? `\x1B[4m${value}\x1B[24m` : value,
20
+ inverse: value => canStyle ? `\x1B[7m${value}\x1B[27m` : value,
21
+ };
22
+
23
+ const clients = {
24
+ codex: {
25
+ label: 'Codex',
26
+ path: './.codex/config.toml',
27
+ color: style.green,
28
+ },
29
+ claude: {
30
+ label: 'Claude Code',
31
+ path: './.mcp.json',
32
+ color: style.magenta,
33
+ },
34
+ cursor: {
35
+ label: 'Cursor',
36
+ path: './.cursor/mcp.json',
37
+ color: style.cyan,
38
+ },
39
+ };
40
+
41
+ function statusIcon(kind) {
42
+ if (kind === 'success') return canStyle ? style.green('✓') : 'Done';
43
+ if (kind === 'warn') return canStyle ? style.yellow('!') : 'Warning';
44
+ return canStyle ? style.cyan('•') : '-';
45
+ }
46
+
47
+ function isCancelError(error) {
48
+ return error?.code === 'ABORT_ERR' || (error?.message || '') === 'Cancelled';
49
+ }
50
+
51
+ function exitCancelled() {
52
+ console.log('\nCancelled.');
53
+ process.exit(130);
54
+ }
9
55
 
10
56
  function printHelp() {
11
- console.log(`enfyra-mcp — write MCP config (Codex + Claude Code + Cursor)
57
+ console.log(`${style.bold('Enfyra MCP config')}
58
+ ${style.dim('Write local MCP client config for Enfyra.')}
12
59
 
13
- Usage:
60
+ ${style.bold('Usage')}
14
61
  npx @enfyra/mcp-server config [options]
15
62
 
16
- Writes project config under the current working directory:
17
- ./.mcp.json — Claude Code project scope
18
- ./.cursor/mcp.json — Cursor project scope
19
- ./.codex/config.toml — Codex project scope
63
+ ${style.bold('Supported clients')}
64
+ Codex ./.codex/config.toml
65
+ Claude Code ./.mcp.json
66
+ Cursor ./.cursor/mcp.json
67
+ Other MCP hosts can use the shared stdio JSON from the README.
20
68
 
21
- Options:
22
- --api-url, -a <url> ENFYRA_API_URL
23
- --api-token, -t <secret> ENFYRA_API_TOKEN
69
+ ${style.bold('Options')}
70
+ --app-url <url> Enfyra app/admin URL, for example https://demo.enfyra.io
71
+ --api-token, -t <secret> ENFYRA_API_TOKEN
24
72
  --global Write global/user config for selected hosts instead of project config
25
73
  --reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
26
74
  --yes Non-interactive: no prompts (CI / scripts); use CLI, env, existing file, then defaults
27
- Target — non-interactive default is all; with TTY and no target flags, choose with ↑/↓:
75
+
76
+ ${style.bold('Client selection')}
77
+ Non-interactive default is all supported clients. In a TTY with no target flags, choose with ↑/↓.
78
+
28
79
  --claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
29
80
  --cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
30
81
  --codex, --codex-only Only ./.codex/config.toml (Codex project scope)
31
82
  Passing multiple target flags writes each selected target.
83
+
32
84
  -h, --help Show this help
33
85
 
34
- Interactive mode: lets you choose Claude Code / Cursor / Codex / all if you did not pass target flags; then asks for URL / API token
35
- when missing. Existing project config is used as defaults. Re-run to update.
86
+ ${style.bold('Interactive mode')}
87
+ Choose Codex, Claude Code, Cursor, or all clients; then enter ENFYRA_APP_URL and ENFYRA_API_TOKEN.
88
+ The API base is inferred by appending /api to the app URL.
89
+ Existing Enfyra config and environment variables are used as defaults. Re-run anytime to update.
36
90
 
37
- Examples:
91
+ ${style.bold('Examples')}
38
92
  npx @enfyra/mcp-server config
93
+ npx @enfyra/mcp-server config --yes
94
+ npx @enfyra/mcp-server config --codex --cursor
39
95
  npx @enfyra/mcp-server config --claude-code
40
- npx @enfyra/mcp-server config --cursor --yes
41
- npx @enfyra/mcp-server config --codex --yes
42
96
  npx @enfyra/mcp-server config --global --codex
43
97
  npx @enfyra/mcp-server config --reconfig
44
- npx @enfyra/mcp-server config -a http://localhost:3000/api -t 'efy_pat_...'
45
- npx @enfyra/mcp-server config --yes
46
- ENFYRA_API_TOKEN=efy_pat_... npx @enfyra/mcp-server config --yes
98
+ npx @enfyra/mcp-server config --app-url http://localhost:3000 -t 'efy_pat_...'
99
+ ENFYRA_APP_URL=https://demo.enfyra.io ENFYRA_API_TOKEN=efy_pat_... npx @enfyra/mcp-server config --yes
47
100
  `);
48
101
  }
49
102
 
50
103
  function parseArgs(argv) {
51
104
  const out = {
52
- apiUrl: undefined,
105
+ appUrl: undefined,
53
106
  apiToken: undefined,
54
107
  claude: true,
55
108
  cursor: true,
@@ -75,7 +128,10 @@ function parseArgs(argv) {
75
128
  else if (a === '--yes') out.yes = true;
76
129
  else if (a === '--reconfig') out.reconfig = true;
77
130
  else if (a === '--global') out.global = true;
78
- else if (a === '--api-url' || a === '-a') out.apiUrl = next();
131
+ else if (a === '--app-url') out.appUrl = next();
132
+ else if (a === '--api-url' || a === '-a') {
133
+ throw new Error(`${a} is no longer supported for setup; use --app-url instead`);
134
+ }
79
135
  else if (a === '--api-token' || a === '-t') out.apiToken = next();
80
136
  else if (a === '--email' || a === '-e' || a === '--password' || a === '-p') {
81
137
  throw new Error(`${a} is no longer supported; use --api-token instead`);
@@ -105,6 +161,34 @@ function buildServerEntry(apiUrl, apiToken) {
105
161
  };
106
162
  }
107
163
 
164
+ function normalizeAppUrl(appUrl) {
165
+ const raw = String(appUrl || '').trim().replace(/\/+$/, '');
166
+ if (!raw) return '';
167
+ return raw.replace(/\/(?:api|enfyra)$/i, '') || raw;
168
+ }
169
+
170
+ function deriveApiUrlFromAppUrl(appUrl) {
171
+ const normalized = normalizeAppUrl(appUrl);
172
+ return normalized ? `${normalized}/api` : 'http://localhost:3000/api';
173
+ }
174
+
175
+ function deriveAppUrlFromApiUrl(apiUrl) {
176
+ return normalizeAppUrl(apiUrl);
177
+ }
178
+
179
+ function deriveMeUrl(appUrl) {
180
+ const normalized = normalizeAppUrl(appUrl);
181
+ return normalized ? `${normalized}/me` : '/me';
182
+ }
183
+
184
+ function resolveDefaultAppUrl(opts, existing) {
185
+ const appCandidate = opts.appUrl ?? process.env.ENFYRA_APP_URL;
186
+ if (appCandidate) return normalizeAppUrl(appCandidate);
187
+ const apiCandidate = process.env.ENFYRA_API_URL ?? existing.apiUrl;
188
+ if (apiCandidate) return deriveAppUrlFromApiUrl(apiCandidate);
189
+ return 'http://localhost:3000';
190
+ }
191
+
108
192
  async function mergeMcpFile(absPath, serverEntry) {
109
193
  let data = { mcpServers: {} };
110
194
  try {
@@ -160,7 +244,8 @@ async function mergeCodexConfig(absPath, apiUrl, apiToken) {
160
244
  if (!skip) kept.push(line);
161
245
  }
162
246
 
163
- const next = `${kept.join('\n').trimEnd()}\n\n${buildCodexTomlBlock(apiUrl, apiToken)}`;
247
+ const prefix = kept.join('\n').trimEnd();
248
+ const next = prefix ? `${prefix}\n\n${buildCodexTomlBlock(apiUrl, apiToken)}` : buildCodexTomlBlock(apiUrl, apiToken);
164
249
  await mkdir(dirname(absPath), { recursive: true });
165
250
  await writeFile(absPath, next, 'utf8');
166
251
  }
@@ -213,6 +298,13 @@ function getCursorConfigPath(root, globalScope) {
213
298
  return globalScope ? join(homedir(), '.cursor', 'mcp.json') : join(root, '.cursor', 'mcp.json');
214
299
  }
215
300
 
301
+ function getClientPath(client, root, globalScope) {
302
+ if (client === 'claude') return getClaudeConfigPath(root, globalScope);
303
+ if (client === 'cursor') return getCursorConfigPath(root, globalScope);
304
+ if (client === 'codex') return getCodexConfigPath(root, globalScope);
305
+ throw new Error(`Unknown MCP client: ${client}`);
306
+ }
307
+
216
308
  async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex, globalScope) {
217
309
  const paths = [];
218
310
  if (readClaude) paths.push(getClaudeConfigPath(root, globalScope));
@@ -246,19 +338,19 @@ async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex, gl
246
338
  async function promptTargetChoice() {
247
339
  const choices = [
248
340
  {
249
- label: 'Claude Code — project ./.mcp.json',
250
- value: { claude: true, cursor: false, codex: false },
341
+ client: 'codex',
342
+ value: { claude: false, cursor: false, codex: true },
251
343
  },
252
344
  {
253
- label: 'Cursor — project ./.cursor/mcp.json',
254
- value: { claude: false, cursor: true, codex: false },
345
+ client: 'claude',
346
+ value: { claude: true, cursor: false, codex: false },
255
347
  },
256
348
  {
257
- label: 'Codex — project ./.codex/config.toml',
258
- value: { claude: false, cursor: false, codex: true },
349
+ client: 'cursor',
350
+ value: { claude: false, cursor: true, codex: false },
259
351
  },
260
352
  {
261
- label: 'All',
353
+ client: 'all',
262
354
  value: { claude: true, cursor: true, codex: true },
263
355
  },
264
356
  ];
@@ -269,9 +361,9 @@ async function promptTargetChoice() {
269
361
  const rl = createInterface({ input, output });
270
362
  const line = (await rl.question(
271
363
  'Where should Enfyra MCP config be written?\n'
272
- + ' [1] Claude Code — ./.mcp.json\n'
273
- + ' [2] Cursor ./.cursor/mcp.json\n'
274
- + ' [3] Codex — ./.codex/config.toml\n'
364
+ + ' [1] Codex ./.codex/config.toml\n'
365
+ + ' [2] Claude Code ./.mcp.json\n'
366
+ + ' [3] Cursor ./.cursor/mcp.json\n'
275
367
  + ' [4] All [default]\n'
276
368
  + 'Choice [4]: ',
277
369
  )).trim().toLowerCase();
@@ -279,15 +371,15 @@ async function promptTargetChoice() {
279
371
  if (line === '' || line === '4' || line === 'all' || line === 'a') {
280
372
  return { claude: true, cursor: true, codex: true };
281
373
  }
282
- if (line === '1' || line === 'c' || line === 'claude') {
374
+ if (line === '1' || line === 'codex' || line === 'x') {
375
+ return { claude: false, cursor: false, codex: true };
376
+ }
377
+ if (line === '2' || line === 'claude' || line === 'claude-code') {
283
378
  return { claude: true, cursor: false, codex: false };
284
379
  }
285
- if (line === '2' || line === 'u' || line === 'cursor') {
380
+ if (line === '3' || line === 'cursor' || line === 'u') {
286
381
  return { claude: false, cursor: true, codex: false };
287
382
  }
288
- if (line === '3' || line === 'x' || line === 'codex') {
289
- return { claude: false, cursor: false, codex: true };
290
- }
291
383
  return { claude: true, cursor: true, codex: true };
292
384
  }
293
385
 
@@ -295,15 +387,35 @@ async function promptTargetSelect(choices, initialIndex = 0) {
295
387
  let selected = Math.max(0, Math.min(initialIndex, choices.length - 1));
296
388
  let renderedLines = 0;
297
389
 
390
+ const formatChoice = (choice, active) => {
391
+ const indicator = active ? style.cyan('◆') : style.dim('◇');
392
+ const accent = active ? style.cyan('│') : style.dim('│');
393
+ if (choice.client === 'all') {
394
+ const label = active ? style.bold(style.underline('All supported clients')) : 'All supported clients';
395
+ const paddedLabel = label + ' '.repeat(22 - 'All supported clients'.length);
396
+ const hint = active ? style.cyan('Codex + Claude Code + Cursor') : style.dim('Codex + Claude Code + Cursor');
397
+ return `${accent} ${indicator} ${paddedLabel} ${hint}`;
398
+ }
399
+
400
+ const meta = clients[choice.client];
401
+ const label = active ? style.bold(meta.color(meta.label)) : meta.color(meta.label);
402
+ const paddedLabel = label + ' '.repeat(Math.max(1, 22 - meta.label.length));
403
+ const path = active ? style.cyan(meta.path) : style.dim(meta.path);
404
+ return `${accent} ${indicator} ${paddedLabel} ${path}`;
405
+ };
406
+
298
407
  const render = () => {
299
408
  if (renderedLines > 0) {
300
409
  output.write(`\x1B[${renderedLines}A\x1B[0J`);
301
410
  }
302
411
  const lines = [
303
- 'Where should Enfyra MCP config be written?',
304
- ...choices.map((choice, index) => `${index === selected ? '›' : ' '} ${choice.label}`),
305
- '',
306
- 'Use ↑/↓ to move, Enter to select.',
412
+ `${style.cyan('◆')} ${style.bold('Enfyra MCP setup')}`,
413
+ `${style.dim('│')} ${style.dim('Choose where to write the project config.')}`,
414
+ style.dim(''),
415
+ ...choices.map((choice, index) => formatChoice(choice, index === selected)),
416
+ style.dim('│'),
417
+ style.dim('Choose one client config, or write all supported project configs.'),
418
+ `${style.dim('└')} ${style.dim('Use ↑/↓ to move, Enter to select, Ctrl+C to cancel.')}`,
307
419
  ];
308
420
  renderedLines = lines.length;
309
421
  output.write(`${lines.join('\n')}\n`);
@@ -352,31 +464,42 @@ async function promptTargetSelect(choices, initialIndex = 0) {
352
464
  }
353
465
 
354
466
  async function promptConfig(opts, existing) {
355
- let apiUrl = opts.apiUrl;
467
+ let appUrl = opts.appUrl ? normalizeAppUrl(opts.appUrl) : '';
356
468
  let apiToken = opts.apiToken;
357
- if (apiUrl !== undefined && apiToken !== undefined) {
358
- return { apiUrl: String(apiUrl).replace(/\/$/, ''), apiToken };
469
+ if (appUrl && apiToken !== undefined) {
470
+ return { apiUrl: deriveApiUrlFromAppUrl(appUrl), apiToken };
359
471
  }
360
472
 
361
473
  const rl = createInterface({ input, output });
362
- const q = (msg) => rl.question(msg);
363
-
364
- const defaultUrl = (
365
- opts.apiUrl ??
366
- process.env.ENFYRA_API_URL ??
367
- (existing.apiUrl || undefined) ??
368
- 'http://localhost:3000/api'
369
- ).replace(/\/$/, '');
370
- if (apiUrl === undefined) {
371
- const line = (await q(`ENFYRA_API_URL [${defaultUrl}]: `)).trim();
372
- apiUrl = line || defaultUrl;
474
+ const q = async (msg) => {
475
+ try {
476
+ return await rl.question(msg);
477
+ } catch (error) {
478
+ if (isCancelError(error)) {
479
+ throw new Error('Cancelled');
480
+ }
481
+ throw error;
482
+ }
483
+ };
484
+
485
+ const defaultAppUrl = resolveDefaultAppUrl(opts, existing);
486
+ if (!appUrl) {
487
+ console.log(`${style.cyan('◆')} ${style.bold('Connect to Enfyra')}`);
488
+ console.log(`${style.dim('│')} Enter the Enfyra app URL. The API base will be inferred as ${style.bold('<app>/api')}.`);
489
+ const line = (await q(`${style.dim('└')} ENFYRA_APP_URL ${style.dim(`[${defaultAppUrl}]`)}: `)).trim();
490
+ appUrl = normalizeAppUrl(line || defaultAppUrl);
373
491
  }
374
- apiUrl = String(apiUrl).replace(/\/$/, '');
492
+ const apiUrl = deriveApiUrlFromAppUrl(appUrl);
375
493
 
376
494
  const defaultApiToken = opts.apiToken ?? process.env.ENFYRA_API_TOKEN ?? existing.apiToken ?? '';
377
495
  if (apiToken === undefined) {
378
496
  const hint = defaultApiToken ? ' (Enter = keep current)' : '';
379
- const line = (await q(`ENFYRA_API_TOKEN${hint}: `)).trim();
497
+ const meUrl = deriveMeUrl(appUrl);
498
+ console.log('');
499
+ console.log(`${style.cyan('◆')} ${style.bold('API token')}`);
500
+ console.log(`${style.dim('│')} If you do not have a token yet, open: ${style.cyan(meUrl)}`);
501
+ console.log(`${style.dim('│')} In the Enfyra admin UI, open ${style.bold('/me')} and create/copy a programmatic API token.`);
502
+ const line = (await q(`${style.dim('└')} ENFYRA_API_TOKEN${hint}: `)).trim();
380
503
  apiToken = line !== '' ? line : defaultApiToken;
381
504
  }
382
505
 
@@ -385,17 +508,25 @@ async function promptConfig(opts, existing) {
385
508
  }
386
509
 
387
510
  function resolveNonInteractive(opts, existing) {
388
- const apiUrl = (
389
- opts.apiUrl ??
390
- process.env.ENFYRA_API_URL ??
391
- (existing.apiUrl || undefined) ??
392
- 'http://localhost:3000/api'
393
- ).replace(/\/$/, '');
511
+ const appUrl = resolveDefaultAppUrl(opts, existing);
512
+ const apiUrl = deriveApiUrlFromAppUrl(appUrl);
394
513
  const apiToken = opts.apiToken ?? process.env.ENFYRA_API_TOKEN ?? existing.apiToken ?? '';
395
514
  return { apiUrl, apiToken };
396
515
  }
397
516
 
398
517
  export async function runLocalConfig(argv) {
518
+ const onSigint = () => exitCancelled();
519
+ const onUnhandledRejection = (error) => {
520
+ if (isCancelError(error)) exitCancelled();
521
+ };
522
+ const onUncaughtException = (error) => {
523
+ if (isCancelError(error)) exitCancelled();
524
+ throw error;
525
+ };
526
+ process.once('SIGINT', onSigint);
527
+ process.once('unhandledRejection', onUnhandledRejection);
528
+ process.once('uncaughtException', onUncaughtException);
529
+
399
530
  let opts;
400
531
  try {
401
532
  opts = parseArgs(argv);
@@ -427,43 +558,67 @@ export async function runLocalConfig(argv) {
427
558
 
428
559
  let apiUrl;
429
560
  let apiToken;
430
- if (usePrompt) {
431
- const resolved = await promptConfig(opts, existing);
432
- apiUrl = resolved.apiUrl;
433
- apiToken = resolved.apiToken;
434
- } else {
435
- const resolved = resolveNonInteractive(opts, existing);
436
- apiUrl = resolved.apiUrl;
437
- apiToken = resolved.apiToken;
561
+ try {
562
+ if (usePrompt) {
563
+ const resolved = await promptConfig(opts, existing);
564
+ apiUrl = resolved.apiUrl;
565
+ apiToken = resolved.apiToken;
566
+ } else {
567
+ const resolved = resolveNonInteractive(opts, existing);
568
+ apiUrl = resolved.apiUrl;
569
+ apiToken = resolved.apiToken;
570
+ }
571
+ } catch (error) {
572
+ if (isCancelError(error)) {
573
+ exitCancelled();
574
+ return;
575
+ }
576
+ throw error;
438
577
  }
439
578
 
440
579
  const serverEntry = buildServerEntry(apiUrl, apiToken);
441
580
  const written = [];
442
581
 
582
+ if (writeCodex) {
583
+ const p = getClientPath('codex', root, opts.global);
584
+ await mergeCodexConfig(p, apiUrl, apiToken);
585
+ written.push({ client: 'codex', path: p });
586
+ }
443
587
  if (writeClaude) {
444
- const p = getClaudeConfigPath(root, opts.global);
588
+ const p = getClientPath('claude', root, opts.global);
445
589
  await mergeMcpFile(p, serverEntry);
446
- written.push(p);
590
+ written.push({ client: 'claude', path: p });
447
591
  }
448
592
  if (writeCursor) {
449
- const p = getCursorConfigPath(root, opts.global);
593
+ const p = getClientPath('cursor', root, opts.global);
450
594
  await mergeMcpFile(p, serverEntry);
451
- written.push(p);
595
+ written.push({ client: 'cursor', path: p });
452
596
  }
453
- if (writeCodex) {
454
- const p = getCodexConfigPath(root, opts.global);
455
- await mergeCodexConfig(p, apiUrl, apiToken);
456
- written.push(p);
597
+
598
+ const scopeLabel = opts.global ? 'global/user' : 'project';
599
+ console.log(`${statusIcon('success')} ${style.bold(style.green('Enfyra MCP config updated'))} ${style.dim(`(${scopeLabel})`)}\n`);
600
+ for (const entry of written) {
601
+ const meta = clients[entry.client];
602
+ console.log(` ${style.cyan('•')} ${style.bold(meta.color(meta.label))}`);
603
+ console.log(` ${style.dim(entry.path)}`);
457
604
  }
458
605
 
459
- console.log('Enfyra MCP local config updated:\n');
460
- for (const p of written) console.log(` ${p}`);
461
- console.log('\nNext steps:');
462
- console.log(' Codex: open this folder in a new Codex session so project ./.codex/config.toml is loaded.');
463
- console.log(' • Claude Code: open this folder; approve project MCP if prompted (`claude mcp reset-project-choices` to reset).');
464
- console.log(' • Cursor: open this folder, restart Cursor or reload MCP, then confirm server under Settings → MCP.');
465
- console.log(' Run `config` again anytime to change values (same files are merged/overwritten for `enfyra`).');
606
+ const selectedClients = new Set(written.map(entry => entry.client));
607
+ console.log(`\n${style.bold(style.blue('Next steps'))}`);
608
+ if (selectedClients.has('codex')) {
609
+ console.log(' - Codex: open this folder in a new Codex session so config is loaded.');
610
+ }
611
+ if (selectedClients.has('claude')) {
612
+ console.log(' - Claude Code: open this folder; approve project MCP if prompted.');
613
+ }
614
+ if (selectedClients.has('cursor')) {
615
+ console.log(' - Cursor: restart Cursor or reload MCP, then confirm the server under Settings -> MCP.');
616
+ }
617
+ console.log(' - Re-run this command anytime to update the same Enfyra entries.');
466
618
  if (!apiToken) {
467
- console.log('\nWarning: ENFYRA_API_TOKEN is empty tools will not authenticate until set.');
619
+ console.log(`\n${statusIcon('warn')} ${style.yellow('ENFYRA_API_TOKEN is empty; tools will not authenticate until it is set.')}`);
468
620
  }
621
+ process.off('SIGINT', onSigint);
622
+ process.off('unhandledRejection', onUnhandledRejection);
623
+ process.off('uncaughtException', onUncaughtException);
469
624
  }
@@ -153,6 +153,26 @@ onUnmounted(() => {
153
153
  'Disconnect the singleton socket when the current user/session clears.',
154
154
  ],
155
155
  },
156
+ {
157
+ name: 'OAuth provider setup values',
158
+ code: `// Enfyra OAuth config row, stored in oauth_config_definition.
159
+ {
160
+ "provider": "google",
161
+ "clientId": "<google-client-id>",
162
+ "clientSecret": "<google-client-secret>",
163
+ "redirectUri": "http://localhost:3000/api/auth/google/callback",
164
+ "isEnabled": true
165
+ }
166
+
167
+ // Google Cloud Console -> Authorized redirect URIs:
168
+ // http://localhost:3000/api/auth/google/callback`,
169
+ notes: [
170
+ 'redirectUri is the Enfyra callback URL: {ENFYRA_API_URL}/auth/google/callback.',
171
+ 'The provider console callback URL and oauth_config_definition.redirectUri must match exactly.',
172
+ 'This callback URL is not the app return page; the app return page is sent as the redirect query when starting OAuth.',
173
+ 'Use appCallbackUrl only for manual-token apps that intentionally read token query parameters.',
174
+ ],
175
+ },
156
176
  {
157
177
  name: 'Google OAuth button',
158
178
  code: `const redirect = new URL("/chat", window.location.origin)
@@ -164,6 +184,7 @@ window.location.href = url.toString()`,
164
184
  'redirect must be absolute and must include the app origin.',
165
185
  'cookieBridgePrefix is the app proxy prefix that forwards to Enfyra API routes.',
166
186
  'Enfyra redirects through {redirect.origin}{cookieBridgePrefix}/auth/set-cookies before returning to redirect.',
187
+ 'After returning, call /enfyra/me to load the authenticated user; do not parse tokens from the URL in proxy-cookie mode.',
167
188
  ],
168
189
  },
169
190
  ],
@@ -1040,7 +1061,7 @@ update_method({
1040
1061
  'Use dedicated method tools instead of generic CRUD on method_definition.',
1041
1062
  'The backend stores the method label in method_definition.name; do not send or filter a method_definition.method field.',
1042
1063
  'buttonColor is the badge background and textColor is the badge text color.',
1043
- 'The eApp management UI is /settings/methods.',
1064
+ 'The Enfyra admin UI is /settings/methods.',
1044
1065
  'delete_method is preview-first and should only be used for unused custom methods.',
1045
1066
  ],
1046
1067
  },
@@ -1080,8 +1101,8 @@ create_extension({
1080
1101
  'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
1081
1102
  'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
1082
1103
  'Page extensions should be full-bleed by default and responsive from the first version.',
1083
- 'The extension root is already inside eApp main; do not add root-level page padding.',
1084
- '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.',
1104
+ 'The extension root is already inside Enfyra admin page main; do not add root-level page padding.',
1105
+ 'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
1085
1106
  ],
1086
1107
  },
1087
1108
  {
@@ -1141,7 +1162,7 @@ create_extension({
1141
1162
  'Do not mutate widget props. Use computed for derived display state, and use watch only when mirroring a prop into local editable draft state.',
1142
1163
  'Prefer defineEmits for child-to-parent requests such as refresh. Use callback props only for parent-owned modal/drawer openers or imperative navigation.',
1143
1164
  'Keep PermissionGate and type="button" plus @click.stop.prevent inside action widgets; server permissions still enforce the real boundary.',
1144
- '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.',
1165
+ 'the Enfyra admin UI batch-fetches widget metadata requested in the same tick and caches loaded widgets, so render Widget components directly instead of manually fetching widget code.',
1145
1166
  ],
1146
1167
  },
1147
1168
  {
@@ -1211,13 +1232,13 @@ create_extension({
1211
1232
  isEnabled: true
1212
1233
  })`,
1213
1234
  notes: [
1214
- 'Global extensions are mounted invisibly by eApp during layout init; do not create a menu and do not embed them with Widget.',
1235
+ 'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
1215
1236
  'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
1216
1237
  'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
1217
- 'For account-panel UI, register data-driven row fields so eApp owns icon size, row spacing, badge placement, hover state, and expanded chrome.',
1238
+ '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.',
1218
1239
  'Use contentComponent only for expanded inner content; use raw component only as an escape hatch when the row cannot fit the shell contract.',
1219
1240
  'Destructure registry functions and register stable ids so reloads replace the same shell item predictably.',
1220
- 'Remove socket or DOM listeners in onUnmounted; eApp unmounts old global components when extension cache reloads or the extension is disabled.',
1241
+ 'Remove socket or DOM listeners in onUnmounted; The Enfyra admin UI unmounts old global components when extension cache reloads or the extension is disabled.',
1221
1242
  ],
1222
1243
  },
1223
1244
  {
@@ -1257,7 +1278,7 @@ register({
1257
1278
  notes: [
1258
1279
  'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
1259
1280
  '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.',
1260
- 'Let eApp handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
1281
+ 'Let the Enfyra admin UI handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
1261
1282
  'Keep contentComponent compact; it is rendered inside account-panel chrome and should not create another large card around itself.',
1262
1283
  'Register the component from a `type="global"` extension, not from a page extension, when it must appear everywhere.',
1263
1284
  ],
@@ -1316,7 +1337,7 @@ registerHeaderActions([
1316
1337
  ],
1317
1338
  },
1318
1339
  {
1319
- name: 'Debug menu or extension changes that do not appear in open eApp tabs',
1340
+ name: 'Debug menu or extension changes that do not appear in open Enfyra admin tabs',
1320
1341
  code: `// Server side: menu_definition and extension_definition are runtime UI definitions.
1321
1342
  // They must participate in partial reload, just like metadata/routes.
1322
1343
  // Expected server contract:
@@ -1324,7 +1345,7 @@ registerHeaderActions([
1324
1345
  // - cache orchestrator maps extension_definition -> extension reload
1325
1346
  // - successful writes emit $system:reload to the admin Socket.IO namespace
1326
1347
 
1327
- // eApp side expected listener behavior:
1348
+ // Enfyra admin UI side expected listener behavior:
1328
1349
  // if reload target is metadata/menu:
1329
1350
  // await fetch menus
1330
1351
  // rebuild menu registry with reset: true
@@ -1335,12 +1356,12 @@ registerHeaderActions([
1335
1356
 
1336
1357
  // Verification pattern:
1337
1358
  // 1. Save the menu or extension record.
1338
- // 2. Watch the open eApp tab for the $system:reload event.
1359
+ // 2. Watch the open Enfyra admin UI tab for the $system:reload event.
1339
1360
  // 3. Confirm sidebar/menu registry or extension component cache changed.
1340
1361
  // 4. Only use manual reload endpoints or browser refresh after the natural event path is proven stale.`,
1341
1362
  notes: [
1342
1363
  'Do not treat menu and extension writes as plain CRUD when debugging live admin UI.',
1343
- 'Check both halves: ASV/ESV emits the reload event, and eApp consumes it.',
1364
+ 'Check both halves: Enfyra Server emits the reload event, and Enfyra admin UI consumes it.',
1344
1365
  'Menu reload should also invalidate extension cache because menu records attach page extensions to routes.',
1345
1366
  'Manual reload is a fallback, not the default fix.',
1346
1367
  ],
@@ -1380,7 +1401,7 @@ create_menu({
1380
1401
  'PageHeader.stats is reserved for deliberate overview headers; operational KPIs belong in body cards/tables.',
1381
1402
  'Operational history pages should not show raw event rows as the primary UI; group by entity/run and translate step keys into operator-facing labels.',
1382
1403
  'Operational lists should use pagination plus search/filter controls; do not rely on arbitrary fixed limits such as limit=50.',
1383
- 'UTabs is available in eApp extension runtime for page-level sections.',
1404
+ 'UTabs is available in the Enfyra admin UI extension runtime for page-level sections.',
1384
1405
  'Admin links for editing or inspecting records should point to /data/<table> routes.',
1385
1406
  ],
1386
1407
  },
@@ -1463,7 +1484,7 @@ console.log(ok, requiredTerms.has('terms'), loaded, label, date)
1463
1484
  </script>`,
1464
1485
  notes: [
1465
1486
  'Do not rewrite extension code to ES5 when tooling rejects modern APIs.',
1466
- 'If diagnostics complain about these APIs, fix eApp extension TypeScript lib/runtime contract.',
1487
+ 'If diagnostics complain about these APIs, fix Enfyra admin extension TypeScript lib/runtime contract.',
1467
1488
  ],
1468
1489
  },
1469
1490
  {
@@ -44,7 +44,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
44
44
  '### Capability map (current Enfyra system)',
45
45
  '- **Schema/metadata:** `table_definition`, `relation_definition`, and schema tools manage tables, columns, relations, validation, and migrations. `column_definition` is internal/no-route; columns are created/updated through table schema operations.',
46
46
  '- **Dynamic REST API:** `route_definition`, `route_handler_definition`, `pre_hook_definition`, `post_hook_definition`, `route_permission_definition`, and `method_definition` define paths, methods, handlers, hooks, and permissions.',
47
- '- **Auth/OAuth/session:** `user_definition`, `role_definition`, `api_token_definition`, `oauth_config_definition`, `oauth_account_definition`; `session_definition` and `api_token_definition` are internal/no-route. OAuth is browser redirect only. MCP uses `ENFYRA_API_TOKEN` through `/auth/token/exchange`; configure tokens from eApp `/me`. `user_definition` is the single source of truth for app users.',
47
+ '- **Auth/OAuth/session:** `user_definition`, `role_definition`, `api_token_definition`, `oauth_config_definition`, `oauth_account_definition`; `session_definition` and `api_token_definition` are internal/no-route. OAuth is browser redirect only. MCP uses `ENFYRA_API_TOKEN` through `/auth/token/exchange`; configure tokens from the Enfyra admin UI `/me`. `user_definition` is the single source of truth for app users.',
48
48
  '- **Guards/permissions/validation:** `guard_definition`, `guard_rule_definition`, `field_permission_definition`, and `column_rule_definition` control route guards, field access, and request body validation.',
49
49
  '- **GraphQL:** `gql_definition` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
50
50
  '- **Files/storage/assets:** `file_definition`, `file_permission_definition`, `folder_definition`, `storage_config_definition` plus upload/assets routes and file helpers.',
@@ -302,7 +302,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
302
302
  '- **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.',
303
303
  '- **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.',
304
304
  '- **Workflow**: Create flow → `create_record` on `flow_definition`. Add steps → `create_record` on `flow_step_definition` with `flow: {id}`. For branch steps, set `parent: {id: conditionStepId}` and `branch: "true"` or `"false"`. Trigger manually via `POST /admin/flow/trigger/{flowId}`.',
305
- '- **Flow source sanity:** after creating or patching a multi-step flow, refetch saved `flow_step_definition` rows and verify every script/condition step has executable step-specific `sourceCode` with a body/return. A helper-only source block that parses successfully is still broken and must be patched before the user sees it in eApp.',
305
+ '- **Flow source sanity:** after creating or patching a multi-step flow, refetch saved `flow_step_definition` rows and verify every script/condition step has executable step-specific `sourceCode` with a body/return. A helper-only source block that parses successfully is still broken and must be patched before the user sees it in the Enfyra admin UI.',
306
306
  '- **Test step**: `POST /admin/test/run` with body `{kind:"flow_step", type, config, timeout}` — runs a single step without saving, returns `{success, result, error, duration}`.',
307
307
  '- MCP wrappers: use **`test_flow_step`** for one flow step, **`run_admin_test`** for flow/websocket tests, and **`trigger_flow`** for saved flows.',
308
308
  '- **In handlers/hooks**: Trigger flows via `$ctx.$trigger("flow-name", {payload})` or `$ctx.$trigger(flowId, {payload})`.',
@@ -315,33 +315,33 @@ export function buildMcpServerInstructions(apiBaseUrl) {
315
315
  '- **Design first for dashboards:** before creating/updating a dashboard extension, define the menu/page split, time range controls, tabs, and drill-down links. Keep `/dashboard` as a compact summary and routing hub only: KPIs, current signal, attention queue, and navigation cards. Put detailed lists/tables/timelines into focused pages such as jobs, orders, reports, integrations, and settings.',
316
316
  '- **Operational page modeling:** model admin operational pages around the operator mental entity, not raw database/event rows. For long-running jobs, group history by entity/run, translate internal step keys into readable labels, show current step, operator meaning, and next action, and keep raw history as a secondary `/data` shortcut.',
317
317
  '- **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 id, name, slug, status, email, or external reference.',
318
- '- **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`.',
319
- '- **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.',
318
+ '- **Enfyra 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`.',
319
+ '- **Aggregate numeric rule:** `sum` and `avg` require a numeric field in Enfyra Server. 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.',
320
320
  '- **Snapshot migrations:** backend metadata/physical schema renames belong in `data/snapshot-migration.json` via table-driven `columnsToModify` entries. The server migration/self-heal path should read table name plus `oldName`/`newName` dynamically; do not hard-code one-off table repairs when the snapshot migration contract can express the change.',
321
- '- **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.',
322
- '- **Menu/extension realtime reload contract:** `menu_definition` and `extension_definition` writes are runtime UI changes, not plain CRUD. The server cache orchestrator must emit `$system:reload` through the admin Socket.IO channel with identifiers that eApp handles; eApp must refetch menus/rebuild the menu registry for menu reloads, invalidate dynamic extension caches for extension reloads, and reload enabled `type="global"` shell extensions when extension/menu metadata changes. Menu reloads can change route-to-extension mapping, so they should also invalidate extension cache. If an open admin tab does not reflect menu/extension changes, debug this two-sided reload contract before telling the user to refresh.',
321
+ '- **Partial reload default:** Enfyra Server 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.',
322
+ '- **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 the Enfyra admin UI handles; Enfyra admin UI must refetch menus/rebuild the menu registry for menu reloads, invalidate dynamic extension caches for extension reloads, and reload enabled `type="global"` shell extensions when extension/menu metadata changes. Menu reloads can change route-to-extension mapping, so they should also invalidate extension cache. If an open admin tab does not reflect menu/extension changes, debug this two-sided reload contract before telling the user to refresh.',
323
323
  '- **Dashboard stats:** time range buttons must change the query filter and reload stats. Dashboards should summarize actionable errors and high-level activity; successful/no-error background runs usually do not need a standalone page unless there is a real workflow to manage.',
324
- '- **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.',
325
- '- **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.',
324
+ '- **Page layout default:** page extensions should render full-bleed inside the app shell by default. The extension root is already inside the Enfyra admin UI 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.',
325
+ '- **PageHeader is mandatory for page extensions:** The Enfyra admin shell 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.',
326
326
  '- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, capacity totals, billing totals, or detail metrics there by default; keep those as body cards/tables where the operator can scan them with the page content. Only use PageHeader stats for a deliberately compact overview page where the stats are truly header-level context.',
327
327
  '- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Destructure `register` first, then call it with one action or an array, for example `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id: "create", label: "Create report", permission: { and: [{ route: "/report_definition", methods: ["POST"] }] }, onClick }])`. Sensitive registry actions must include a `permission` condition.',
328
328
  '- **Header action button variants:** choose the button variant by intent. Use `color: "primary", variant: "solid"` for the main page action. Use `color: "neutral", variant: "ghost"` for back/navigation actions and `color: "neutral", variant: "outline"` for visible secondary actions. `variant: "soft"` is only for low-emphasis secondary/chrome actions; do not use soft for critical or primary header actions just because it looks acceptable in dark mode.',
329
- '- **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.',
329
+ '- **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 Enfyra admin UI 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.',
330
330
  '- **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.',
331
- '- **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.*`.',
332
- '- **Global extension lifecycle:** `extension_definition.type="global"` records are Vue SFC components mounted invisibly once at the eApp shell level. Use them for app-wide registrations such as account panel items, notification bells, admin socket listeners, and background refresh bridges. They do not need a menu, must not render page body UI, and should clean up socket/listener side effects with `onUnmounted`. Prefer registry composables such as `useAccountPanelRegistry`, `useHeaderActionRegistry`, or `useSubHeaderActionRegistry` instead of directly editing shell DOM. For account-panel entries, register data (`label`, `icon`, `description`, `badge`, `badgeColor`, `expanded`, `onToggle`, `contentComponent`) so eApp owns row spacing, icon sizing, badges, and expanded chrome; use a raw row `component` only for a true custom escape hatch.',
333
- '- **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.',
331
+ '- **Extension runtime scope:** Enfyra admin UI 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.*`.',
332
+ '- **Global extension lifecycle:** `extension_definition.type="global"` records are Vue SFC components mounted invisibly once at the Enfyra admin UI shell level. Use them for app-wide registrations such as account panel items, notification bells, admin socket listeners, and background refresh bridges. They do not need a menu, must not render page body UI, and should clean up socket/listener side effects with `onUnmounted`. Prefer registry composables such as `useAccountPanelRegistry`, `useHeaderActionRegistry`, or `useSubHeaderActionRegistry` instead of directly editing shell DOM. For account-panel entries, register data (`label`, `icon`, `description`, `badge`, `badgeColor`, `expanded`, `onToggle`, `contentComponent`) so Enfyra admin UI owns row spacing, icon sizing, badges, and expanded chrome; use a raw row `component` only for a true custom escape hatch.',
333
+ '- **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 Enfyra admin UI build.',
334
334
  '- **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.',
335
- '- **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.',
335
+ '- **Admin record links:** when an admin extension links to backend records for management or inspection, point to Enfyra admin UI 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.',
336
336
  '- **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"] }] }`.',
337
337
  '- **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.',
338
338
  '- **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.',
339
339
  '- **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.',
340
- '- **Admin operation UI:** use eApp `CommonModal` for compact create, disable, delete, and multi-field confirmation workflows. Use `CommonDrawer` for longer setup workflows such as multi-step create/edit forms 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.',
340
+ '- **Admin operation UI:** use Enfyra admin `CommonModal` for compact create, disable, delete, and multi-field confirmation workflows. Use `CommonDrawer` for longer setup workflows such as multi-step create/edit forms 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.',
341
341
  '- **FormEditor is preferred for table-record forms:** when an extension creates or edits a concrete table record such as `report_definition`, `order_definition`, or another table-backed entity, 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.',
342
342
  '- **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.',
343
343
  '- **Confirmation modal flow:** destructive/admin confirmation modals must read top-to-bottom as the operator workflow. For server-hash confirmations, render: 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.',
344
- '- **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.',
344
+ '- **Do not downgrade extension code to ES5 to appease tooling.** Enfyra admin 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 Enfyra admin 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.',
345
345
  '',
346
346
  '#### Injected Vue API functions:',
347
347
  '- Reactivity: `ref`, `reactive`, `computed`, `readonly`, `shallowRef`, `shallowReactive`',
@@ -367,7 +367,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
367
367
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
368
368
  '- **useHeaderActionRegistry:** Register header actions. Use `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id, label, onClick, color, icon, order, side, global, permission }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component, permission }`; admin actions should set `permission` by default.',
369
369
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header: destructure `register` first, then call it with one action or an array.',
370
- '- **useAccountPanelRegistry:** Register rows in the sidebar account panel. Destructure `const { register } = useAccountPanelRegistry()` and call `register(itemOrItems)`. Prefer data-driven items: `{ id, order, label, icon, description, badge, badgeColor, trailingIcon, expanded, onToggle, contentComponent, contentProps }`. eApp renders the row chrome and lifecycle-unregisters the caller automatically. Use `contentComponent` only for expanded inner content, and use raw `component` only when the entire row truly cannot use the shell contract.',
370
+ '- **useAccountPanelRegistry:** Register rows in the sidebar account panel. Destructure `const { register } = useAccountPanelRegistry()` and call `register(itemOrItems)`. Prefer data-driven items: `{ id, order, label, icon, description, badge, badgeColor, trailingIcon, expanded, onToggle, contentComponent, contentProps }`. Enfyra admin UI renders the row chrome and lifecycle-unregisters the caller automatically. Use `contentComponent` only for expanded inner content, and use raw `component` only when the entire row truly cannot use the shell contract.',
371
371
  '- **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).',
372
372
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
373
373
  '- **useMenuApi:** Low-level menu API.',
@@ -385,7 +385,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
385
385
  '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
386
386
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
387
387
  '- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
388
- '- **Tabs:** `UTabs` is available in current eApp extension runtime. Use it for page-level sections when a page would otherwise become too long.',
388
+ '- **Tabs:** `UTabs` is available in current Enfyra admin extension runtime. Use it for page-level sections when a page would otherwise become too long.',
389
389
  '- **Extension:** `Widget` — embed widget extension records by numeric database id, e.g. `<Widget :id="123" />`',
390
390
  '- **WebSocket:** `WebSocketManager`',
391
391
  '- **Permission:** `PermissionGate`, `PermissionManager`',
@@ -398,13 +398,13 @@ export function buildMcpServerInstructions(apiBaseUrl) {
398
398
  '- **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`.',
399
399
  '- **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.',
400
400
  '- **type "widget":** Widget extension. No menu required. Embed by numeric database id via `<Widget :id="123" />` in other extensions or pages.',
401
- '- **type "global":** Global shell extension. No menu required and not embedded manually; eApp fetches all enabled global extensions during layout init and mounts them invisibly with normal Vue lifecycle. Use for global notification/account-panel/realtime registration code, not for route content. Keep `<template></template>` empty or minimal hidden markup, and keep visible UI in registered components or existing shell slots.',
401
+ '- **type "global":** Global shell extension. No menu required and not embedded manually; the Enfyra admin UI fetches all enabled global extensions during layout init and mounts them invisibly with normal Vue lifecycle. Use for global notification/account-panel/realtime registration code, not for route content. Keep `<template></template>` empty or minimal hidden markup, and keep visible UI in registered components or existing shell slots.',
402
402
  '- **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.',
403
403
  '- **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.',
404
404
  '- **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.',
405
405
  '- **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.',
406
406
  '- **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.',
407
- '- **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.',
407
+ '- **Widget loading performance:** Enfyra admin UI 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.',
408
408
  '- **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.',
409
409
  '',
410
410
  '#### NPM packages (install via MCP):',
@@ -421,7 +421,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
421
421
  '- **Header actions:** `const { register: registerHeaderActions } = useHeaderActionRegistry(); registerHeaderActions([{ id: \'back\', label: \'Hosts\', icon: \'lucide:arrow-left\', color: \'neutral\', variant: \'ghost\', order: 0, onClick: goBack }, { id: \'refresh\', label: \'Refresh\', icon: \'lucide:refresh-cw\', color: \'primary\', variant: \'solid\', order: 1, onClick: refresh }])`',
422
422
  '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
423
423
  '- **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.',
424
- '- **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.',
424
+ '- **After menu/extension create/update:** open Enfyra admin 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/Enfyra admin UI version does not support menu/extension reload yet.',
425
425
  '',
426
426
  '#### Minimal example:',
427
427
  '`<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>`',
@@ -51,7 +51,7 @@ const CAPABILITY_AREAS = [
51
51
  {
52
52
  area: 'Auth, roles, sessions, OAuth',
53
53
  tables: ['user_definition', 'role_definition', 'api_token_definition', 'session_definition', 'oauth_config_definition', 'oauth_account_definition'],
54
- workflow: 'MCP auth exchanges ENFYRA_API_TOKEN through /auth/token/exchange. Configure an API token from eApp /me.',
54
+ workflow: 'MCP auth exchanges ENFYRA_API_TOKEN through /auth/token/exchange. Configure an API token from Enfyra admin UI /me.',
55
55
  },
56
56
  {
57
57
  area: 'Guards and permissions',
@@ -2467,7 +2467,7 @@ server.tool(
2467
2467
  const payload = {
2468
2468
  guidance: {
2469
2469
  publicAccess: 'publicMethods bypass RoleGuard and do not require route_permission_definition.',
2470
- authenticatedAccess: 'For non-public methods, eApp PermissionGate and backend RoleGuard both expect enabled route_permission_definition rows with matching route + HTTP method.',
2470
+ authenticatedAccess: 'For non-public methods, Enfyra admin UI PermissionGate and backend RoleGuard both expect enabled route_permission_definition rows with matching route + HTTP method.',
2471
2471
  directUserAccess: 'allowedRoutePermissions on /me represent direct user-scoped route permissions; role.routePermissions represent role-scoped permissions.',
2472
2472
  },
2473
2473
  expectedScope: {
@@ -2910,7 +2910,7 @@ server.tool(
2910
2910
  'create_extension',
2911
2911
  [
2912
2912
  'Create an extension (Vue SFC page, widget, or global shell extension). Code must be Vue SFC: <template>...</template> + <script setup>...</script> — NO imports, use globals (ref, useToast, useApi, UButton, etc).',
2913
- 'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget: no menu, embed via <Widget>. For type=global: no menu, eApp mounts it invisibly at shell level for registries/realtime. Server auto-compiles and should emit realtime reload to open eApp tabs. See extension rules in MCP instructions.',
2913
+ 'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget: no menu, embed via <Widget>. For type=global: no menu, the Enfyra admin UI mounts it invisibly at shell level for registries/realtime. Server auto-compiles and should emit realtime reload to open Enfyra admin tabs. See extension rules in MCP instructions.',
2914
2914
  ].join(' '),
2915
2915
  {
2916
2916
  name: z.string().describe('Extension name (unique)'),
@@ -2935,7 +2935,7 @@ server.tool(
2935
2935
  }
2936
2936
  const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(body) });
2937
2937
  const created = firstDataRecord(result);
2938
- return { content: [{ type: 'text', text: `Extension created (ID: ${getId(created)}). Open eApp tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
2938
+ return { content: [{ type: 'text', text: `Extension created (ID: ${getId(created)}). Open Enfyra admin tabs should update through the realtime reload contract.\n${JSON.stringify(result, null, 2)}` }] };
2939
2939
  },
2940
2940
  );
2941
2941