@enfyra/mcp-server 0.0.123 → 0.1.1
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 +76 -6
- package/package.json +1 -1
- package/src/index.mjs +5 -3
- package/src/lib/config-local.mjs +158 -63
- package/src/lib/mcp-examples.js +22 -9
- package/src/lib/mcp-instructions.js +9 -4
- package/src/lib/platform-operation-tools.js +36 -4
- package/src/lib/required-knowledge.js +116 -0
- package/src/mcp-server-entry.mjs +55 -11
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Enfyra MCP Server
|
|
2
2
|
|
|
3
|
-
Manage Enfyra instances from MCP-compatible coding tools such as **Codex**, **Claude Code**, **Cursor**, MCP Inspector, and other STDIO MCP hosts.
|
|
3
|
+
Manage Enfyra instances from MCP-compatible coding tools such as **Codex**, **Claude Code**, **Cursor**, **VS Code / GitHub Copilot**, **Google Antigravity**, MCP Inspector, and other STDIO MCP hosts.
|
|
4
4
|
|
|
5
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`.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ From your project root:
|
|
|
12
12
|
npx @enfyra/mcp-server config
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The config command writes project config for Codex, Claude Code, and
|
|
15
|
+
The config command writes project config for Codex, Claude Code, Cursor, VS Code / GitHub Copilot, and Google Antigravity. It preserves other MCP servers and replaces only the `enfyra` entry.
|
|
16
16
|
|
|
17
17
|
Interactive setup asks for your Enfyra app/admin URL, then guides you to the token page when needed and asks for `ENFYRA_API_TOKEN`.
|
|
18
18
|
|
|
@@ -25,6 +25,8 @@ npx @enfyra/mcp-server config --yes \
|
|
|
25
25
|
# One or more clients
|
|
26
26
|
npx @enfyra/mcp-server config --codex
|
|
27
27
|
npx @enfyra/mcp-server config --cursor --claude-code
|
|
28
|
+
npx @enfyra/mcp-server config --vscode
|
|
29
|
+
npx @enfyra/mcp-server config --antigravity
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Equivalent in this repo:
|
|
@@ -40,7 +42,9 @@ yarn mcp:config
|
|
|
40
42
|
| Codex | `npx @enfyra/mcp-server config --codex` | `.codex/config.toml` |
|
|
41
43
|
| Claude Code | `npx @enfyra/mcp-server config --claude-code` | `.mcp.json` |
|
|
42
44
|
| Cursor | `npx @enfyra/mcp-server config --cursor` | `.cursor/mcp.json` |
|
|
43
|
-
|
|
|
45
|
+
| VS Code / GitHub Copilot | `npx @enfyra/mcp-server config --vscode` | `.vscode/mcp.json` |
|
|
46
|
+
| Google Antigravity | `npx @enfyra/mcp-server config --antigravity` | `.agents/mcp_config.json` |
|
|
47
|
+
| MCP Inspector / other project-scoped hosts | Paste the shared STDIO config below | Host-specific project config |
|
|
44
48
|
|
|
45
49
|
<details>
|
|
46
50
|
<summary><strong>Codex setup</strong></summary>
|
|
@@ -108,10 +112,71 @@ Official reference: [Cursor MCP](https://cursor.com/docs/context/mcp).
|
|
|
108
112
|
|
|
109
113
|
</details>
|
|
110
114
|
|
|
115
|
+
<details>
|
|
116
|
+
<summary><strong>VS Code / GitHub Copilot setup</strong></summary>
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx @enfyra/mcp-server config --vscode
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
VS Code workspace config is written to `.vscode/mcp.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"servers": {
|
|
127
|
+
"enfyra": {
|
|
128
|
+
"type": "stdio",
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "@enfyra/mcp-server"],
|
|
131
|
+
"env": {
|
|
132
|
+
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
133
|
+
"ENFYRA_API_TOKEN": "efy_pat_your-token"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Use the VS Code command `MCP: List Servers` to inspect or start the server after setup. This is a workspace config, so it stays tied to the current project.
|
|
141
|
+
|
|
142
|
+
Official references: [VS Code MCP servers](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) and [VS Code MCP configuration](https://code.visualstudio.com/docs/agents/reference/mcp-configuration).
|
|
143
|
+
|
|
144
|
+
</details>
|
|
145
|
+
|
|
146
|
+
<details>
|
|
147
|
+
<summary><strong>Google Antigravity setup</strong></summary>
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx @enfyra/mcp-server config --antigravity
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Antigravity project config is written to `.agents/mcp_config.json`:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"mcpServers": {
|
|
158
|
+
"enfyra": {
|
|
159
|
+
"command": "npx",
|
|
160
|
+
"args": ["-y", "@enfyra/mcp-server"],
|
|
161
|
+
"env": {
|
|
162
|
+
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
163
|
+
"ENFYRA_API_TOKEN": "efy_pat_your-token"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Antigravity also documents a shared user config at `~/.gemini/config/mcp_config.json`; this helper intentionally writes the project-local `.agents/mcp_config.json` file so Enfyra URL and token stay scoped to the current workspace.
|
|
171
|
+
|
|
172
|
+
Official reference: [Antigravity MCP](https://antigravity.google/docs/mcp).
|
|
173
|
+
|
|
174
|
+
</details>
|
|
175
|
+
|
|
111
176
|
<details>
|
|
112
177
|
<summary><strong>Other MCP hosts and MCP Inspector</strong></summary>
|
|
113
178
|
|
|
114
|
-
Use the shared STDIO config with any host that accepts an `mcpServers` JSON block:
|
|
179
|
+
Use the shared STDIO config with any project-scoped host that accepts an `mcpServers` JSON block:
|
|
115
180
|
|
|
116
181
|
```json
|
|
117
182
|
{
|
|
@@ -145,11 +210,12 @@ npx @enfyra/mcp-server config [options]
|
|
|
145
210
|
| `--app-url` | Set the Enfyra app/admin URL |
|
|
146
211
|
| `--api-token`, `-t` | Set `ENFYRA_API_TOKEN` |
|
|
147
212
|
| `--yes` | Non-interactive mode for CI/scripts |
|
|
148
|
-
| `--global` | Write global/user config instead of project config |
|
|
149
213
|
| `--reconfig` | Prompt for target clients again and replace the existing `enfyra` entry |
|
|
150
214
|
| `--codex` | Write Codex config |
|
|
151
215
|
| `--claude-code`, `--claude` | Write Claude Code config |
|
|
152
216
|
| `--cursor` | Write Cursor config |
|
|
217
|
+
| `--vscode`, `--copilot` | Write VS Code / GitHub Copilot config |
|
|
218
|
+
| `--antigravity` | Write Google Antigravity config |
|
|
153
219
|
| `-h`, `--help` | Show CLI help |
|
|
154
220
|
|
|
155
221
|
Without a target flag, interactive mode asks which client to configure. Non-interactive mode defaults to all supported clients.
|
|
@@ -178,14 +244,18 @@ Use `get_enfyra_examples` from the MCP tool list when asking an LLM to generate
|
|
|
178
244
|
- files and storage
|
|
179
245
|
- Enfyra admin extensions
|
|
180
246
|
|
|
247
|
+
Use `get_enfyra_required_knowledge` before asking an LLM to save dynamic server code or Enfyra extension code. It returns short required contracts plus acknowledgement keys that code-writing tools verify before saving.
|
|
248
|
+
|
|
181
249
|
## Runtime Safety
|
|
182
250
|
|
|
183
251
|
The MCP server includes safety guards for LLM callers:
|
|
184
252
|
|
|
185
253
|
- Generic record mutations validate fields against live metadata.
|
|
254
|
+
- Code-writing tools require `get_enfyra_required_knowledge` acknowledgement before saving dynamic scripts or extension code. Discovery, validation, and preview tools remain available without the acknowledgement so agents can read and plan first.
|
|
186
255
|
- Script-backed records validate `sourceCode` through `/admin/script/validate` before saving.
|
|
187
256
|
- `validate_dynamic_script` checks handler, hook, flow, websocket, GraphQL, and bootstrap script source without saving.
|
|
188
257
|
- `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
258
|
+
- Dynamic script guidance distinguishes secure repositories (`@REPOS.main`, `@REPOS.secure.<table>`) from trusted internal repositories (`@REPOS.<table>`), and tells agents not to return raw trusted records to users.
|
|
189
259
|
- `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
|
|
190
260
|
- JSON responses include `compressionStats` with estimated token savings. Arrays of objects are converted to columnar form only when the compact shape is smaller than raw JSON.
|
|
191
261
|
- Relation tools reject physical FK/junction names and resolve table ids from exact table names or aliases before schema mutation.
|
|
@@ -229,7 +299,7 @@ Do not create custom login/logout/me routes that manually set Enfyra token cooki
|
|
|
229
299
|
|
|
230
300
|
## Tool Summary
|
|
231
301
|
|
|
232
|
-
The MCP server exposes tools for metadata discovery, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, and `
|
|
302
|
+
The MCP server exposes tools for metadata discovery, required knowledge, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, `get_enfyra_api_context`, and `get_enfyra_required_knowledge`.
|
|
233
303
|
|
|
234
304
|
Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods` and `private_route_methods` for that access boundary.
|
|
235
305
|
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -11,15 +11,17 @@ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
13
|
npx @enfyra/mcp-server Start the MCP stdio server
|
|
14
|
-
npx @enfyra/mcp-server config [flags] Write local MCP host config
|
|
14
|
+
npx @enfyra/mcp-server config [flags] Write project-local MCP host config
|
|
15
15
|
|
|
16
16
|
Common config flags:
|
|
17
|
-
--codex Write
|
|
17
|
+
--codex Write ./.codex/config.toml
|
|
18
18
|
--claude-code Write ./.mcp.json
|
|
19
19
|
--cursor Write ./.cursor/mcp.json
|
|
20
|
+
--vscode Write ./.vscode/mcp.json
|
|
21
|
+
--antigravity Write ./.agents/mcp_config.json
|
|
20
22
|
--reconfig Prompt for host and credentials again, replacing the enfyra entry
|
|
21
23
|
--yes Non-interactive
|
|
22
|
-
|
|
24
|
+
--app-url ENFYRA_APP_URL
|
|
23
25
|
-t, --api-token ENFYRA_API_TOKEN
|
|
24
26
|
-h, --help Show config help
|
|
25
27
|
|
package/src/lib/config-local.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { createInterface } from 'node:readline/promises';
|
|
|
3
3
|
import { emitKeypressEvents } from 'node:readline';
|
|
4
4
|
import { stdin as input, stdout as output, cwd } from 'node:process';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
7
6
|
|
|
8
7
|
const SERVER_KEY = 'enfyra';
|
|
9
8
|
const forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== '0';
|
|
@@ -36,6 +35,16 @@ const clients = {
|
|
|
36
35
|
path: './.cursor/mcp.json',
|
|
37
36
|
color: style.cyan,
|
|
38
37
|
},
|
|
38
|
+
vscode: {
|
|
39
|
+
label: 'VS Code / Copilot',
|
|
40
|
+
path: './.vscode/mcp.json',
|
|
41
|
+
color: style.blue,
|
|
42
|
+
},
|
|
43
|
+
antigravity: {
|
|
44
|
+
label: 'Antigravity',
|
|
45
|
+
path: './.agents/mcp_config.json',
|
|
46
|
+
color: style.yellow,
|
|
47
|
+
},
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
function statusIcon(kind) {
|
|
@@ -55,7 +64,7 @@ function exitCancelled() {
|
|
|
55
64
|
|
|
56
65
|
function printHelp() {
|
|
57
66
|
console.log(`${style.bold('Enfyra MCP config')}
|
|
58
|
-
${style.dim('Write local MCP client config for Enfyra.')}
|
|
67
|
+
${style.dim('Write project-local MCP client config for Enfyra.')}
|
|
59
68
|
|
|
60
69
|
${style.bold('Usage')}
|
|
61
70
|
npx @enfyra/mcp-server config [options]
|
|
@@ -64,12 +73,12 @@ ${style.bold('Supported clients')}
|
|
|
64
73
|
Codex ./.codex/config.toml
|
|
65
74
|
Claude Code ./.mcp.json
|
|
66
75
|
Cursor ./.cursor/mcp.json
|
|
67
|
-
|
|
76
|
+
VS Code ./.vscode/mcp.json
|
|
77
|
+
Antigravity ./.agents/mcp_config.json
|
|
68
78
|
|
|
69
79
|
${style.bold('Options')}
|
|
70
80
|
--app-url <url> Enfyra app/admin URL, for example https://demo.enfyra.io
|
|
71
81
|
--api-token, -t <secret> ENFYRA_API_TOKEN
|
|
72
|
-
--global Write global/user config for selected clients instead of project config
|
|
73
82
|
--reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
|
|
74
83
|
--yes Non-interactive: no prompts (CI / scripts); use CLI, env, existing file, then defaults
|
|
75
84
|
|
|
@@ -79,20 +88,23 @@ ${style.bold('Client selection')}
|
|
|
79
88
|
--claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
|
|
80
89
|
--cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
|
|
81
90
|
--codex, --codex-only Only ./.codex/config.toml (Codex project scope)
|
|
91
|
+
--vscode, --copilot, --vscode-only Only ./.vscode/mcp.json (VS Code / Copilot)
|
|
92
|
+
--antigravity, --antigravity-only Only ./.agents/mcp_config.json (Antigravity)
|
|
82
93
|
Passing multiple target flags writes each selected target.
|
|
83
94
|
|
|
84
95
|
-h, --help Show this help
|
|
85
96
|
|
|
86
97
|
${style.bold('Interactive mode')}
|
|
87
|
-
Choose Codex, Claude Code, Cursor, or all clients; then enter ENFYRA_APP_URL and ENFYRA_API_TOKEN.
|
|
98
|
+
Choose Codex, Claude Code, Cursor, VS Code, Antigravity, or all clients; then enter ENFYRA_APP_URL and ENFYRA_API_TOKEN.
|
|
88
99
|
Existing Enfyra config and environment variables are used as defaults. Re-run anytime to update.
|
|
89
100
|
|
|
90
101
|
${style.bold('Examples')}
|
|
91
102
|
npx @enfyra/mcp-server config
|
|
92
103
|
npx @enfyra/mcp-server config --yes
|
|
93
104
|
npx @enfyra/mcp-server config --codex --cursor
|
|
105
|
+
npx @enfyra/mcp-server config --vscode
|
|
106
|
+
npx @enfyra/mcp-server config --antigravity
|
|
94
107
|
npx @enfyra/mcp-server config --claude-code
|
|
95
|
-
npx @enfyra/mcp-server config --global --codex
|
|
96
108
|
npx @enfyra/mcp-server config --reconfig
|
|
97
109
|
npx @enfyra/mcp-server config --app-url http://localhost:3000 -t 'efy_pat_...'
|
|
98
110
|
ENFYRA_APP_URL=https://demo.enfyra.io ENFYRA_API_TOKEN=efy_pat_... npx @enfyra/mcp-server config --yes
|
|
@@ -109,11 +121,14 @@ function parseArgs(argv) {
|
|
|
109
121
|
help: false,
|
|
110
122
|
yes: false,
|
|
111
123
|
reconfig: false,
|
|
112
|
-
|
|
124
|
+
vscode: true,
|
|
125
|
+
antigravity: true,
|
|
113
126
|
};
|
|
114
127
|
let pickClaude = false;
|
|
115
128
|
let pickCursor = false;
|
|
116
129
|
let pickCodex = false;
|
|
130
|
+
let pickVscode = false;
|
|
131
|
+
let pickAntigravity = false;
|
|
117
132
|
for (let i = 0; i < argv.length; i += 1) {
|
|
118
133
|
const a = argv[i];
|
|
119
134
|
const next = () => {
|
|
@@ -126,7 +141,6 @@ function parseArgs(argv) {
|
|
|
126
141
|
else if (a === 'help') out.help = true;
|
|
127
142
|
else if (a === '--yes') out.yes = true;
|
|
128
143
|
else if (a === '--reconfig') out.reconfig = true;
|
|
129
|
-
else if (a === '--global') out.global = true;
|
|
130
144
|
else if (a === '--app-url') out.appUrl = next();
|
|
131
145
|
else if (a === '--api-url' || a === '-a') {
|
|
132
146
|
throw new Error(`${a} is no longer supported for setup; use --app-url instead`);
|
|
@@ -138,13 +152,17 @@ function parseArgs(argv) {
|
|
|
138
152
|
else if (a === '--claude-only' || a === '--claude-code' || a === '--claude') pickClaude = true;
|
|
139
153
|
else if (a === '--cursor-only' || a === '--cursor') pickCursor = true;
|
|
140
154
|
else if (a === '--codex-only' || a === '--codex') pickCodex = true;
|
|
155
|
+
else if (a === '--vscode-only' || a === '--vscode' || a === '--copilot') pickVscode = true;
|
|
156
|
+
else if (a === '--antigravity-only' || a === '--antigravity') pickAntigravity = true;
|
|
141
157
|
else throw new Error(`Unknown argument: ${a}`);
|
|
142
158
|
}
|
|
143
|
-
out.targetExplicit = pickClaude || pickCursor || pickCodex;
|
|
159
|
+
out.targetExplicit = pickClaude || pickCursor || pickCodex || pickVscode || pickAntigravity;
|
|
144
160
|
if (out.targetExplicit) {
|
|
145
161
|
out.claude = pickClaude;
|
|
146
162
|
out.cursor = pickCursor;
|
|
147
163
|
out.codex = pickCodex;
|
|
164
|
+
out.vscode = pickVscode;
|
|
165
|
+
out.antigravity = pickAntigravity;
|
|
148
166
|
}
|
|
149
167
|
return out;
|
|
150
168
|
}
|
|
@@ -193,9 +211,7 @@ async function mergeMcpFile(absPath, serverEntry) {
|
|
|
193
211
|
try {
|
|
194
212
|
const raw = await readFile(absPath, 'utf8');
|
|
195
213
|
const parsed = JSON.parse(raw);
|
|
196
|
-
if (parsed && typeof parsed === 'object' &&
|
|
197
|
-
data.mcpServers = { ...parsed.mcpServers };
|
|
198
|
-
} else if (parsed && typeof parsed === 'object') {
|
|
214
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
199
215
|
data = { ...parsed, mcpServers: parsed.mcpServers && typeof parsed.mcpServers === 'object' ? { ...parsed.mcpServers } : {} };
|
|
200
216
|
}
|
|
201
217
|
} catch (e) {
|
|
@@ -207,6 +223,31 @@ async function mergeMcpFile(absPath, serverEntry) {
|
|
|
207
223
|
await writeFile(absPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
208
224
|
}
|
|
209
225
|
|
|
226
|
+
async function mergeVscodeMcpFile(absPath, serverEntry) {
|
|
227
|
+
let data = { servers: {} };
|
|
228
|
+
try {
|
|
229
|
+
const raw = await readFile(absPath, 'utf8');
|
|
230
|
+
const parsed = JSON.parse(raw);
|
|
231
|
+
if (parsed && typeof parsed === 'object') {
|
|
232
|
+
data = {
|
|
233
|
+
...parsed,
|
|
234
|
+
servers: parsed.servers && typeof parsed.servers === 'object' ? { ...parsed.servers } : {},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
if (e.code !== 'ENOENT') throw e;
|
|
239
|
+
}
|
|
240
|
+
data.servers = {
|
|
241
|
+
...data.servers,
|
|
242
|
+
[SERVER_KEY]: {
|
|
243
|
+
type: 'stdio',
|
|
244
|
+
...serverEntry,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
248
|
+
await writeFile(absPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
|
|
210
251
|
function tomlString(value) {
|
|
211
252
|
return JSON.stringify(String(value ?? ''));
|
|
212
253
|
}
|
|
@@ -285,50 +326,69 @@ async function readCodexEnfyraEnv(absPath) {
|
|
|
285
326
|
return null;
|
|
286
327
|
}
|
|
287
328
|
|
|
288
|
-
function getCodexConfigPath(root
|
|
289
|
-
return
|
|
329
|
+
function getCodexConfigPath(root) {
|
|
330
|
+
return join(root, '.codex', 'config.toml');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getClaudeConfigPath(root) {
|
|
334
|
+
return join(root, '.mcp.json');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getCursorConfigPath(root) {
|
|
338
|
+
return join(root, '.cursor', 'mcp.json');
|
|
290
339
|
}
|
|
291
340
|
|
|
292
|
-
function
|
|
293
|
-
return
|
|
341
|
+
function getVscodeConfigPath(root) {
|
|
342
|
+
return join(root, '.vscode', 'mcp.json');
|
|
294
343
|
}
|
|
295
344
|
|
|
296
|
-
function
|
|
297
|
-
return
|
|
345
|
+
function getAntigravityConfigPath(root) {
|
|
346
|
+
return join(root, '.agents', 'mcp_config.json');
|
|
298
347
|
}
|
|
299
348
|
|
|
300
|
-
function getClientPath(client, root
|
|
301
|
-
if (client === 'claude') return getClaudeConfigPath(root
|
|
302
|
-
if (client === 'cursor') return getCursorConfigPath(root
|
|
303
|
-
if (client === 'codex') return getCodexConfigPath(root
|
|
349
|
+
function getClientPath(client, root) {
|
|
350
|
+
if (client === 'claude') return getClaudeConfigPath(root);
|
|
351
|
+
if (client === 'cursor') return getCursorConfigPath(root);
|
|
352
|
+
if (client === 'codex') return getCodexConfigPath(root);
|
|
353
|
+
if (client === 'vscode') return getVscodeConfigPath(root);
|
|
354
|
+
if (client === 'antigravity') return getAntigravityConfigPath(root);
|
|
304
355
|
throw new Error(`Unknown MCP client: ${client}`);
|
|
305
356
|
}
|
|
306
357
|
|
|
307
|
-
async function
|
|
358
|
+
async function readMcpServerEnv(absPath, serverRootKey) {
|
|
359
|
+
try {
|
|
360
|
+
const raw = await readFile(absPath, 'utf8');
|
|
361
|
+
const j = JSON.parse(raw);
|
|
362
|
+
const e = j?.[serverRootKey]?.[SERVER_KEY]?.env;
|
|
363
|
+
if (e && typeof e === 'object' && (e.ENFYRA_API_URL || e.ENFYRA_API_TOKEN)) {
|
|
364
|
+
return {
|
|
365
|
+
apiUrl: typeof e.ENFYRA_API_URL === 'string' ? e.ENFYRA_API_URL : '',
|
|
366
|
+
apiToken: typeof e.ENFYRA_API_TOKEN === 'string' ? e.ENFYRA_API_TOKEN : '',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
/* */
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex, readVscode, readAntigravity) {
|
|
308
376
|
const paths = [];
|
|
309
|
-
if (readClaude) paths.push(getClaudeConfigPath(root,
|
|
310
|
-
if (readCursor) paths.push(getCursorConfigPath(root,
|
|
311
|
-
if (!
|
|
377
|
+
if (readClaude) paths.push({ path: getClaudeConfigPath(root), rootKey: 'mcpServers' });
|
|
378
|
+
if (readCursor) paths.push({ path: getCursorConfigPath(root), rootKey: 'mcpServers' });
|
|
379
|
+
if (!readClaude && readCursor) paths.push({ path: getClaudeConfigPath(root), rootKey: 'mcpServers' });
|
|
380
|
+
if (readVscode) paths.push({ path: getVscodeConfigPath(root), rootKey: 'servers' });
|
|
381
|
+
if (readAntigravity) paths.push({ path: getAntigravityConfigPath(root), rootKey: 'mcpServers' });
|
|
312
382
|
const seen = new Set();
|
|
313
|
-
for (const
|
|
314
|
-
|
|
315
|
-
seen.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const e = j?.mcpServers?.[SERVER_KEY]?.env;
|
|
320
|
-
if (e && typeof e === 'object' && (e.ENFYRA_API_URL || e.ENFYRA_API_TOKEN)) {
|
|
321
|
-
return {
|
|
322
|
-
apiUrl: typeof e.ENFYRA_API_URL === 'string' ? e.ENFYRA_API_URL : '',
|
|
323
|
-
apiToken: typeof e.ENFYRA_API_TOKEN === 'string' ? e.ENFYRA_API_TOKEN : '',
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
} catch {
|
|
327
|
-
/* */
|
|
328
|
-
}
|
|
383
|
+
for (const entry of paths) {
|
|
384
|
+
const key = `${entry.rootKey}:${entry.path}`;
|
|
385
|
+
if (seen.has(key)) continue;
|
|
386
|
+
seen.add(key);
|
|
387
|
+
const env = await readMcpServerEnv(entry.path, entry.rootKey);
|
|
388
|
+
if (env) return env;
|
|
329
389
|
}
|
|
330
390
|
if (readCodex) {
|
|
331
|
-
const codex = await readCodexEnfyraEnv(getCodexConfigPath(root
|
|
391
|
+
const codex = await readCodexEnfyraEnv(getCodexConfigPath(root));
|
|
332
392
|
if (codex) return codex;
|
|
333
393
|
}
|
|
334
394
|
return { apiUrl: '', apiToken: '' };
|
|
@@ -338,23 +398,31 @@ async function promptTargetChoice() {
|
|
|
338
398
|
const choices = [
|
|
339
399
|
{
|
|
340
400
|
client: 'codex',
|
|
341
|
-
value: { claude: false, cursor: false, codex: true },
|
|
401
|
+
value: { claude: false, cursor: false, codex: true, vscode: false, antigravity: false },
|
|
342
402
|
},
|
|
343
403
|
{
|
|
344
404
|
client: 'claude',
|
|
345
|
-
value: { claude: true, cursor: false, codex: false },
|
|
405
|
+
value: { claude: true, cursor: false, codex: false, vscode: false, antigravity: false },
|
|
346
406
|
},
|
|
347
407
|
{
|
|
348
408
|
client: 'cursor',
|
|
349
|
-
value: { claude: false, cursor: true, codex: false },
|
|
409
|
+
value: { claude: false, cursor: true, codex: false, vscode: false, antigravity: false },
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
client: 'vscode',
|
|
413
|
+
value: { claude: false, cursor: false, codex: false, vscode: true, antigravity: false },
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
client: 'antigravity',
|
|
417
|
+
value: { claude: false, cursor: false, codex: false, vscode: false, antigravity: true },
|
|
350
418
|
},
|
|
351
419
|
{
|
|
352
420
|
client: 'all',
|
|
353
|
-
value: { claude: true, cursor: true, codex: true },
|
|
421
|
+
value: { claude: true, cursor: true, codex: true, vscode: true, antigravity: true },
|
|
354
422
|
},
|
|
355
423
|
];
|
|
356
424
|
if (input.setRawMode && output.isTTY) {
|
|
357
|
-
return promptTargetSelect(choices,
|
|
425
|
+
return promptTargetSelect(choices, 5);
|
|
358
426
|
}
|
|
359
427
|
|
|
360
428
|
const rl = createInterface({ input, output });
|
|
@@ -363,23 +431,31 @@ async function promptTargetChoice() {
|
|
|
363
431
|
+ ' [1] Codex ./.codex/config.toml\n'
|
|
364
432
|
+ ' [2] Claude Code ./.mcp.json\n'
|
|
365
433
|
+ ' [3] Cursor ./.cursor/mcp.json\n'
|
|
366
|
-
+ ' [4]
|
|
367
|
-
+ '
|
|
434
|
+
+ ' [4] VS Code ./.vscode/mcp.json\n'
|
|
435
|
+
+ ' [5] Antigravity ./.agents/mcp_config.json\n'
|
|
436
|
+
+ ' [6] All [default]\n'
|
|
437
|
+
+ 'Choice [6]: ',
|
|
368
438
|
)).trim().toLowerCase();
|
|
369
439
|
await rl.close();
|
|
370
|
-
if (line === '' || line === '
|
|
371
|
-
return { claude: true, cursor: true, codex: true };
|
|
440
|
+
if (line === '' || line === '6' || line === 'all' || line === 'a') {
|
|
441
|
+
return { claude: true, cursor: true, codex: true, vscode: true, antigravity: true };
|
|
372
442
|
}
|
|
373
443
|
if (line === '1' || line === 'codex' || line === 'x') {
|
|
374
|
-
return { claude: false, cursor: false, codex: true };
|
|
444
|
+
return { claude: false, cursor: false, codex: true, vscode: false, antigravity: false };
|
|
375
445
|
}
|
|
376
446
|
if (line === '2' || line === 'claude' || line === 'claude-code') {
|
|
377
|
-
return { claude: true, cursor: false, codex: false };
|
|
447
|
+
return { claude: true, cursor: false, codex: false, vscode: false, antigravity: false };
|
|
378
448
|
}
|
|
379
449
|
if (line === '3' || line === 'cursor' || line === 'u') {
|
|
380
|
-
return { claude: false, cursor: true, codex: false };
|
|
450
|
+
return { claude: false, cursor: true, codex: false, vscode: false, antigravity: false };
|
|
451
|
+
}
|
|
452
|
+
if (line === '4' || line === 'vscode' || line === 'vs-code' || line === 'copilot') {
|
|
453
|
+
return { claude: false, cursor: false, codex: false, vscode: true, antigravity: false };
|
|
454
|
+
}
|
|
455
|
+
if (line === '5' || line === 'antigravity') {
|
|
456
|
+
return { claude: false, cursor: false, codex: false, vscode: false, antigravity: true };
|
|
381
457
|
}
|
|
382
|
-
return { claude: true, cursor: true, codex: true };
|
|
458
|
+
return { claude: true, cursor: true, codex: true, vscode: true, antigravity: true };
|
|
383
459
|
}
|
|
384
460
|
|
|
385
461
|
async function promptTargetSelect(choices, initialIndex = 0) {
|
|
@@ -392,7 +468,7 @@ async function promptTargetSelect(choices, initialIndex = 0) {
|
|
|
392
468
|
if (choice.client === 'all') {
|
|
393
469
|
const label = active ? style.bold(style.underline('All supported clients')) : 'All supported clients';
|
|
394
470
|
const paddedLabel = label + ' '.repeat(22 - 'All supported clients'.length);
|
|
395
|
-
const hint = active ? style.cyan('Codex + Claude Code + Cursor') : style.dim('Codex + Claude Code + Cursor');
|
|
471
|
+
const hint = active ? style.cyan('Codex + Claude Code + Cursor + VS Code + Antigravity') : style.dim('Codex + Claude Code + Cursor + VS Code + Antigravity');
|
|
396
472
|
return `${accent} ${indicator} ${paddedLabel} ${hint}`;
|
|
397
473
|
}
|
|
398
474
|
|
|
@@ -545,14 +621,18 @@ export async function runLocalConfig(argv) {
|
|
|
545
621
|
let writeClaude = opts.claude;
|
|
546
622
|
let writeCursor = opts.cursor;
|
|
547
623
|
let writeCodex = opts.codex;
|
|
624
|
+
let writeVscode = opts.vscode;
|
|
625
|
+
let writeAntigravity = opts.antigravity;
|
|
548
626
|
if (usePrompt && (!opts.targetExplicit || opts.reconfig)) {
|
|
549
627
|
const t = await promptTargetChoice();
|
|
550
628
|
writeClaude = t.claude;
|
|
551
629
|
writeCursor = t.cursor;
|
|
552
630
|
writeCodex = t.codex;
|
|
631
|
+
writeVscode = t.vscode;
|
|
632
|
+
writeAntigravity = t.antigravity;
|
|
553
633
|
}
|
|
554
634
|
|
|
555
|
-
const existing = await loadExistingEnfyraEnv(root, writeClaude, writeCursor, writeCodex,
|
|
635
|
+
const existing = await loadExistingEnfyraEnv(root, writeClaude, writeCursor, writeCodex, writeVscode, writeAntigravity);
|
|
556
636
|
|
|
557
637
|
let apiUrl;
|
|
558
638
|
let apiToken;
|
|
@@ -578,23 +658,32 @@ export async function runLocalConfig(argv) {
|
|
|
578
658
|
const written = [];
|
|
579
659
|
|
|
580
660
|
if (writeCodex) {
|
|
581
|
-
const p = getClientPath('codex', root
|
|
661
|
+
const p = getClientPath('codex', root);
|
|
582
662
|
await mergeCodexConfig(p, apiUrl, apiToken);
|
|
583
663
|
written.push({ client: 'codex', path: p });
|
|
584
664
|
}
|
|
585
665
|
if (writeClaude) {
|
|
586
|
-
const p = getClientPath('claude', root
|
|
666
|
+
const p = getClientPath('claude', root);
|
|
587
667
|
await mergeMcpFile(p, serverEntry);
|
|
588
668
|
written.push({ client: 'claude', path: p });
|
|
589
669
|
}
|
|
590
670
|
if (writeCursor) {
|
|
591
|
-
const p = getClientPath('cursor', root
|
|
671
|
+
const p = getClientPath('cursor', root);
|
|
592
672
|
await mergeMcpFile(p, serverEntry);
|
|
593
673
|
written.push({ client: 'cursor', path: p });
|
|
594
674
|
}
|
|
675
|
+
if (writeVscode) {
|
|
676
|
+
const p = getClientPath('vscode', root);
|
|
677
|
+
await mergeVscodeMcpFile(p, serverEntry);
|
|
678
|
+
written.push({ client: 'vscode', path: p });
|
|
679
|
+
}
|
|
680
|
+
if (writeAntigravity) {
|
|
681
|
+
const p = getClientPath('antigravity', root);
|
|
682
|
+
await mergeMcpFile(p, serverEntry);
|
|
683
|
+
written.push({ client: 'antigravity', path: p });
|
|
684
|
+
}
|
|
595
685
|
|
|
596
|
-
|
|
597
|
-
console.log(`${statusIcon('success')} ${style.bold(style.green('Enfyra MCP config updated'))} ${style.dim(`(${scopeLabel})`)}\n`);
|
|
686
|
+
console.log(`${statusIcon('success')} ${style.bold(style.green('Enfyra MCP config updated'))} ${style.dim('(project)')}\n`);
|
|
598
687
|
for (const entry of written) {
|
|
599
688
|
const meta = clients[entry.client];
|
|
600
689
|
console.log(` ${style.cyan('•')} ${style.bold(meta.color(meta.label))}`);
|
|
@@ -612,6 +701,12 @@ export async function runLocalConfig(argv) {
|
|
|
612
701
|
if (selectedClients.has('cursor')) {
|
|
613
702
|
console.log(' - Cursor: restart Cursor or reload MCP, then confirm the server under Settings -> MCP.');
|
|
614
703
|
}
|
|
704
|
+
if (selectedClients.has('vscode')) {
|
|
705
|
+
console.log(' - VS Code / Copilot: run MCP: List Servers or reload the workspace, then start the Enfyra server if prompted.');
|
|
706
|
+
}
|
|
707
|
+
if (selectedClients.has('antigravity')) {
|
|
708
|
+
console.log(' - Antigravity: reopen the workspace or reload MCP servers so ./.agents/mcp_config.json is picked up.');
|
|
709
|
+
}
|
|
615
710
|
console.log(' - Re-run this command anytime to update the same Enfyra entries.');
|
|
616
711
|
if (!apiToken) {
|
|
617
712
|
console.log(`\n${statusIcon('warn')} ${style.yellow('ENFYRA_API_TOKEN is empty; tools will not authenticate until it is set.')}`);
|
package/src/lib/mcp-examples.js
CHANGED
|
@@ -894,7 +894,8 @@ query_table({
|
|
|
894
894
|
notes: [
|
|
895
895
|
'Use _max(relation.field) for latest-child ordering, _min(relation.field) for earliest-child ordering, and _count(relation) for child-count ordering.',
|
|
896
896
|
'Aggregate sort helpers only work on direct one-to-many or many-to-many list relations.',
|
|
897
|
-
'The aggregate field must be a real non-encrypted scalar field on the related table.',
|
|
897
|
+
'The aggregate field must be a real published, non-encrypted scalar field on the related table for user-facing APIs.',
|
|
898
|
+
'Do not use _max, _min, or _count on private relations or unpublished fields unless the endpoint intentionally exposes that fact.',
|
|
898
899
|
'Do not use raw sort=-messages.createdAt for parent ordering; it is ambiguous and rejected.',
|
|
899
900
|
'deep.messages.sort only orders the loaded message rows inside each ticket, so keep parent sort and child pagination as separate concerns.',
|
|
900
901
|
],
|
|
@@ -943,6 +944,7 @@ const found = await #integrations.find({
|
|
|
943
944
|
routeId: "<route_id>",
|
|
944
945
|
method: "POST",
|
|
945
946
|
scriptLanguage: "javascript",
|
|
947
|
+
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
946
948
|
sourceCode: \`const email = @BODY.email
|
|
947
949
|
if (!email) @THROW400("Email is required")
|
|
948
950
|
|
|
@@ -950,6 +952,7 @@ return { ok: true, email }\`
|
|
|
950
952
|
})`,
|
|
951
953
|
notes: [
|
|
952
954
|
'Use sourceCode, not logic. The server generates compiledCode.',
|
|
955
|
+
'Call get_enfyra_required_knowledge before saving dynamic code and pass dynamicCodeAckKey as knowledgeAckKey.',
|
|
953
956
|
'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
|
|
954
957
|
'Do not pass name to enfyra_route_handler; one handler is identified by route + method.',
|
|
955
958
|
],
|
|
@@ -1030,6 +1033,7 @@ const scope = {
|
|
|
1030
1033
|
name: "strip_email_verification_fields",
|
|
1031
1034
|
methods: ["PATCH"],
|
|
1032
1035
|
priority: -10,
|
|
1036
|
+
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1033
1037
|
code: \`delete @BODY.emailVerifiedAt
|
|
1034
1038
|
delete @BODY.emailVerificationStatus
|
|
1035
1039
|
delete @BODY.emailVerificationSentAt\`
|
|
@@ -1047,6 +1051,7 @@ delete @BODY.emailVerificationSentAt\`
|
|
|
1047
1051
|
name: "shape_display_title",
|
|
1048
1052
|
methods: ["GET"],
|
|
1049
1053
|
priority: 0,
|
|
1054
|
+
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1050
1055
|
code: \`if (@ERROR) {
|
|
1051
1056
|
@LOGS("Request failed", @ERROR.message)
|
|
1052
1057
|
return
|
|
@@ -1320,7 +1325,7 @@ const socket = io("/chat", {
|
|
|
1320
1325
|
code: `const conversationId = @BODY.conversationId
|
|
1321
1326
|
if (!conversationId) @THROW400("conversationId is required")
|
|
1322
1327
|
|
|
1323
|
-
const membership = await @REPOS.chat_conversation_member.find({
|
|
1328
|
+
const membership = await @REPOS.secure.chat_conversation_member.find({
|
|
1324
1329
|
filter: {
|
|
1325
1330
|
conversation: { id: { _eq: conversationId } },
|
|
1326
1331
|
member: { id: { _eq: @USER.id } }
|
|
@@ -1336,6 +1341,7 @@ if (!membership.data[0]) @THROW403("Not a conversation member")
|
|
|
1336
1341
|
'Join conversation rooms, not member-id rooms.',
|
|
1337
1342
|
'conversationId is a request/room identifier; DB filters still use the relation property conversation.',
|
|
1338
1343
|
'Check membership server-side; do not trust the client.',
|
|
1344
|
+
'Use @REPOS.secure.<table> for explicit table access in user-facing websocket scripts.',
|
|
1339
1345
|
],
|
|
1340
1346
|
},
|
|
1341
1347
|
{
|
|
@@ -1343,7 +1349,7 @@ if (!membership.data[0]) @THROW403("Not a conversation member")
|
|
|
1343
1349
|
code: `const { conversationId, text, clientId } = @BODY
|
|
1344
1350
|
if (!conversationId || !text) @THROW400("conversationId and text are required")
|
|
1345
1351
|
|
|
1346
|
-
const membership = await @REPOS.chat_conversation_member.find({
|
|
1352
|
+
const membership = await @REPOS.secure.chat_conversation_member.find({
|
|
1347
1353
|
filter: {
|
|
1348
1354
|
conversation: { id: { _eq: conversationId } },
|
|
1349
1355
|
member: { id: { _eq: @USER.id } }
|
|
@@ -1352,7 +1358,7 @@ const membership = await @REPOS.chat_conversation_member.find({
|
|
|
1352
1358
|
})
|
|
1353
1359
|
if (!membership.data[0]) @THROW403("Not a conversation member")
|
|
1354
1360
|
|
|
1355
|
-
const created = await @REPOS.chat_message.create({
|
|
1361
|
+
const created = await @REPOS.secure.chat_message.create({
|
|
1356
1362
|
data: {
|
|
1357
1363
|
conversation: { id: conversationId },
|
|
1358
1364
|
sender: { id: @USER.id },
|
|
@@ -1363,7 +1369,7 @@ const created = await @REPOS.chat_message.create({
|
|
|
1363
1369
|
|
|
1364
1370
|
const message = created.data?.[0] ?? null
|
|
1365
1371
|
if (message?.id) {
|
|
1366
|
-
await @REPOS.chat_conversation.update({
|
|
1372
|
+
await @REPOS.secure.chat_conversation.update({
|
|
1367
1373
|
id: conversationId,
|
|
1368
1374
|
data: { lastMessage: { id: message.id }, updatedAt: message.createdAt || new Date().toISOString() }
|
|
1369
1375
|
})
|
|
@@ -1378,6 +1384,7 @@ return { ok: true, message }`,
|
|
|
1378
1384
|
'Do not ask the client for senderId. The sender relation is derived from @USER.id.',
|
|
1379
1385
|
'conversationId is accepted only as the room/business identifier; persistence uses relation properties conversation and sender, not physical FK fields.',
|
|
1380
1386
|
'Event scripts should explicitly emit replies/broadcasts.',
|
|
1387
|
+
'Use @REPOS.secure.<table> for explicit table access in user-facing websocket scripts; trusted @REPOS.<table> is only for internal/admin logic that will sanitize output.',
|
|
1381
1388
|
],
|
|
1382
1389
|
},
|
|
1383
1390
|
],
|
|
@@ -1551,7 +1558,8 @@ ensure_page_extension({
|
|
|
1551
1558
|
description: "Reports dashboard",
|
|
1552
1559
|
menuId: "<created-menu-id>",
|
|
1553
1560
|
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\\"><article class=\\"eapp-surface-card p-4\\"><div class=\\"flex items-start justify-between gap-3\\"><div><p class=\\"text-sm font-medium eapp-text-tertiary\\">Total</p><p class=\\"mt-2 text-2xl font-semibold eapp-text-primary\\">0</p></div><span class=\\"eapp-primary-soft eapp-icon-tile\\"><span class=\\"eapp-primary-text\\">◆</span></span></div><div class=\\"mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted\\"><div class=\\"eapp-primary-solid h-full w-1/2\\"></div></div></article><article class=\\"eapp-primary-surface eapp-radius-panel border p-4\\"><p class=\\"text-sm font-semibold eapp-text-primary\\">Selected report</p><p class=\\"mt-1 text-sm eapp-text-tertiary\\">Only selected/current identity blocks use identity surface.</p></article></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); const { register: registerHeaderActions } = useHeaderActionRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); registerHeaderActions([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', color: 'neutral', variant: 'outline', onClick: () => {}, order: 80 }])</script>",
|
|
1554
|
-
isEnabled: true
|
|
1561
|
+
isEnabled: true,
|
|
1562
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1555
1563
|
})`,
|
|
1556
1564
|
notes: [
|
|
1557
1565
|
'Menu provides navigation; extension provides content.',
|
|
@@ -1559,6 +1567,7 @@ ensure_page_extension({
|
|
|
1559
1567
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1560
1568
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1561
1569
|
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
|
|
1570
|
+
'Call get_enfyra_required_knowledge before saving extension code and pass extensionAckKey as extensionKnowledgeAckKey.',
|
|
1562
1571
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1563
1572
|
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1564
1573
|
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
@@ -1623,7 +1632,8 @@ ensure_widget_extension({
|
|
|
1623
1632
|
name: "ReportStatusWidget",
|
|
1624
1633
|
description: "Report status summary cards",
|
|
1625
1634
|
code: reportStatusWidgetCode,
|
|
1626
|
-
isEnabled: true
|
|
1635
|
+
isEnabled: true,
|
|
1636
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1627
1637
|
})
|
|
1628
1638
|
|
|
1629
1639
|
// Read the created widget record id, then embed it from the page extension.
|
|
@@ -1631,7 +1641,8 @@ ensure_page_extension({
|
|
|
1631
1641
|
name: "ReportsPage",
|
|
1632
1642
|
menuId: "<reports-menu-id>",
|
|
1633
1643
|
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
|
|
1634
|
-
isEnabled: true
|
|
1644
|
+
isEnabled: true,
|
|
1645
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1635
1646
|
})`,
|
|
1636
1647
|
notes: [
|
|
1637
1648
|
'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
|
|
@@ -1707,7 +1718,8 @@ ensure_global_extension({
|
|
|
1707
1718
|
name: "NotificationBellGlobal",
|
|
1708
1719
|
description: "Registers the app-wide notification bell in the account panel",
|
|
1709
1720
|
code: notificationBellCode,
|
|
1710
|
-
isEnabled: true
|
|
1721
|
+
isEnabled: true,
|
|
1722
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1711
1723
|
})`,
|
|
1712
1724
|
notes: [
|
|
1713
1725
|
'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
|
|
@@ -2030,6 +2042,7 @@ onMounted(() => Promise.all([flowStats.execute(), orderStats.execute()]))
|
|
|
2030
2042
|
'Aggregate keys must be real fields or relations.',
|
|
2031
2043
|
'Read results from response.meta.aggregate.',
|
|
2032
2044
|
'Use top-level filter for time windows and cross-field conditions.',
|
|
2045
|
+
'Only aggregate fields and relations that the dashboard is allowed to expose; aggregate values can reveal hidden data even when rows omit that field.',
|
|
2033
2046
|
'sum/avg require numeric fields; amount_usd must be a real float/numeric SQL column, not metadata-only float over a varchar physical column.',
|
|
2034
2047
|
],
|
|
2035
2048
|
},
|
|
@@ -27,25 +27,30 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
27
27
|
'- For a quick target/base sanity check, call `get_enfyra_api_context`; do not call broad discovery just to confirm which instance this MCP is connected to.',
|
|
28
28
|
'- Discover before deciding. For architecture/capability questions call `discover_enfyra_system`; for DB/pk/runtime/cache context call `discover_runtime_context`; for filters/deep/sort/relation query shape call `discover_query_capabilities`. Run broad discovery tools sequentially, not in parallel.',
|
|
29
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
|
-
'- Load examples only when needed.
|
|
30
|
+
'- Load examples only when needed. Use `get_enfyra_examples` by category. Before extension UI, call `get_extension_theme_contract`; call `get_theme_class_reference` for exact eapp/Nuxt UI theme classes.',
|
|
31
31
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
|
+
'- Before writing dynamic server or extension code, call `get_enfyra_required_knowledge` and pass the matching acknowledgement key into write tools.',
|
|
32
33
|
'- With non-root API tokens, call `get_permission_profile` before relying on admin helper tools or when debugging 403s. MCP admin helpers require ordinary route permissions for static admin routes such as `/admin/script/validate`, `/admin/test/run`, `/admin/flow/trigger/:id`, and `/admin/reload/*`.',
|
|
33
|
-
'- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow
|
|
34
|
+
'- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow`, `create_api_endpoint`, `enable_route`, `add_route_methods`, `public_route_methods`, `set_table_graphql`, guard/permission/rule tools, websocket tools, flow tools, and `ensure_page_extension`/menu tools.',
|
|
34
35
|
'- Before saving standalone dynamic script or extension code, call `validate_dynamic_script` or `validate_extension_code` unless the chosen ensure/update tool already validates the code.',
|
|
35
36
|
'- For existing script-backed records, use `trace_metadata_usage` then `get_script_source`; edit with `patch_script_source` or `update_script_source` so source is hash-checked and validated.',
|
|
36
37
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
37
38
|
'',
|
|
38
39
|
'### Core Contracts',
|
|
39
|
-
'- Tool JSON responses use `responseFormat: "json+columnar-v1"`.
|
|
40
|
+
'- Tool JSON responses use `responseFormat: "json+columnar-v1"`. If rows are columnar, read values by matching `columns[index]` to `rows[n][index]`; do not guess row keys.',
|
|
40
41
|
'- `query_table`, `get_all_routes`, and `get_all_tables` require explicit intent: pass `limit` for bounded reads or `all: true` for a complete list. Do not invent arbitrary limits such as 30 or 50.',
|
|
41
42
|
'- Read tools are minimal by default. Pass explicit `fields`; use metadata inspection before guessing field/relation names. Field exclusion mode exists: `fields=-compiledCode`, and `fields=id,-compiledCode` still means all readable fields except `compiledCode`.',
|
|
42
43
|
'- Mutations return ids/status by default. Re-read with `find_one_record` or `query_table` and explicit `fields` when the saved row matters.',
|
|
43
|
-
'- Dynamic repository reads use `filter`, not `where`: `@REPOS.table.find({ filter: {...} })`, `#table.find({ filter: {...} })`, and `exists(filter)`.',
|
|
44
|
+
'- Dynamic repository reads use `filter`, not `where`: `@REPOS.table.find({ filter: {...} })`, `@REPOS.secure.table.find({ filter: {...} })`, `#table.find({ filter: {...} })`, and `exists(filter)`.',
|
|
45
|
+
'- Dynamic repositories have two trust paths. Use secure `@REPOS.main` or `@REPOS.secure.<table>` for user-facing data. `@REPOS.<table>` is trusted/internal and can see hidden fields; never return raw trusted rows to users.',
|
|
46
|
+
'- Secure repository choice is not a substitute for authorization. Handlers and hooks still need route access, owner/tenant filters, and explicit checks before returning or mutating records.',
|
|
47
|
+
'- Filters, sort helpers, counts, and aggregates over unpublished fields/private relations are sensitive data surfaces; do not expose them in user-facing endpoints.',
|
|
44
48
|
'- Use `enfyra_user` as the user table. Model record links as real relations using relation `propertyName` values, not physical FK fields like `userId`, `conversationId`, `senderId`, or `memberId` in generated DB code.',
|
|
45
49
|
'- Relation design must stay minimal. Create the owning relation needed for writes/filters first; add `inversePropertyName` only when a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal will use that reverse field. For schema work, explicitly review existing relations and mention which inverses are intentionally present or intentionally omitted.',
|
|
46
50
|
'- Do not call internal/no-route system tables such as `enfyra_column` or `enfyra_session` through generic CRUD. Use table/column/relation tools and route-backed tables discovered from metadata.',
|
|
47
51
|
'- Custom API paths use `api_endpoint_workflow` when a handler is needed and the model should follow returned nextSteps. Use lower-level `create_route` without `mainTableId` only when intentionally creating a route shell; `create_table` is only for new persisted data.',
|
|
48
52
|
'- For canonical table reads and RLS, preserve client-controlled query shape: do not override `@QUERY.fields`, `@QUERY.deep`, `@QUERY.sort`, `@QUERY.limit`, `@QUERY.page`, `@QUERY.meta`, `@QUERY.aggregate`, or `debugMode`. Merge only security filters into `@QUERY.filter`.',
|
|
53
|
+
'- If a REST read returns a column or relation marked `isPublished=false`, including through dotted relation fields such as `fields=owner.secret` or equivalent `deep` projections, treat it as an Enfyra core support issue. Confirm the minimal repro with `test_rest_endpoint`, tell the user to send a Cloud/support ticket with the table, field path, and response shape, and do not present route-local pre-hooks or frontend hiding as the real fix.',
|
|
49
54
|
'- Script source is `sourceCode`; `compiledCode` is generated and may differ textually because macros expand. Do not warn about source/compiled mismatch unless validation or runtime behavior proves the compiled artifact is stale.',
|
|
50
55
|
'- For intentional user/domain errors in scripts use `@THROW400`-style helpers or `$ctx.$throw[...]`, not `throw new Error(...)`.',
|
|
51
56
|
'- Destructive operations are preview-first. Do not pass `confirm=true` until the user explicitly approves.',
|
|
@@ -2,6 +2,13 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import { fetchAPI } from './fetch.js';
|
|
4
4
|
import { validateScriptSourceIfPresent } from './mutation-guards.js';
|
|
5
|
+
import {
|
|
6
|
+
assertDynamicCodeKnowledgeAck,
|
|
7
|
+
assertDynamicCodeKnowledgeAckIf,
|
|
8
|
+
assertExtensionKnowledgeAck,
|
|
9
|
+
dynamicCodeKnowledgeAckParam,
|
|
10
|
+
extensionKnowledgeAckParam,
|
|
11
|
+
} from './required-knowledge.js';
|
|
5
12
|
|
|
6
13
|
function unwrapData(result) {
|
|
7
14
|
return Array.isArray(result?.data) ? result.data : [];
|
|
@@ -656,7 +663,9 @@ async function ensureExtension(apiUrl, {
|
|
|
656
663
|
description,
|
|
657
664
|
isEnabled = true,
|
|
658
665
|
version = '1.0.0',
|
|
666
|
+
extensionKnowledgeAckKey,
|
|
659
667
|
}) {
|
|
668
|
+
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|
|
660
669
|
if (type === 'page' && !menuId) {
|
|
661
670
|
throw new Error('menuId is required for page extensions. Use ensure_menu first, then ensure_page_extension.');
|
|
662
671
|
}
|
|
@@ -718,6 +727,7 @@ async function ensureFlowStep(apiUrl, {
|
|
|
718
727
|
scriptLanguage,
|
|
719
728
|
timeout,
|
|
720
729
|
isEnabled,
|
|
730
|
+
knowledgeAckKey,
|
|
721
731
|
}) {
|
|
722
732
|
if (!flowName && !flowId) throw new Error('Provide flowName or flowId.');
|
|
723
733
|
if (flowName && flowId) throw new Error('Provide flowName or flowId, not both.');
|
|
@@ -726,6 +736,7 @@ async function ensureFlowStep(apiUrl, {
|
|
|
726
736
|
: await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: flowName } }, 'id,_id,name');
|
|
727
737
|
if (!flow) throw new Error(`Flow not found: ${flowId || flowName}`);
|
|
728
738
|
const parsedConfig = parseJsonObjectArg('config', config, {});
|
|
739
|
+
assertDynamicCodeKnowledgeAckIf(Boolean(sourceCode && ['script', 'condition'].includes(type)), knowledgeAckKey);
|
|
729
740
|
const validation = sourceCode && ['script', 'condition'].includes(type)
|
|
730
741
|
? await validateDynamicScript(apiUrl, sourceCode, scriptLanguage)
|
|
731
742
|
: { validated: false, reason: 'no script validation required' };
|
|
@@ -971,7 +982,12 @@ async function resolveApiEndpointWorkflowState(apiUrl, opts) {
|
|
|
971
982
|
nextSteps: blocked
|
|
972
983
|
? [{ tool: 'api_endpoint_workflow', input: { path: normalizedPath, method: methodName, overwrite: true }, reason: blocked.reason }]
|
|
973
984
|
: firstRunnable
|
|
974
|
-
? [{
|
|
985
|
+
? [{
|
|
986
|
+
tool: 'api_endpoint_workflow',
|
|
987
|
+
input: { path: normalizedPath, method: methodName, apply: true },
|
|
988
|
+
stepId: firstRunnable.id,
|
|
989
|
+
requiresKnowledgeAck: firstRunnable.id === 'save_handler' ? 'dynamicCodeAckKey from get_enfyra_required_knowledge' : undefined,
|
|
990
|
+
}]
|
|
975
991
|
: [],
|
|
976
992
|
};
|
|
977
993
|
}
|
|
@@ -1020,6 +1036,7 @@ async function applyApiEndpointWorkflowStep(apiUrl, state, opts, stepId) {
|
|
|
1020
1036
|
}
|
|
1021
1037
|
|
|
1022
1038
|
if (selectedStep.id === 'save_handler') {
|
|
1039
|
+
assertDynamicCodeKnowledgeAck(opts.knowledgeAckKey);
|
|
1023
1040
|
if (!endpoint.routeId) throw new Error('Route must exist before saving handler.');
|
|
1024
1041
|
const body = {
|
|
1025
1042
|
sourceCode: opts.sourceCode,
|
|
@@ -1097,6 +1114,9 @@ async function runApiEndpointWorkflow(apiUrl, opts) {
|
|
|
1097
1114
|
const operations = [];
|
|
1098
1115
|
let completedEphemeralStepId = null;
|
|
1099
1116
|
if (opts.apply || opts.applyAll) {
|
|
1117
|
+
if (opts.applyAll && state.steps.some((item) => item.id === 'save_handler' && ['pending', 'waiting'].includes(item.status))) {
|
|
1118
|
+
assertDynamicCodeKnowledgeAck(opts.knowledgeAckKey);
|
|
1119
|
+
}
|
|
1100
1120
|
const maxSteps = opts.applyAll ? 10 : 1;
|
|
1101
1121
|
for (let i = 0; i < maxSteps; i += 1) {
|
|
1102
1122
|
if (state.blocked || !state.firstRunnable) break;
|
|
@@ -1369,6 +1389,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1369
1389
|
apply: z.boolean().optional().default(false).describe('false returns plan only; true applies exactly the next pending step.'),
|
|
1370
1390
|
applyAll: z.boolean().optional().default(false).describe('true applies all safe pending steps in order. Prefer apply=true for production changes.'),
|
|
1371
1391
|
stepId: z.string().optional().describe('Optional pending step id to apply. Omit to apply the next pending step.'),
|
|
1392
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply/applyAll reaches the save_handler step. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
|
|
1372
1393
|
},
|
|
1373
1394
|
async (input) => jsonText(await runApiEndpointWorkflow(ENFYRA_API_URL, input)),
|
|
1374
1395
|
);
|
|
@@ -1392,8 +1413,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1392
1413
|
overwrite: z.boolean().optional().default(false).describe('If a handler already exists for route+method, false fails; true updates its sourceCode.'),
|
|
1393
1414
|
smokeTestQuery: z.string().optional().describe('Optional query JSON object for a smoke test after save, e.g. {"a":"1","b":"2"}.'),
|
|
1394
1415
|
smokeTestBody: z.string().optional().describe('Optional body JSON object for a smoke test after save.'),
|
|
1416
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
1395
1417
|
},
|
|
1396
|
-
async ({ path, method, sourceCode, scriptLanguage, public: makePublic, description, timeout, overwrite, smokeTestQuery, smokeTestBody }) => {
|
|
1418
|
+
async ({ path, method, sourceCode, scriptLanguage, public: makePublic, description, timeout, overwrite, smokeTestQuery, smokeTestBody, knowledgeAckKey }) => {
|
|
1419
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
1397
1420
|
const normalizedPath = normalizeRestPath(path);
|
|
1398
1421
|
const methodName = normalizeMethodName(method);
|
|
1399
1422
|
const { methodMap, methodIdNameMap } = await getMethodContext(ENFYRA_API_URL);
|
|
@@ -1700,8 +1723,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1700
1723
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for connection handler.'),
|
|
1701
1724
|
isEnabled: z.boolean().optional().default(true).describe('Enable gateway.'),
|
|
1702
1725
|
description: z.string().optional().describe('Admin note.'),
|
|
1726
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when sourceCode is provided. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
|
|
1703
1727
|
},
|
|
1704
|
-
async ({ path, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
1728
|
+
async ({ path, sourceCode, scriptLanguage, isEnabled, description, knowledgeAckKey }) => {
|
|
1729
|
+
assertDynamicCodeKnowledgeAckIf(sourceCode !== undefined, knowledgeAckKey);
|
|
1705
1730
|
const normalizedPath = normalizeRestPath(path);
|
|
1706
1731
|
const validation = sourceCode === undefined
|
|
1707
1732
|
? { validated: false, reason: 'no sourceCode' }
|
|
@@ -1730,8 +1755,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1730
1755
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1731
1756
|
isEnabled: z.boolean().optional().default(true).describe('Enable event.'),
|
|
1732
1757
|
description: z.string().optional().describe('Admin note.'),
|
|
1758
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
1733
1759
|
},
|
|
1734
|
-
async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description }) => {
|
|
1760
|
+
async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description, knowledgeAckKey }) => {
|
|
1761
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
1735
1762
|
if (!gatewayPath && !gatewayId) throw new Error('Provide gatewayPath or gatewayId.');
|
|
1736
1763
|
if (gatewayPath && gatewayId) throw new Error('Provide gatewayPath or gatewayId, not both.');
|
|
1737
1764
|
const gateway = gatewayId
|
|
@@ -1833,6 +1860,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1833
1860
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1834
1861
|
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1835
1862
|
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1863
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
1836
1864
|
},
|
|
1837
1865
|
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1838
1866
|
...input,
|
|
@@ -1853,6 +1881,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1853
1881
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
|
|
1854
1882
|
timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
|
|
1855
1883
|
isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
|
|
1884
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
1856
1885
|
},
|
|
1857
1886
|
async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
|
|
1858
1887
|
...input,
|
|
@@ -2033,6 +2062,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2033
2062
|
description: z.string().optional().describe('Extension description.'),
|
|
2034
2063
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
2035
2064
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
2065
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2036
2066
|
},
|
|
2037
2067
|
async (input) => jsonText({
|
|
2038
2068
|
action: 'page_extension_ensured',
|
|
@@ -2049,6 +2079,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2049
2079
|
description: z.string().optional().describe('Extension description.'),
|
|
2050
2080
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
2051
2081
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
2082
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2052
2083
|
},
|
|
2053
2084
|
async (input) => jsonText({
|
|
2054
2085
|
action: 'global_extension_ensured',
|
|
@@ -2065,6 +2096,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2065
2096
|
description: z.string().optional().describe('Extension description.'),
|
|
2066
2097
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
2067
2098
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
2099
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2068
2100
|
},
|
|
2069
2101
|
async (input) => jsonText({
|
|
2070
2102
|
action: 'widget_extension_ensured',
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export const DYNAMIC_CODE_KNOWLEDGE_ACK_KEY = 'EFYRA::SECURE-REPO-CONTRACT::R9x-kelp-42Q::NO-RAW-TRUSTED';
|
|
2
|
+
export const EXTENSION_KNOWLEDGE_ACK_KEY = 'EFYRA::EXTENSION-THEME-CONTRACT::VIOLET-IS-NOT-A-PLAN::7mQ';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_KNOWLEDGE_VERSION = '2026-06-30.secure-repo-v1';
|
|
5
|
+
|
|
6
|
+
export function dynamicCodeKnowledgeAckParam(z) {
|
|
7
|
+
return z.string().describe('Required dynamic-code acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the dynamic server code knowledge, then pass dynamicCodeAckKey exactly.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extensionKnowledgeAckParam(z) {
|
|
11
|
+
return z.string().describe('Required extension acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the extension/theme knowledge, then pass extensionAckKey exactly.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertDynamicCodeKnowledgeAck(key) {
|
|
15
|
+
if (key !== DYNAMIC_CODE_KNOWLEDGE_ACK_KEY) {
|
|
16
|
+
throw new Error('Missing or invalid dynamic-code knowledge acknowledgement. Call get_enfyra_required_knowledge, read the dynamic server code contracts, then pass dynamicCodeAckKey as knowledgeAckKey.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertDynamicCodeKnowledgeAckIf(condition, key) {
|
|
21
|
+
if (condition) assertDynamicCodeKnowledgeAck(key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertExtensionKnowledgeAck(key) {
|
|
25
|
+
if (key !== EXTENSION_KNOWLEDGE_ACK_KEY) {
|
|
26
|
+
throw new Error('Missing or invalid extension knowledge acknowledgement. Call get_enfyra_required_knowledge, read the extension/theme contracts, then pass extensionAckKey as extensionKnowledgeAckKey.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertExtensionKnowledgeAckIf(condition, key) {
|
|
31
|
+
if (condition) assertExtensionKnowledgeAck(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildRequiredKnowledgePayload() {
|
|
35
|
+
return {
|
|
36
|
+
version: REQUIRED_KNOWLEDGE_VERSION,
|
|
37
|
+
purpose: 'Read this before generating or saving Enfyra dynamic server code through MCP.',
|
|
38
|
+
dynamicCodeAckKey: DYNAMIC_CODE_KNOWLEDGE_ACK_KEY,
|
|
39
|
+
extensionAckKey: EXTENSION_KNOWLEDGE_ACK_KEY,
|
|
40
|
+
usage: [
|
|
41
|
+
'Pass dynamicCodeAckKey exactly as knowledgeAckKey when calling MCP tools that create or update dynamic server code.',
|
|
42
|
+
'Pass extensionAckKey exactly as extensionKnowledgeAckKey when calling MCP tools that create or update Enfyra extension code.',
|
|
43
|
+
],
|
|
44
|
+
dynamicServerCode: [
|
|
45
|
+
{
|
|
46
|
+
id: 'secure-vs-trusted-repositories',
|
|
47
|
+
rules: [
|
|
48
|
+
'@REPOS.main is the secure repository for the current route main table and preserves normal route query behavior.',
|
|
49
|
+
'@REPOS.secure.<table> is the secure explicit-table repository. Use it for public/user-facing custom handlers, hooks, websocket scripts, flows that return data, and third-party app integrations.',
|
|
50
|
+
'@REPOS.<table> is the trusted internal repository. It may read/write hidden fields and is only for server-owned maintenance/admin logic that intentionally needs that access.',
|
|
51
|
+
'Never return raw trusted-repository records to users. Project or sanitize output before returning it.',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'authorization-is-separate',
|
|
56
|
+
rules: [
|
|
57
|
+
'Secure repository selection does not prove the user is allowed to access a record.',
|
|
58
|
+
'Handlers and hooks still need route access, owner/tenant filters, membership checks, and explicit mutation authorization.',
|
|
59
|
+
'For canonical table reads and RLS, merge security filters into @QUERY.filter and preserve @QUERY.fields, @QUERY.deep, @QUERY.sort, @QUERY.limit, @QUERY.page, @QUERY.meta, @QUERY.aggregate, and debugMode.',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'hidden-field-query-surfaces',
|
|
64
|
+
rules: [
|
|
65
|
+
'Unpublished fields and private relations are sensitive even when the field is not selected.',
|
|
66
|
+
'Do not expose filter predicate-oracle behavior over hidden fields in user-facing endpoints.',
|
|
67
|
+
'Do not expose aggregate, _max, _min, _count, sort helpers, or counts over unpublished fields/private relations unless the endpoint intentionally exposes that fact.',
|
|
68
|
+
'If a normal REST read returns an isPublished=false field through fields/deep/dotted projection, treat it as an Enfyra core bug and verify the minimal REST repro.',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'dynamic-script-shape',
|
|
73
|
+
rules: [
|
|
74
|
+
'Use sourceCode and scriptLanguage; never send compiledCode.',
|
|
75
|
+
'Prefer macros such as @BODY, @QUERY, @PARAMS, @USER, @REQ, @RES, @REPOS, @HELPERS, @STORAGE, @SOCKET, and @THROW* when available.',
|
|
76
|
+
'Repository reads use filter, not where.',
|
|
77
|
+
'Create/update repository calls return collection-shaped data arrays; read result.data?.[0] for a single row.',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
extensions: [
|
|
82
|
+
{
|
|
83
|
+
id: 'theme-contract-first',
|
|
84
|
+
rules: [
|
|
85
|
+
'Call get_extension_theme_contract before writing or reviewing page, widget, or global extension UI.',
|
|
86
|
+
'Call get_theme_class_reference when an exact eapp-* class or Nuxt UI color mapping is needed.',
|
|
87
|
+
'Use eapp-surface-*, eapp-text-*, eapp-divide-y, and eapp-divider for neutral app-shell surfaces.',
|
|
88
|
+
'Use eapp-primary-* or Nuxt UI primary only for runtime-primary identity/accent intent controlled by the app color picker.',
|
|
89
|
+
'Use semantic state colors only for true status/error/warning/success/info indicators, not for large KPI/list containers.',
|
|
90
|
+
'Do not use raw CSS variable utilities such as text-[var(...)], bg-[var(...)], or border-[var(...)] when class tokens exist.',
|
|
91
|
+
'Do not hard-code concrete palettes such as color="violet" or Tailwind palette accents for themeable UI.',
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'extension-shell-boundary',
|
|
96
|
+
rules: [
|
|
97
|
+
'Extension roots render inside the Enfyra admin app shell. Do not add root-level page padding such as p-4 sm:p-6 xl:p-8.',
|
|
98
|
+
'Page extensions should be full-bleed by default and responsive from the first version.',
|
|
99
|
+
'Do not wrap whole pages in decorative cards; use cards only for repeated items, modals, or genuinely framed tools.',
|
|
100
|
+
'Use NuxtLink or Nuxt UI components with :to for visible navigation links; reserve navigateTo for imperative side effects.',
|
|
101
|
+
'Admin extension links for record management should point to /data/<table>, not public website paths stored on records.',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'extension-runtime-contract',
|
|
106
|
+
rules: [
|
|
107
|
+
'Save extensions as enfyra_extension Vue SFC records; no static import statements in extension code.',
|
|
108
|
+
'Load app packages with getPackages(["package-name"]) inside extension runtime code.',
|
|
109
|
+
'Prefer FormEditor/FormEditorLazy for direct table-backed forms when the form maps to metadata fields.',
|
|
110
|
+
'For long admin setup workflows, open CommonDrawer immediately and show loading/error/content inside it.',
|
|
111
|
+
'Use Widget with numeric enfyra_extension widget ids; pass safe reactive props/events and keep page-level mutation ownership in the page unless the widget intentionally owns the workflow.',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -22,7 +22,15 @@ import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instruct
|
|
|
22
22
|
import { getExamples, listExampleCategories } from './lib/mcp-examples.js';
|
|
23
23
|
import { registerTableTools } from './lib/table-tools.js';
|
|
24
24
|
import { registerPlatformOperationTools } from './lib/platform-operation-tools.js';
|
|
25
|
-
import { prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
25
|
+
import { parseRecordData, prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
26
|
+
import {
|
|
27
|
+
assertDynamicCodeKnowledgeAck,
|
|
28
|
+
assertDynamicCodeKnowledgeAckIf,
|
|
29
|
+
assertExtensionKnowledgeAckIf,
|
|
30
|
+
buildRequiredKnowledgePayload,
|
|
31
|
+
dynamicCodeKnowledgeAckParam,
|
|
32
|
+
extensionKnowledgeAckParam,
|
|
33
|
+
} from './lib/required-knowledge.js';
|
|
26
34
|
import { validateMainTableRoutePath } from './lib/route-guards.js';
|
|
27
35
|
import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
|
|
28
36
|
import {
|
|
@@ -208,6 +216,7 @@ const SCRIPT_BACKED_TABLES = [
|
|
|
208
216
|
'enfyra_graphql',
|
|
209
217
|
'enfyra_bootstrap_script',
|
|
210
218
|
];
|
|
219
|
+
const SCRIPT_BACKED_TABLE_SET = new Set(SCRIPT_BACKED_TABLES);
|
|
211
220
|
|
|
212
221
|
const SCRIPT_SOURCE_FIELDS = [
|
|
213
222
|
'sourceCode',
|
|
@@ -596,6 +605,12 @@ async function prepareGenericMutation(tableName, data) {
|
|
|
596
605
|
});
|
|
597
606
|
}
|
|
598
607
|
|
|
608
|
+
function assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey }) {
|
|
609
|
+
const payload = parseRecordData(data);
|
|
610
|
+
assertDynamicCodeKnowledgeAckIf(SCRIPT_BACKED_TABLE_SET.has(tableName) && typeof payload.sourceCode === 'string', knowledgeAckKey);
|
|
611
|
+
assertExtensionKnowledgeAckIf(tableName === 'enfyra_extension' && typeof payload.code === 'string', extensionKnowledgeAckKey);
|
|
612
|
+
}
|
|
613
|
+
|
|
599
614
|
function parseQueryParamsArg(queryParams) {
|
|
600
615
|
const parsed = parseJsonArg(queryParams, {});
|
|
601
616
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
@@ -775,6 +790,16 @@ installColumnarToolFormatter(server);
|
|
|
775
790
|
// METADATA TOOLS
|
|
776
791
|
// ============================================================================
|
|
777
792
|
|
|
793
|
+
server.tool(
|
|
794
|
+
'get_enfyra_required_knowledge',
|
|
795
|
+
[
|
|
796
|
+
'Return required Enfyra knowledge and acknowledgement keys for MCP code-writing tools.',
|
|
797
|
+
'Call this before creating or updating dynamic server code or Enfyra extension code. Read the returned contracts and pass the matching ack key into write tools.',
|
|
798
|
+
].join(' '),
|
|
799
|
+
{},
|
|
800
|
+
async () => jsonContent(buildRequiredKnowledgePayload()),
|
|
801
|
+
);
|
|
802
|
+
|
|
778
803
|
server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Use get_table_metadata or inspect_table for detail.', {
|
|
779
804
|
includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
|
|
780
805
|
search: z.string().optional().describe('Optional table-name/alias substring filter.'),
|
|
@@ -1051,6 +1076,7 @@ server.tool(
|
|
|
1051
1076
|
deep: 'Nested relation fetch object keyed by relation propertyName.',
|
|
1052
1077
|
},
|
|
1053
1078
|
countPattern: 'For counts, query only fields=id with limit=1 and request meta. Use meta=totalCount without a filter, or meta=filterCount when a filter is supplied. MCP count_records wraps this pattern.',
|
|
1079
|
+
security: 'Filters, sorts, counts, and aggregate values can leak information even when a field is not selected. In generated public/user-facing APIs, do not filter, sort, count, or aggregate unpublished fields or private relations unless the endpoint intentionally exposes that fact.',
|
|
1054
1080
|
deep: {
|
|
1055
1081
|
shape: '{ [relationName]: { fields?, filter?, sort?, limit?, page?, deep? } }',
|
|
1056
1082
|
rules: [
|
|
@@ -1180,7 +1206,7 @@ server.tool(
|
|
|
1180
1206
|
},
|
|
1181
1207
|
handler: {
|
|
1182
1208
|
runs: 'Main route logic, or canonical CRUD if no handler overrides.',
|
|
1183
|
-
data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@REQ', '@RES when response streaming is available', '@UPLOADED_FILE for multipart request file metadata', '@REPOS.main', '@REPOS.<table>', '@CACHE', '@HELPERS', '@FETCH', '@STORAGE', '@PKGS', '@SOCKET global emit helpers/roomSize', '@TRIGGER'],
|
|
1209
|
+
data: ['@BODY', '@QUERY', '@PARAMS', '@USER', '@REQ', '@RES when response streaming is available', '@UPLOADED_FILE for multipart request file metadata', '@REPOS.main secure route main table repo', '@REPOS.secure.<table> secure explicit table repo', '@REPOS.<table> trusted internal table repo', '@CACHE', '@HELPERS', '@FETCH', '@STORAGE', '@PKGS', '@SOCKET global emit helpers/roomSize', '@TRIGGER'],
|
|
1184
1210
|
queryContract: 'When a handler wraps a canonical table read, pass through client fields/deep/sort/page/limit/meta/aggregate/debugMode unless the route is a clearly custom summary or workflow endpoint.',
|
|
1185
1211
|
returnBehavior: 'Return value becomes response body unless post-hook changes it.',
|
|
1186
1212
|
},
|
|
@@ -1217,7 +1243,9 @@ server.tool(
|
|
|
1217
1243
|
},
|
|
1218
1244
|
helpers: {
|
|
1219
1245
|
repos: {
|
|
1220
|
-
scopes: '$repos.main is the
|
|
1246
|
+
scopes: '$repos.main is the secure repository for the route main table and preserves normal route query behavior. $repos.secure.<table> is the secure repository for explicit table access in public/user-facing custom handlers, hooks, websocket scripts, flows that return data, and third-party app integrations. $repos.<table> is a trusted internal repository for server-owned maintenance/admin logic that intentionally needs hidden fields; never return raw trusted-repo records to users.',
|
|
1247
|
+
security: 'Secure repos enforce the normal field visibility/projection path, including unpublished columns and relations. Trusted repos bypass that exposure boundary; if trusted access is necessary, project or sanitize the result before returning it. Authorization is still required in either path: enforce route access plus owner/tenant filters or membership checks.',
|
|
1248
|
+
sensitiveQuerySurface: 'Filters, sort helpers, counts, and aggregate values on unpublished fields or private relations can leak information even when the value is not selected. Do not expose aggregate, _max, _min, _count, or predicate-oracle behavior over hidden fields in generated user-facing endpoints.',
|
|
1221
1249
|
mutationReturnShape: '$repos.<table>.create({ data }) and $repos.<table>.update({ id, data }) return a collection-shaped result: { data: [...], count? }. data is always an array for create/update, even for one created/updated record. If a script needs the single record object, it must read result.data[0] or result.data?.[0] ?? null.',
|
|
1222
1250
|
preferredExample: 'const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;',
|
|
1223
1251
|
wrongSingleRecordAccess: 'Do not use result.data.id, do not return result.data when one object is expected, and do not assume create/update returns the bare row object.',
|
|
@@ -1288,7 +1316,7 @@ server.tool('query_table', 'Query any route-backed table. Response is minimal un
|
|
|
1288
1316
|
fields: z.array(z.string()).optional().describe('Fields to select. If omitted, MCP selects only the table primary key to avoid oversized responses.'),
|
|
1289
1317
|
meta: z.string().optional().describe('Optional REST meta request, e.g. "totalCount", "filterCount", or aggregate modes supported by the route. Use count_records for simple counts.'),
|
|
1290
1318
|
deep: z.string().optional().describe('Optional deep relation fetch object as JSON string. Keys must be relation propertyName values.'),
|
|
1291
|
-
aggregate: z.string().optional().describe('Optional aggregate object as JSON string, keyed by real fields/relations. Results are returned in response.meta.aggregate when supported.'),
|
|
1319
|
+
aggregate: z.string().optional().describe('Optional aggregate object as JSON string, keyed by real fields/relations. Results are returned in response.meta.aggregate when supported. Do not request aggregates over hidden fields/private relations in user-facing APIs.'),
|
|
1292
1320
|
}, async ({ tableName, filter, sort, page, limit, all, fields, meta, deep, aggregate }) => {
|
|
1293
1321
|
if (!all && limit === undefined) {
|
|
1294
1322
|
throw new Error('query_table requires either limit or all=true. Do not rely on implicit default page sizes.');
|
|
@@ -1442,8 +1470,11 @@ server.tool('create_record', 'Create a new record in any route-backed table. The
|
|
|
1442
1470
|
tableName: z.string().describe('Table name to insert into'),
|
|
1443
1471
|
data: z.string().describe('Record data as JSON string'),
|
|
1444
1472
|
queryParams: z.string().optional().describe('Optional query params as JSON object string, e.g. {"expired_at":"2026-09-20"}. Use for route contracts that intentionally keep workflow fields out of the validated body.'),
|
|
1445
|
-
|
|
1473
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
|
|
1474
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and data contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
|
|
1475
|
+
}, async ({ tableName, data, queryParams, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1446
1476
|
validateTableName(tableName);
|
|
1477
|
+
assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
|
|
1447
1478
|
const prepared = await prepareGenericMutation(tableName, data);
|
|
1448
1479
|
const query = parseQueryParamsArg(queryParams);
|
|
1449
1480
|
const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}`, query), { method: 'POST', body: JSON.stringify(prepared.payload) });
|
|
@@ -1458,8 +1489,11 @@ server.tool('update_record', 'Update an existing record by ID using PATCH. The t
|
|
|
1458
1489
|
id: z.string().describe('Record ID to update'),
|
|
1459
1490
|
data: z.string().describe('Fields to update as JSON string'),
|
|
1460
1491
|
queryParams: z.string().optional().describe('Optional query params as JSON object string for route contracts that intentionally keep workflow fields out of the validated body.'),
|
|
1461
|
-
|
|
1492
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
|
|
1493
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and data contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
|
|
1494
|
+
}, async ({ tableName, id, data, queryParams, knowledgeAckKey, extensionKnowledgeAckKey }) => {
|
|
1462
1495
|
validateTableName(tableName);
|
|
1496
|
+
assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
|
|
1463
1497
|
const prepared = await prepareGenericMutation(tableName, data);
|
|
1464
1498
|
const query = parseQueryParamsArg(queryParams);
|
|
1465
1499
|
const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${id}`, query), { method: 'PATCH', body: JSON.stringify(prepared.payload) });
|
|
@@ -1511,8 +1545,9 @@ server.tool(
|
|
|
1511
1545
|
expectedSourceSha256: z.string().optional().describe('Optional SHA-256 from get_script_source; fails if source changed.'),
|
|
1512
1546
|
scriptLanguage: z.string().optional().describe('Script language to save. Defaults to existing scriptLanguage or javascript.'),
|
|
1513
1547
|
apply: z.boolean().optional().default(false).describe('false returns preview only; true validates and saves.'),
|
|
1548
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply=true. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
|
|
1514
1549
|
},
|
|
1515
|
-
async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply }) => {
|
|
1550
|
+
async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply, knowledgeAckKey }) => {
|
|
1516
1551
|
const { record, sourceField, sourceCode } = await fetchScriptRecord(tableName, id);
|
|
1517
1552
|
if (sourceField !== 'sourceCode') {
|
|
1518
1553
|
throw new Error(`patch_script_source only saves sourceCode records. Record uses "${sourceField}"; use update_record intentionally for this legacy field.`);
|
|
@@ -1543,6 +1578,7 @@ server.tool(
|
|
|
1543
1578
|
if (!apply) {
|
|
1544
1579
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
1545
1580
|
}
|
|
1581
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
1546
1582
|
const language = scriptLanguage || record.scriptLanguage || 'javascript';
|
|
1547
1583
|
const prepared = await prepareGenericMutation(
|
|
1548
1584
|
tableName,
|
|
@@ -1584,8 +1620,10 @@ server.tool(
|
|
|
1584
1620
|
id: z.string().describe('Record ID to update'),
|
|
1585
1621
|
sourceCode: z.string().describe('Editable script sourceCode. Pass the raw code string; do not JSON-escape it yourself.'),
|
|
1586
1622
|
scriptLanguage: z.string().optional().default('javascript').describe('Script language, usually javascript or typescript'),
|
|
1623
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
1587
1624
|
},
|
|
1588
|
-
async ({ tableName, id, sourceCode, scriptLanguage }) => {
|
|
1625
|
+
async ({ tableName, id, sourceCode, scriptLanguage, knowledgeAckKey }) => {
|
|
1626
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
1589
1627
|
validateTableName(tableName);
|
|
1590
1628
|
const prepared = await prepareGenericMutation(
|
|
1591
1629
|
tableName,
|
|
@@ -2472,8 +2510,10 @@ server.tool(
|
|
|
2472
2510
|
sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
|
|
2473
2511
|
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
|
|
2474
2512
|
timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
|
|
2513
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
2475
2514
|
},
|
|
2476
|
-
async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout }) => {
|
|
2515
|
+
async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, knowledgeAckKey }) => {
|
|
2516
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
2477
2517
|
const methodNames = methods && methods.length > 0 ? methods : method ? [method] : [];
|
|
2478
2518
|
if (methodNames.length === 0) throw new Error('Provide method or methods');
|
|
2479
2519
|
const methodMap = await getMethodMap();
|
|
@@ -2533,8 +2573,10 @@ server.tool(
|
|
|
2533
2573
|
.describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
|
|
2534
2574
|
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
2535
2575
|
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
2576
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
2536
2577
|
},
|
|
2537
|
-
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled }) => {
|
|
2578
|
+
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
|
|
2579
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
2538
2580
|
const methodMap = await getMethodMap();
|
|
2539
2581
|
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
2540
2582
|
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, 'enfyra_pre_hook', {
|
|
@@ -2587,8 +2629,10 @@ server.tool(
|
|
|
2587
2629
|
.describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
|
|
2588
2630
|
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
2589
2631
|
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
2632
|
+
knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
|
|
2590
2633
|
},
|
|
2591
|
-
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled }) => {
|
|
2634
|
+
async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
|
|
2635
|
+
assertDynamicCodeKnowledgeAck(knowledgeAckKey);
|
|
2592
2636
|
const methodMap = await getMethodMap();
|
|
2593
2637
|
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
2594
2638
|
const scriptValidation = await validateScriptSourceIfPresent(fetchAPI, ENFYRA_API_URL, 'enfyra_post_hook', {
|