@enfyra/mcp-server 0.1.10 → 0.1.12
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 +25 -23
- package/package.json +4 -3
- package/src/index.mjs +3 -3
- package/src/lib/config-local.mjs +13 -12
- package/src/lib/mcp-examples.js +3 -3
- package/src/lib/mcp-instructions.js +1 -0
- package/src/lib/platform-operation-tools.js +91 -0
- package/src/lib/required-knowledge.js +12 -2
- package/src/lib/table-tools.js +51 -6
package/README.md
CHANGED
|
@@ -9,24 +9,26 @@ This package is the MCP bridge only. Assistant rules, schema behavior, dynamic s
|
|
|
9
9
|
From your project root:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx @enfyra/mcp-server config
|
|
12
|
+
npx @enfyra/mcp-server@latest config
|
|
13
13
|
```
|
|
14
14
|
|
|
15
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
|
|
|
19
|
+
Generated MCP host configs run `npx -y @enfyra/mcp-server@latest` so every host start resolves the current npm `latest` dist-tag published by Enfyra.
|
|
20
|
+
|
|
19
21
|
```bash
|
|
20
22
|
# Non-interactive, all supported clients
|
|
21
|
-
npx @enfyra/mcp-server config --yes \
|
|
23
|
+
npx @enfyra/mcp-server@latest config --yes \
|
|
22
24
|
--app-url http://localhost:3000 \
|
|
23
25
|
-t efy_pat_your-token
|
|
24
26
|
|
|
25
27
|
# One or more clients
|
|
26
|
-
npx @enfyra/mcp-server config --codex
|
|
27
|
-
npx @enfyra/mcp-server config --cursor --claude-code
|
|
28
|
-
npx @enfyra/mcp-server config --vscode
|
|
29
|
-
npx @enfyra/mcp-server config --antigravity
|
|
28
|
+
npx @enfyra/mcp-server@latest config --codex
|
|
29
|
+
npx @enfyra/mcp-server@latest config --cursor --claude-code
|
|
30
|
+
npx @enfyra/mcp-server@latest config --vscode
|
|
31
|
+
npx @enfyra/mcp-server@latest config --antigravity
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
Equivalent in this repo:
|
|
@@ -39,18 +41,18 @@ yarn mcp:config
|
|
|
39
41
|
|
|
40
42
|
| Client | Command | Project config |
|
|
41
43
|
|--------|---------|----------------|
|
|
42
|
-
| Codex | `npx @enfyra/mcp-server config --codex` | `.codex/config.toml` |
|
|
43
|
-
| Claude Code | `npx @enfyra/mcp-server config --claude-code` | `.mcp.json` |
|
|
44
|
-
| Cursor | `npx @enfyra/mcp-server config --cursor` | `.cursor/mcp.json` |
|
|
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` |
|
|
44
|
+
| Codex | `npx @enfyra/mcp-server@latest config --codex` | `.codex/config.toml` |
|
|
45
|
+
| Claude Code | `npx @enfyra/mcp-server@latest config --claude-code` | `.mcp.json` |
|
|
46
|
+
| Cursor | `npx @enfyra/mcp-server@latest config --cursor` | `.cursor/mcp.json` |
|
|
47
|
+
| VS Code / GitHub Copilot | `npx @enfyra/mcp-server@latest config --vscode` | `.vscode/mcp.json` |
|
|
48
|
+
| Google Antigravity | `npx @enfyra/mcp-server@latest config --antigravity` | `.agents/mcp_config.json` |
|
|
47
49
|
| MCP Inspector / other project-scoped hosts | Paste the shared STDIO config below | Host-specific project config |
|
|
48
50
|
|
|
49
51
|
<details>
|
|
50
52
|
<summary><strong>Codex setup</strong></summary>
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
|
-
npx @enfyra/mcp-server config --codex
|
|
55
|
+
npx @enfyra/mcp-server@latest config --codex
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Generated project config:
|
|
@@ -58,7 +60,7 @@ Generated project config:
|
|
|
58
60
|
```toml
|
|
59
61
|
[mcp_servers.enfyra]
|
|
60
62
|
command = "npx"
|
|
61
|
-
args = ["-y", "@enfyra/mcp-server"]
|
|
63
|
+
args = ["-y", "@enfyra/mcp-server@latest"]
|
|
62
64
|
|
|
63
65
|
[mcp_servers.enfyra.env]
|
|
64
66
|
ENFYRA_API_URL = "http://localhost:3000/api"
|
|
@@ -77,7 +79,7 @@ Official reference: [Codex config](https://developers.openai.com/codex/config-re
|
|
|
77
79
|
<summary><strong>Claude Code setup</strong></summary>
|
|
78
80
|
|
|
79
81
|
```bash
|
|
80
|
-
npx @enfyra/mcp-server config --claude-code
|
|
82
|
+
npx @enfyra/mcp-server@latest config --claude-code
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
Project config is written to `.mcp.json`. MCP server definitions do not belong in `.claude/settings.json`.
|
|
@@ -88,7 +90,7 @@ Claude Code also supports its own CLI:
|
|
|
88
90
|
claude mcp add --transport stdio --scope project \
|
|
89
91
|
--env ENFYRA_API_URL=http://localhost:3000/api \
|
|
90
92
|
--env ENFYRA_API_TOKEN=efy_pat_your-token \
|
|
91
|
-
enfyra -- npx -y @enfyra/mcp-server
|
|
93
|
+
enfyra -- npx -y @enfyra/mcp-server@latest
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
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.
|
|
@@ -101,7 +103,7 @@ Official references: [Claude Code MCP](https://docs.anthropic.com/en/docs/claude
|
|
|
101
103
|
<summary><strong>Cursor setup</strong></summary>
|
|
102
104
|
|
|
103
105
|
```bash
|
|
104
|
-
npx @enfyra/mcp-server config --cursor
|
|
106
|
+
npx @enfyra/mcp-server@latest config --cursor
|
|
105
107
|
```
|
|
106
108
|
|
|
107
109
|
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.
|
|
@@ -116,7 +118,7 @@ Official reference: [Cursor MCP](https://cursor.com/docs/context/mcp).
|
|
|
116
118
|
<summary><strong>VS Code / GitHub Copilot setup</strong></summary>
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
|
-
npx @enfyra/mcp-server config --vscode
|
|
121
|
+
npx @enfyra/mcp-server@latest config --vscode
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
VS Code workspace config is written to `.vscode/mcp.json`:
|
|
@@ -127,7 +129,7 @@ VS Code workspace config is written to `.vscode/mcp.json`:
|
|
|
127
129
|
"enfyra": {
|
|
128
130
|
"type": "stdio",
|
|
129
131
|
"command": "npx",
|
|
130
|
-
"args": ["-y", "@enfyra/mcp-server"],
|
|
132
|
+
"args": ["-y", "@enfyra/mcp-server@latest"],
|
|
131
133
|
"env": {
|
|
132
134
|
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
133
135
|
"ENFYRA_API_TOKEN": "efy_pat_your-token"
|
|
@@ -147,7 +149,7 @@ Official references: [VS Code MCP servers](https://code.visualstudio.com/docs/co
|
|
|
147
149
|
<summary><strong>Google Antigravity setup</strong></summary>
|
|
148
150
|
|
|
149
151
|
```bash
|
|
150
|
-
npx @enfyra/mcp-server config --antigravity
|
|
152
|
+
npx @enfyra/mcp-server@latest config --antigravity
|
|
151
153
|
```
|
|
152
154
|
|
|
153
155
|
Antigravity project config is written to `.agents/mcp_config.json`:
|
|
@@ -157,7 +159,7 @@ Antigravity project config is written to `.agents/mcp_config.json`:
|
|
|
157
159
|
"mcpServers": {
|
|
158
160
|
"enfyra": {
|
|
159
161
|
"command": "npx",
|
|
160
|
-
"args": ["-y", "@enfyra/mcp-server"],
|
|
162
|
+
"args": ["-y", "@enfyra/mcp-server@latest"],
|
|
161
163
|
"env": {
|
|
162
164
|
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
163
165
|
"ENFYRA_API_TOKEN": "efy_pat_your-token"
|
|
@@ -183,7 +185,7 @@ Use the shared STDIO config with any project-scoped host that accepts an `mcpSer
|
|
|
183
185
|
"mcpServers": {
|
|
184
186
|
"enfyra": {
|
|
185
187
|
"command": "npx",
|
|
186
|
-
"args": ["-y", "@enfyra/mcp-server"],
|
|
188
|
+
"args": ["-y", "@enfyra/mcp-server@latest"],
|
|
187
189
|
"env": {
|
|
188
190
|
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
189
191
|
"ENFYRA_API_TOKEN": "efy_pat_your-token"
|
|
@@ -202,7 +204,7 @@ Official reference: [MCP Inspector](https://modelcontextprotocol.io/docs/tools/i
|
|
|
202
204
|
## Config Command
|
|
203
205
|
|
|
204
206
|
```bash
|
|
205
|
-
npx @enfyra/mcp-server config [options]
|
|
207
|
+
npx @enfyra/mcp-server@latest config [options]
|
|
206
208
|
```
|
|
207
209
|
|
|
208
210
|
| Option | Use |
|
|
@@ -256,7 +258,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
256
258
|
- Write tools require `get_enfyra_required_knowledge` acknowledgement before mutating Enfyra state. Discovery, validation, and preview tools remain available without the acknowledgement so agents can read and plan first. If the acknowledgement is missing, the tool error tells the caller to read `get_enfyra_required_knowledge` and pass the required key.
|
|
257
259
|
- Script-backed records validate `sourceCode` through `/admin/script/validate` before saving.
|
|
258
260
|
- `validate_dynamic_script` checks handler, hook, flow, websocket, GraphQL, and bootstrap script source without saving.
|
|
259
|
-
- `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
261
|
+
- `validate_extension_code` locally rejects common extension component-resolution mistakes, such as `resolveComponent()` or lowercase auto-injected component tags like `<ubutton>`, then checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
260
262
|
- 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.
|
|
261
263
|
- `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
|
|
262
264
|
- Long source/code values in read responses are written to `/tmp/enfyra-mcp-sources` and returned as length/hash/preview/tmpFile metadata so LLM callers can inspect full source from the file path without truncating tool output.
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enfyra/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "yarn@4.17.0",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"main": "src/index.mjs",
|
|
8
9
|
"bin": {
|
|
9
|
-
"mcp-server": "src/index.mjs",
|
|
10
|
-
"
|
|
10
|
+
"enfyra-mcp-server": "src/index.mjs",
|
|
11
|
+
"mcp-server": "src/index.mjs"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src"
|
package/src/index.mjs
CHANGED
|
@@ -10,8 +10,8 @@ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
|
10
10
|
console.log(`Enfyra MCP Server
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
|
-
npx @enfyra/mcp-server Start the MCP stdio server
|
|
14
|
-
npx @enfyra/mcp-server config [flags] Write project-local MCP host config
|
|
13
|
+
npx @enfyra/mcp-server@latest Start the MCP stdio server
|
|
14
|
+
npx @enfyra/mcp-server@latest config [flags] Write project-local MCP host config
|
|
15
15
|
|
|
16
16
|
Common config flags:
|
|
17
17
|
--codex Write ./.codex/config.toml
|
|
@@ -25,7 +25,7 @@ Common config flags:
|
|
|
25
25
|
-t, --api-token ENFYRA_API_TOKEN
|
|
26
26
|
-h, --help Show config help
|
|
27
27
|
|
|
28
|
-
Run \`npx @enfyra/mcp-server config --help\` for full config details.
|
|
28
|
+
Run \`npx @enfyra/mcp-server@latest config --help\` for full config details.
|
|
29
29
|
`);
|
|
30
30
|
process.exit(0);
|
|
31
31
|
}
|
package/src/lib/config-local.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { stdin as input, stdout as output, cwd } from 'node:process';
|
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
|
|
7
7
|
const SERVER_KEY = 'enfyra';
|
|
8
|
+
const MCP_PACKAGE_SPEC = '@enfyra/mcp-server@latest';
|
|
8
9
|
const forceColor = process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== '0';
|
|
9
10
|
const canStyle = forceColor || (output.isTTY && process.env.NO_COLOR == null);
|
|
10
11
|
const style = {
|
|
@@ -67,7 +68,7 @@ function printHelp() {
|
|
|
67
68
|
${style.dim('Write project-local MCP client config for Enfyra.')}
|
|
68
69
|
|
|
69
70
|
${style.bold('Usage')}
|
|
70
|
-
npx @enfyra/mcp-server config [options]
|
|
71
|
+
npx @enfyra/mcp-server@latest config [options]
|
|
71
72
|
|
|
72
73
|
${style.bold('Supported clients')}
|
|
73
74
|
Codex ./.codex/config.toml
|
|
@@ -99,15 +100,15 @@ ${style.bold('Interactive mode')}
|
|
|
99
100
|
Existing Enfyra config and environment variables are used as defaults. Re-run anytime to update.
|
|
100
101
|
|
|
101
102
|
${style.bold('Examples')}
|
|
102
|
-
npx @enfyra/mcp-server config
|
|
103
|
-
npx @enfyra/mcp-server config --yes
|
|
104
|
-
npx @enfyra/mcp-server config --codex --cursor
|
|
105
|
-
npx @enfyra/mcp-server config --vscode
|
|
106
|
-
npx @enfyra/mcp-server config --antigravity
|
|
107
|
-
npx @enfyra/mcp-server config --claude-code
|
|
108
|
-
npx @enfyra/mcp-server config --reconfig
|
|
109
|
-
npx @enfyra/mcp-server config --app-url http://localhost:3000 -t 'efy_pat_...'
|
|
110
|
-
ENFYRA_APP_URL=https://demo.enfyra.io ENFYRA_API_TOKEN=efy_pat_... npx @enfyra/mcp-server config --yes
|
|
103
|
+
npx @enfyra/mcp-server@latest config
|
|
104
|
+
npx @enfyra/mcp-server@latest config --yes
|
|
105
|
+
npx @enfyra/mcp-server@latest config --codex --cursor
|
|
106
|
+
npx @enfyra/mcp-server@latest config --vscode
|
|
107
|
+
npx @enfyra/mcp-server@latest config --antigravity
|
|
108
|
+
npx @enfyra/mcp-server@latest config --claude-code
|
|
109
|
+
npx @enfyra/mcp-server@latest config --reconfig
|
|
110
|
+
npx @enfyra/mcp-server@latest config --app-url http://localhost:3000 -t 'efy_pat_...'
|
|
111
|
+
ENFYRA_APP_URL=https://demo.enfyra.io ENFYRA_API_TOKEN=efy_pat_... npx @enfyra/mcp-server@latest config --yes
|
|
111
112
|
`);
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -170,7 +171,7 @@ function parseArgs(argv) {
|
|
|
170
171
|
function buildServerEntry(apiUrl, apiToken) {
|
|
171
172
|
return {
|
|
172
173
|
command: 'npx',
|
|
173
|
-
args: ['-y',
|
|
174
|
+
args: ['-y', MCP_PACKAGE_SPEC],
|
|
174
175
|
env: {
|
|
175
176
|
ENFYRA_API_URL: apiUrl,
|
|
176
177
|
ENFYRA_API_TOKEN: apiToken,
|
|
@@ -256,7 +257,7 @@ function buildCodexTomlBlock(apiUrl, apiToken) {
|
|
|
256
257
|
return [
|
|
257
258
|
'[mcp_servers.enfyra]',
|
|
258
259
|
'command = "npx"',
|
|
259
|
-
|
|
260
|
+
`args = ["-y", "${MCP_PACKAGE_SPEC}"]`,
|
|
260
261
|
'',
|
|
261
262
|
'[mcp_servers.enfyra.env]',
|
|
262
263
|
`ENFYRA_API_URL = ${tomlString(apiUrl)}`,
|
package/src/lib/mcp-examples.js
CHANGED
|
@@ -667,13 +667,13 @@ update_record({
|
|
|
667
667
|
]),
|
|
668
668
|
uniques: JSON.stringify([["message", "member"]]),
|
|
669
669
|
indexes: JSON.stringify([
|
|
670
|
-
["
|
|
671
|
-
["conversation", "member", "isRead"]
|
|
670
|
+
["conversation", "isRead"]
|
|
672
671
|
])
|
|
673
672
|
})`,
|
|
674
673
|
notes: [
|
|
675
674
|
'Unread is per user and per message; do not put global read state on conversation.',
|
|
676
|
-
'
|
|
675
|
+
'message and member appear in the unique constraint, so they must not be added to indexes; unique fields are already indexed by the unique constraint.',
|
|
676
|
+
'readAt is a datetime field and gets its own auto index; explicit indexes should cover only non-unique hot lookup fields.',
|
|
677
677
|
'For chat-list UX, default to a boolean unread dot instead of exact counts.',
|
|
678
678
|
],
|
|
679
679
|
},
|
|
@@ -34,6 +34,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
34
34
|
'- 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/*`.',
|
|
35
35
|
'- Prefer the most specific business operation tool over raw metadata CRUD. `discover_enfyra_workflows` provides the current operation-tool map and negative-routing avoidTools.',
|
|
36
36
|
'- 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.',
|
|
37
|
+
'- Extension SFCs must use auto-injected components directly in templates, such as `<UButton>`, and must not call `resolveComponent()` for Nuxt UI/eApp components.',
|
|
37
38
|
'- 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.',
|
|
38
39
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
39
40
|
'',
|
|
@@ -12,6 +12,36 @@ import {
|
|
|
12
12
|
globalRulesAckParam,
|
|
13
13
|
} from './required-knowledge.js';
|
|
14
14
|
|
|
15
|
+
const AUTO_INJECTED_EXTENSION_COMPONENT_TAGS = [
|
|
16
|
+
'CommonDrawer',
|
|
17
|
+
'CommonModal',
|
|
18
|
+
'EmptyState',
|
|
19
|
+
'FormEditor',
|
|
20
|
+
'FormEditorLazy',
|
|
21
|
+
'NuxtLink',
|
|
22
|
+
'PermissionGate',
|
|
23
|
+
'UBadge',
|
|
24
|
+
'UButton',
|
|
25
|
+
'UCheckbox',
|
|
26
|
+
'UDropdownMenu',
|
|
27
|
+
'UForm',
|
|
28
|
+
'UFormField',
|
|
29
|
+
'UIcon',
|
|
30
|
+
'UInput',
|
|
31
|
+
'UModal',
|
|
32
|
+
'USelect',
|
|
33
|
+
'USelectMenu',
|
|
34
|
+
'USkeleton',
|
|
35
|
+
'USwitch',
|
|
36
|
+
'UTabs',
|
|
37
|
+
'UTextarea',
|
|
38
|
+
'UTooltip',
|
|
39
|
+
'Widget',
|
|
40
|
+
];
|
|
41
|
+
const AUTO_INJECTED_EXTENSION_COMPONENT_BY_LOWERCASE = new Map(
|
|
42
|
+
AUTO_INJECTED_EXTENSION_COMPONENT_TAGS.map((tag) => [tag.toLowerCase(), tag]),
|
|
43
|
+
);
|
|
44
|
+
|
|
15
45
|
function unwrapData(result) {
|
|
16
46
|
return Array.isArray(result?.data) ? result.data : [];
|
|
17
47
|
}
|
|
@@ -360,6 +390,7 @@ function getExtensionThemeContract() {
|
|
|
360
390
|
],
|
|
361
391
|
components: [
|
|
362
392
|
'Use Nuxt UI/eApp components for normal controls: UButton, UInput, UTextarea, USelectMenu/USelect, USwitch, UCheckbox, UTabs, UBadge, UModal, and CommonDrawer when available.',
|
|
393
|
+
'Use auto-injected components directly in the template with PascalCase names. Do not call resolveComponent() to manually resolve Nuxt UI/eApp components inside extension SFCs; it can compile but render unresolved lowercase DOM tags such as <ubutton>.',
|
|
363
394
|
'Buttons should have stable geometry: hover may change color, border, or shadow but must not move the button or resize its content. Disabled buttons keep disabled cursor/visual state.',
|
|
364
395
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
365
396
|
'Dynamic extensions resolve UModal to the app CommonModal. Do not pass ui.content: "eapp-surface-card" or "surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller ui.content should only append z-index, width, or max-width classes.',
|
|
@@ -541,7 +572,66 @@ async function validateDynamicScript(apiUrl, sourceCode, scriptLanguage = 'javas
|
|
|
541
572
|
};
|
|
542
573
|
}
|
|
543
574
|
|
|
575
|
+
function readTemplateBlocks(code) {
|
|
576
|
+
const blocks = [];
|
|
577
|
+
const lower = String(code || '').toLowerCase();
|
|
578
|
+
let index = 0;
|
|
579
|
+
while (index < lower.length) {
|
|
580
|
+
const openStart = lower.indexOf('<template', index);
|
|
581
|
+
if (openStart === -1) break;
|
|
582
|
+
const boundary = lower[openStart + '<template'.length];
|
|
583
|
+
if (boundary && !/\s|>/.test(boundary)) {
|
|
584
|
+
index = openStart + 1;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const openEnd = lower.indexOf('>', openStart + '<template'.length);
|
|
588
|
+
if (openEnd === -1) break;
|
|
589
|
+
const closeStart = lower.indexOf('</template', openEnd + 1);
|
|
590
|
+
if (closeStart === -1) break;
|
|
591
|
+
blocks.push(String(code).slice(openEnd + 1, closeStart));
|
|
592
|
+
index = closeStart + '</template'.length;
|
|
593
|
+
}
|
|
594
|
+
return blocks;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function readTemplateTagName(template, start) {
|
|
598
|
+
const next = template[start + 1];
|
|
599
|
+
if (!next || next === '!' || next === '?') return null;
|
|
600
|
+
let index = start + (next === '/' ? 2 : 1);
|
|
601
|
+
while (/\s/.test(template[index] || '')) index += 1;
|
|
602
|
+
const nameStart = index;
|
|
603
|
+
while (/[\w.-]/.test(template[index] || '')) index += 1;
|
|
604
|
+
return index > nameStart ? template.slice(nameStart, index) : null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function validateExtensionCodeLocally(code) {
|
|
608
|
+
if (/\bresolveComponent\s*\(/.test(String(code || ''))) {
|
|
609
|
+
throw new Error('Invalid extension component resolution: do not call resolveComponent() in Enfyra extensions. Use auto-injected components such as <UButton> directly in the template so the app/compiler resolves them correctly.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const violations = [];
|
|
613
|
+
for (const template of readTemplateBlocks(code)) {
|
|
614
|
+
let index = 0;
|
|
615
|
+
while (index < template.length) {
|
|
616
|
+
const tagStart = template.indexOf('<', index);
|
|
617
|
+
if (tagStart === -1) break;
|
|
618
|
+
const tagName = readTemplateTagName(template, tagStart);
|
|
619
|
+
if (tagName && tagName === tagName.toLowerCase() && !tagName.includes('-')) {
|
|
620
|
+
const expected = AUTO_INJECTED_EXTENSION_COMPONENT_BY_LOWERCASE.get(tagName);
|
|
621
|
+
if (expected) violations.push({ tag: tagName, expected });
|
|
622
|
+
}
|
|
623
|
+
index = tagStart + 1;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (violations.length) {
|
|
627
|
+
const first = violations[0];
|
|
628
|
+
throw new Error(`Invalid extension component casing: use <${first.expected}> instead of <${first.tag}>. Enfyra/Nuxt UI auto-injected components must keep PascalCase in extension templates; lowercase tags render as unresolved DOM elements.`);
|
|
629
|
+
}
|
|
630
|
+
return { componentCasing: 'passed' };
|
|
631
|
+
}
|
|
632
|
+
|
|
544
633
|
async function validateExtensionCode(apiUrl, code, name) {
|
|
634
|
+
const localChecks = validateExtensionCodeLocally(code);
|
|
545
635
|
const result = await fetchAPI(apiUrl, '/enfyra_extension/preview', {
|
|
546
636
|
method: 'POST',
|
|
547
637
|
body: JSON.stringify({ code, name }),
|
|
@@ -551,6 +641,7 @@ async function validateExtensionCode(apiUrl, code, name) {
|
|
|
551
641
|
}
|
|
552
642
|
return {
|
|
553
643
|
valid: true,
|
|
644
|
+
localChecks,
|
|
554
645
|
extensionId: result?.extensionId || name || null,
|
|
555
646
|
compiledLength: typeof result?.compiledCode === 'string' ? result.compiledCode.length : undefined,
|
|
556
647
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const GLOBAL_RULES_ACK_KEY = 'EFYRA::GLOBAL-RULES::
|
|
1
|
+
export const GLOBAL_RULES_ACK_KEY = 'EFYRA::GLOBAL-RULES::UNIQUE-FIELDS-ARE-INDEXED::2bW-20260701B';
|
|
2
2
|
export const DYNAMIC_CODE_KNOWLEDGE_ACK_KEY = 'EFYRA::SECURE-REPO-CONTRACT::R9x-kelp-42Q::NO-RAW-TRUSTED';
|
|
3
3
|
export const EXTENSION_KNOWLEDGE_ACK_KEY = 'EFYRA::EXTENSION-THEME-CONTRACT::VIOLET-IS-NOT-A-PLAN::7mQ';
|
|
4
4
|
|
|
5
|
-
const REQUIRED_KNOWLEDGE_VERSION = '2026-07-01.global-rules-
|
|
5
|
+
const REQUIRED_KNOWLEDGE_VERSION = '2026-07-01.global-rules-v2';
|
|
6
6
|
|
|
7
7
|
export function globalRulesAckParam(z) {
|
|
8
8
|
return z.string().describe('Required global-rules acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the global Enfyra MCP rules, then pass globalRulesAckKey exactly.');
|
|
@@ -84,6 +84,15 @@ export function buildRequiredKnowledgePayload() {
|
|
|
84
84
|
'Never fabricate ids, field names, relation names, paths, package names, or permission scopes.',
|
|
85
85
|
],
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
id: 'schema-constraints',
|
|
89
|
+
rules: [
|
|
90
|
+
'For table schema, a field that appears in any uniques group must not appear in indexes.',
|
|
91
|
+
'A unique constraint already creates the indexed unique lookup for its fields.',
|
|
92
|
+
'Use uniques for data integrity and indexes only for non-unique query-performance fields that are not already unique.',
|
|
93
|
+
'Before create_table or update_table with indexes/uniques, inspect the current table and remove indexes that reference unique fields.',
|
|
94
|
+
],
|
|
95
|
+
},
|
|
87
96
|
{
|
|
88
97
|
id: 'security-first',
|
|
89
98
|
rules: [
|
|
@@ -166,6 +175,7 @@ export function buildRequiredKnowledgePayload() {
|
|
|
166
175
|
id: 'extension-runtime-contract',
|
|
167
176
|
rules: [
|
|
168
177
|
'Save extensions as enfyra_extension Vue SFC records; no static import statements in extension code.',
|
|
178
|
+
'Do not call resolveComponent() in extension SFCs. Use auto-injected components such as <UButton>, <UBadge>, <PermissionGate>, and <Widget> directly in the template so the app/compiler resolves them correctly.',
|
|
169
179
|
'Load app packages with getPackages(["package-name"]) inside extension runtime code.',
|
|
170
180
|
'Prefer FormEditor/FormEditorLazy for direct table-backed forms when the form maps to metadata fields.',
|
|
171
181
|
'For long admin setup workflows, open CommonDrawer immediately and show loading/error/content inside it.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -125,6 +125,37 @@ function normalizeConstraintGroups(name, groups) {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function parseConstraintGroupsParam(name, value) {
|
|
129
|
+
return normalizeConstraintGroups(name, parseJsonArrayParam(name, value));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeConstraintGroupsValue(name, value) {
|
|
133
|
+
if (value == null) return [];
|
|
134
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
135
|
+
if (!Array.isArray(parsed)) {
|
|
136
|
+
throw new Error(`${name} must be a JSON array.`);
|
|
137
|
+
}
|
|
138
|
+
return normalizeConstraintGroups(name, parsed);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function assertIndexesDoNotReferenceUniqueFields(indexes, uniques) {
|
|
142
|
+
const uniqueFields = new Set(uniques.flat());
|
|
143
|
+
const conflicts = indexes
|
|
144
|
+
.map((group) => ({
|
|
145
|
+
index: group,
|
|
146
|
+
uniqueFields: group.filter((field) => uniqueFields.has(field)),
|
|
147
|
+
}))
|
|
148
|
+
.filter((conflict) => conflict.uniqueFields.length > 0);
|
|
149
|
+
if (conflicts.length > 0) {
|
|
150
|
+
const groups = conflicts
|
|
151
|
+
.map((conflict) => `${JSON.stringify(conflict.index)} uses unique field(s) ${JSON.stringify(conflict.uniqueFields)}`)
|
|
152
|
+
.join('; ');
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Invalid schema constraints: indexes must not include fields that are already unique. Conflict(s): ${groups}. Unique constraints already create indexed lookups; remove unique fields from indexes and keep those fields only in uniques.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
export function normalizeRelationForTablePatch(relation) {
|
|
129
160
|
for (const key of FORBIDDEN_RELATION_KEYS) {
|
|
130
161
|
if (Object.prototype.hasOwnProperty.call(relation, key)) {
|
|
@@ -566,6 +597,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
566
597
|
'**Not** for adding a custom API path or handler only — for that use **`create_route`** without `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
567
598
|
'PREFERRED: pass `columns` and `relations` params as JSON arrays to create a table WITH columns and relations in one call (cascade). Only use create_column/create_relation separately when adding to an existing table later.',
|
|
568
599
|
'Indexes and uniques are first-class table metadata. Use `indexes` for query performance and `uniques` for data integrity. Each entry is a logical field group such as [["member","isRead","conversation"]] or [{"value":["message","member"]}]. Relation property names are allowed; Enfyra resolves them to physical FK columns.',
|
|
600
|
+
'A field that appears in any `uniques` group must not appear in `indexes`; unique constraints already create indexed unique lookups.',
|
|
569
601
|
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id, {id}, or an exact table name that MCP resolves to an id before mutation. Omit inversePropertyName unless a concrete parent-to-child query or UI surface needs it.',
|
|
570
602
|
'Do NOT provide physical FK/junction columns. Never include fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn. Enfyra derives and hides those physical columns from relation propertyName/table metadata.',
|
|
571
603
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
@@ -583,7 +615,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
583
615
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to enfyra_table create.'),
|
|
584
616
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, defaultValue?, description?, options? }. Set isEncrypted=true for values encrypted at rest; set isUpdatable=false separately only when the field should be immutable. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"api_key","type":"varchar","isEncrypted":true,"isPublished":false}]'),
|
|
585
617
|
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Omit inversePropertyName unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal needs the reverse field. Example only when parent posts are queried: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
586
|
-
indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["
|
|
618
|
+
indexes: z.string().optional().describe('JSON array of logical non-unique index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Do not include any field that appears in uniques; unique constraints already create indexed unique lookups. Example: [["status","createdAt"]]'),
|
|
587
619
|
uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
|
|
588
620
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
589
621
|
},
|
|
@@ -596,8 +628,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
596
628
|
const userRelations = metadata
|
|
597
629
|
? resolveRelationTargetsFromMetadata(metadata, parsedRelations)
|
|
598
630
|
: parsedRelations;
|
|
599
|
-
const indexes =
|
|
600
|
-
const uniques =
|
|
631
|
+
const indexes = parseConstraintGroupsParam('indexes', indexesJson);
|
|
632
|
+
const uniques = parseConstraintGroupsParam('uniques', uniquesJson);
|
|
633
|
+
assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
|
|
601
634
|
const body = { name, description, columns: [idColumn, ...userColumns], relations: userRelations };
|
|
602
635
|
if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
|
|
603
636
|
if (indexesJson !== undefined) body.indexes = indexes;
|
|
@@ -654,6 +687,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
654
687
|
'Update table properties: name (rename), alias, description, isSingleRecord, graphqlEnabled, indexes, and uniques.',
|
|
655
688
|
'Does NOT modify columns or relations — use create_column, update_column, delete_column, create_relation for those.',
|
|
656
689
|
'When passing `indexes` or `uniques`, pass the complete desired array of logical field groups; omitted fields are preserved. Relation property names are allowed and are resolved by Enfyra. Example indexes: [["member","isRead","conversation"],["conversation","member","isRead"]].',
|
|
690
|
+
'A field that appears in any `uniques` group must not appear in `indexes`; unique constraints already create indexed unique lookups.',
|
|
657
691
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
658
692
|
].join(' '),
|
|
659
693
|
{
|
|
@@ -663,7 +697,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
663
697
|
description: z.string().optional().describe('New description.'),
|
|
664
698
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record table (e.g., settings/config).'),
|
|
665
699
|
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL table data still requires Bearer auth; anonymous root or schema probes may return 200.'),
|
|
666
|
-
indexes: z.string().optional().describe('Complete JSON array of logical index field groups to store on enfyra_table.indexes. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current indexes; pass [] to clear.'),
|
|
700
|
+
indexes: z.string().optional().describe('Complete JSON array of logical non-unique index field groups to store on enfyra_table.indexes. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current indexes; pass [] to clear. Do not include any field that appears in uniques; unique constraints already create indexed unique lookups.'),
|
|
667
701
|
uniques: z.string().optional().describe('Complete JSON array of logical unique field groups to store on enfyra_table.uniques. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current uniques; pass [] to clear.'),
|
|
668
702
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
669
703
|
},
|
|
@@ -675,8 +709,19 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
675
709
|
if (description !== undefined) body.description = description;
|
|
676
710
|
if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
|
|
677
711
|
if (graphqlEnabled !== undefined) body.graphqlEnabled = graphqlEnabled;
|
|
678
|
-
if (indexesJson !== undefined) body.indexes =
|
|
679
|
-
if (uniquesJson !== undefined) body.uniques =
|
|
712
|
+
if (indexesJson !== undefined) body.indexes = parseConstraintGroupsParam('indexes', indexesJson);
|
|
713
|
+
if (uniquesJson !== undefined) body.uniques = parseConstraintGroupsParam('uniques', uniquesJson);
|
|
714
|
+
|
|
715
|
+
if (indexesJson !== undefined || uniquesJson !== undefined) {
|
|
716
|
+
let indexes = body.indexes;
|
|
717
|
+
let uniques = body.uniques;
|
|
718
|
+
if (indexes === undefined || uniques === undefined) {
|
|
719
|
+
const existing = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
720
|
+
if (indexes === undefined) indexes = normalizeConstraintGroupsValue('indexes', existing.indexes);
|
|
721
|
+
if (uniques === undefined) uniques = normalizeConstraintGroupsValue('uniques', existing.uniques);
|
|
722
|
+
}
|
|
723
|
+
assertIndexesDoNotReferenceUniqueFields(indexes ?? [], uniques ?? []);
|
|
724
|
+
}
|
|
680
725
|
|
|
681
726
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, body);
|
|
682
727
|
return jsonContent({
|