@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 +150 -129
- package/package.json +2 -2
- package/src/lib/config-local.mjs +241 -86
- package/src/lib/mcp-examples.js +35 -14
- package/src/lib/mcp-instructions.js +20 -20
- package/src/mcp-server-entry.mjs +4 -4
package/README.md
CHANGED
|
@@ -1,66 +1,55 @@
|
|
|
1
1
|
# Enfyra MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
7
|
+
## Quick Start
|
|
7
8
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
Equivalent in this repo:
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
36
|
+
## Choose A Client
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
Official reference: [Codex config](https://developers.openai.com/codex/config-reference).
|
|
83
69
|
|
|
84
|
-
|
|
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
|
-
|
|
72
|
+
<details>
|
|
73
|
+
<summary><strong>Claude Code setup</strong></summary>
|
|
93
74
|
|
|
94
|
-
|
|
75
|
+
```bash
|
|
76
|
+
npx @enfyra/mcp-server config --claude-code
|
|
77
|
+
```
|
|
95
78
|
|
|
96
|
-
|
|
79
|
+
Project config is written to `.mcp.json`. MCP server definitions do not belong in `.claude/settings.json`.
|
|
97
80
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
| `
|
|
183
|
-
| `
|
|
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
|
-
|
|
167
|
+
## Common Examples
|
|
186
168
|
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
## OAuth Setup
|
|
192
181
|
|
|
193
|
-
|
|
182
|
+
OAuth has three different URLs:
|
|
194
183
|
|
|
195
|
-
|
|
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
|
-
|
|
190
|
+
Example Google callback when `ENFYRA_API_URL=http://localhost:3000/api`:
|
|
198
191
|
|
|
199
|
-
|
|
192
|
+
```text
|
|
193
|
+
http://localhost:3000/api/auth/google/callback
|
|
194
|
+
```
|
|
200
195
|
|
|
201
|
-
|
|
196
|
+
Start OAuth from the app proxy:
|
|
202
197
|
|
|
203
198
|
```text
|
|
204
|
-
|
|
199
|
+
/enfyra/auth/google?redirect=<absoluteReturnUrl>&cookieBridgePrefix=/enfyra
|
|
205
200
|
```
|
|
206
201
|
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
## Enfyra URL Pattern
|
|
212
220
|
|
|
213
|
-
|
|
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
|
-
|
|
245
|
+
Do not create custom login/logout/me routes that manually set Enfyra token cookies when the proxy is enough.
|
|
225
246
|
|
|
226
|
-
|
|
247
|
+
## Tool Summary
|
|
227
248
|
|
|
228
|
-
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "MCP server for Enfyra - manage
|
|
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",
|
package/src/lib/config-local.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
--
|
|
23
|
-
--api-token, -t <secret>
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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 -
|
|
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
|
-
|
|
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 === '--
|
|
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
|
|
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
|
-
|
|
250
|
-
value: { claude:
|
|
341
|
+
client: 'codex',
|
|
342
|
+
value: { claude: false, cursor: false, codex: true },
|
|
251
343
|
},
|
|
252
344
|
{
|
|
253
|
-
|
|
254
|
-
value: { claude:
|
|
345
|
+
client: 'claude',
|
|
346
|
+
value: { claude: true, cursor: false, codex: false },
|
|
255
347
|
},
|
|
256
348
|
{
|
|
257
|
-
|
|
258
|
-
value: { claude: false, cursor:
|
|
349
|
+
client: 'cursor',
|
|
350
|
+
value: { claude: false, cursor: true, codex: false },
|
|
259
351
|
},
|
|
260
352
|
{
|
|
261
|
-
|
|
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]
|
|
273
|
-
+ ' [2]
|
|
274
|
-
+ ' [3]
|
|
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 === '
|
|
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 === '
|
|
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
|
-
'
|
|
304
|
-
|
|
305
|
-
'',
|
|
306
|
-
|
|
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
|
|
467
|
+
let appUrl = opts.appUrl ? normalizeAppUrl(opts.appUrl) : '';
|
|
356
468
|
let apiToken = opts.apiToken;
|
|
357
|
-
if (
|
|
358
|
-
return { apiUrl:
|
|
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) =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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 =
|
|
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
|
|
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
|
|
389
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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('
|
|
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
|
}
|
package/src/lib/mcp-examples.js
CHANGED
|
@@ -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
|
|
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
|
|
1084
|
-
'After saving, open
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
'- **
|
|
319
|
-
'- **Aggregate numeric rule:** `sum` and `avg` require a numeric field in
|
|
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:**
|
|
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
|
|
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
|
|
325
|
-
'- **PageHeader is mandatory for page extensions:**
|
|
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
|
|
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:**
|
|
332
|
-
'- **Global extension lifecycle:** `extension_definition.type="global"` records are Vue SFC components mounted invisibly once at the
|
|
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
|
|
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
|
|
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
|
|
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.**
|
|
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 }`.
|
|
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
|
|
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;
|
|
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:**
|
|
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
|
|
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>`',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|