@enfyra/mcp-server 0.0.25 → 0.0.27
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 +57 -12
- package/package.json +1 -1
- package/src/index.mjs +22 -0
- package/src/lib/config-local.mjs +127 -27
- package/src/lib/mcp-instructions.js +23 -7
- package/src/mcp-server-entry.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Enfyra MCP Server
|
|
2
2
|
|
|
3
|
-
MCP server for managing Enfyra instances from **Claude Code**, **Cursor**, and other MCP-compatible clients. All operations go through Enfyra's REST API.
|
|
3
|
+
MCP server for managing Enfyra instances from **Codex**, **Claude Code**, **Cursor**, and other MCP-compatible clients. All operations go through Enfyra's REST API.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
**LLM rules (REST, GraphQL, auth, URL, mutation `create_{tableName}`, etc.):** not in this README — see **`src/lib/mcp-instructions.js`** (content sent via MCP `instructions`) and tool descriptions in **`src/index.mjs`**. This README only covers **MCP installation and configuration** for users/devs.
|
|
@@ -11,17 +11,18 @@ MCP server for managing Enfyra instances from **Claude Code**, **Cursor**, and o
|
|
|
11
11
|
|
|
12
12
|
## Quick local setup (`config` command)
|
|
13
13
|
|
|
14
|
-
From your **Enfyra project root
|
|
14
|
+
From your **Enfyra project root**:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npx @enfyra/mcp-server config
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- **Interactive (default in a terminal):** first asks **where** to write config — `[1]` Claude Code
|
|
20
|
+
- **Interactive (default in a terminal):** first asks **where** to write config — `[1]` Claude Code, `[2]` Cursor, `[3]` Codex, `[4]` all (default) — unless you already passed target flags. Then prompts for `ENFYRA_API_URL`, `ENFYRA_EMAIL`, and `ENFYRA_PASSWORD` when missing. Press **Enter** to accept bracketed defaults from env or existing `enfyra` config. Password **Enter** keeps the current saved password when updating.
|
|
21
21
|
- **Re-run anytime** to update the same files; other entries under `mcpServers` are preserved.
|
|
22
22
|
- **Non-interactive** (CI / scripts): `npx @enfyra/mcp-server config --yes` plus optional `-a` / `-e` / `-p` and/or env vars.
|
|
23
|
-
- **One
|
|
24
|
-
- **
|
|
23
|
+
- **One host only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json`. `--cursor` / `--cursor-only` → `./.cursor/mcp.json`. `--codex` / `--codex-only` → `~/.codex/config.toml`. Pass multiple target flags to write each selected host.
|
|
24
|
+
- **Reconfigure:** `npx @enfyra/mcp-server config --reconfig` prompts for the target host again, uses existing values as defaults, and replaces the old `enfyra` entry for that host.
|
|
25
|
+
- **Help:** `npx @enfyra/mcp-server -h` or `npx @enfyra/mcp-server config --help`
|
|
25
26
|
|
|
26
27
|
Equivalent in this repo: `yarn mcp:config` (Yarn v1 reserves `yarn config` for registry settings). Same as `node src/index.mjs config` / `npm run mcp:config`.
|
|
27
28
|
|
|
@@ -31,16 +32,51 @@ Equivalent in this repo: `yarn mcp:config` (Yarn v1 reserves `yarn config` for r
|
|
|
31
32
|
|
|
32
33
|
Use this table to see **where** each host stores config. The **`mcpServers.enfyra` JSON block** at the bottom of each section is identical; only the **file paths** and **CLI** differ.
|
|
33
34
|
|
|
34
|
-
| | **Claude Code** | **Cursor** |
|
|
35
|
-
|
|
36
|
-
| **Global (all projects)** | `~/.claude.json` — scopes **user** or **local** | `~/.cursor/mcp.json` |
|
|
37
|
-
| **Project (repo)** | **`.mcp.json`** at repository root (`--scope project`) | **`.cursor/mcp.json`** in the project |
|
|
38
|
-
| **Typical install** | `claude mcp add --transport stdio …` | Edit `mcp.json` or **Settings → MCP** |
|
|
39
|
-
| **Precedence / merge** | local → project `.mcp.json` → user | Project `.cursor/mcp.json` overrides global `~/.cursor/mcp.json` |
|
|
40
|
-
| **Gotcha** | Do not put MCP server definitions in `.claude/settings.json` | Root **`.mcp.json`** is for Claude Code project scope, not Cursor — use **`.cursor/mcp.json`** for Cursor |
|
|
35
|
+
| | **Codex** | **Claude Code** | **Cursor** |
|
|
36
|
+
|---|-----------|-----------------|------------|
|
|
37
|
+
| **Global (all projects)** | `~/.codex/config.toml` | `~/.claude.json` — scopes **user** or **local** | `~/.cursor/mcp.json` |
|
|
38
|
+
| **Project (repo)** | Use global config | **`.mcp.json`** at repository root (`--scope project`) | **`.cursor/mcp.json`** in the project |
|
|
39
|
+
| **Typical install** | `npx @enfyra/mcp-server config --codex` | `claude mcp add --transport stdio …` | Edit `mcp.json` or **Settings → MCP** |
|
|
40
|
+
| **Precedence / merge** | `config.toml` section is replaced for `enfyra`; other servers are preserved | local → project `.mcp.json` → user | Project `.cursor/mcp.json` overrides global `~/.cursor/mcp.json` |
|
|
41
|
+
| **Gotcha** | Restart Codex or start a new session after editing config | Do not put MCP server definitions in `.claude/settings.json` | Root **`.mcp.json`** is for Claude Code project scope, not Cursor — use **`.cursor/mcp.json`** for Cursor |
|
|
41
42
|
|
|
42
43
|
Expand **one** block below for step-by-step setup.
|
|
43
44
|
|
|
45
|
+
<details open>
|
|
46
|
+
<summary><strong>Codex</strong> — setup</summary>
|
|
47
|
+
|
|
48
|
+
The config command can write/update `~/.codex/config.toml` directly:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @enfyra/mcp-server config --codex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Non-interactive:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @enfyra/mcp-server config --codex --yes \
|
|
58
|
+
-a http://localhost:3000/api \
|
|
59
|
+
-e your-email@example.com \
|
|
60
|
+
-p your-password
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The generated TOML section is:
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
[mcp_servers.enfyra]
|
|
67
|
+
command = "npx"
|
|
68
|
+
args = ["-y", "@enfyra/mcp-server"]
|
|
69
|
+
|
|
70
|
+
[mcp_servers.enfyra.env]
|
|
71
|
+
ENFYRA_API_URL = "http://localhost:3000/api"
|
|
72
|
+
ENFYRA_EMAIL = "your-email@example.com"
|
|
73
|
+
ENFYRA_PASSWORD = "your-password"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The config writer replaces only `[mcp_servers.enfyra]` and `[mcp_servers.enfyra.env]`; other Codex config and other MCP servers are preserved. Restart Codex or start a new session after updating `~/.codex/config.toml`.
|
|
77
|
+
|
|
78
|
+
</details>
|
|
79
|
+
|
|
44
80
|
<details open>
|
|
45
81
|
<summary><strong>Claude Code</strong> — setup</summary>
|
|
46
82
|
|
|
@@ -161,6 +197,15 @@ Use this block in any host-specific `mcp.json` / `mcpServers` merge (adjust env
|
|
|
161
197
|
|
|
162
198
|
Pick the base URL that matches how **your** HTTP client reaches the same server as the Enfyra REST API.
|
|
163
199
|
|
|
200
|
+
### SSR app auth pattern
|
|
201
|
+
|
|
202
|
+
When an LLM builds a Nuxt, Next, or other SSR frontend for Enfyra, use a same-origin proxy:
|
|
203
|
+
|
|
204
|
+
- Browser code calls `{{ appOrigin }}/api/**`, never the raw Enfyra backend URL.
|
|
205
|
+
- Cookie-managed password login is `POST {{ appOrigin }}/api/login`, not `/api/auth/login`. The SSR route calls backend `/auth/login` and stores Enfyra `accessToken`, `refreshToken`, and `expTime` as httpOnly cookies.
|
|
206
|
+
- Cookie-managed OAuth should enable Enfyra OAuth cookie handling (`autoSetCookies` / set-cookies mode). Start OAuth at `{{ appOrigin }}/api/auth/:provider?redirect=...`; Enfyra redirects to `{{ appOrigin }}/api/auth/set-cookies`, then the SSR route sets cookies and redirects to the requested page.
|
|
207
|
+
- Use token-query OAuth callback pages only for non-SSR/manual-token apps.
|
|
208
|
+
|
|
164
209
|
---
|
|
165
210
|
|
|
166
211
|
## Tools (summary)
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -6,6 +6,28 @@
|
|
|
6
6
|
import { config as loadEnv } from 'dotenv';
|
|
7
7
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
|
+
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
10
|
+
console.log(`Enfyra MCP Server
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
npx @enfyra/mcp-server Start the MCP stdio server
|
|
14
|
+
npx @enfyra/mcp-server config [flags] Write local MCP host config
|
|
15
|
+
|
|
16
|
+
Common config flags:
|
|
17
|
+
--codex Write ~/.codex/config.toml
|
|
18
|
+
--claude-code Write ./.mcp.json
|
|
19
|
+
--cursor Write ./.cursor/mcp.json
|
|
20
|
+
--reconfig Prompt for host and credentials again, replacing the enfyra entry
|
|
21
|
+
--yes Non-interactive
|
|
22
|
+
-a, --api-url ENFYRA_API_URL
|
|
23
|
+
-e, --email ENFYRA_EMAIL
|
|
24
|
+
-p, --password ENFYRA_PASSWORD
|
|
25
|
+
-h, --help Show config help
|
|
26
|
+
|
|
27
|
+
Run \`npx @enfyra/mcp-server config --help\` for full config details.
|
|
28
|
+
`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
9
31
|
if (args[0] === 'config') {
|
|
10
32
|
loadEnv({ quiet: true });
|
|
11
33
|
const { runLocalConfig } = await import('./lib/config-local.mjs');
|
package/src/lib/config-local.mjs
CHANGED
|
@@ -2,37 +2,43 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { stdin as input, stdout as output, cwd } from 'node:process';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
|
|
6
7
|
const SERVER_KEY = 'enfyra';
|
|
7
8
|
|
|
8
9
|
function printHelp() {
|
|
9
|
-
console.log(`enfyra-mcp — write
|
|
10
|
+
console.log(`enfyra-mcp — write MCP config (Codex + Claude Code + Cursor)
|
|
10
11
|
|
|
11
12
|
Usage:
|
|
12
13
|
npx @enfyra/mcp-server config [options]
|
|
13
14
|
|
|
14
|
-
Writes
|
|
15
|
+
Writes project config under the current working directory and Codex config under your home directory:
|
|
15
16
|
• ./.mcp.json — Claude Code project scope
|
|
16
17
|
• ./.cursor/mcp.json — Cursor project scope
|
|
18
|
+
• ~/.codex/config.toml — Codex user scope
|
|
17
19
|
|
|
18
20
|
Options:
|
|
19
21
|
--api-url, -a <url> ENFYRA_API_URL
|
|
20
22
|
--email, -e <email> ENFYRA_EMAIL
|
|
21
23
|
--password, -p <secret> ENFYRA_PASSWORD
|
|
24
|
+
--reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
|
|
22
25
|
--yes Non-interactive: no prompts (CI / scripts); use CLI, env, existing file, then defaults
|
|
23
|
-
Target — non-interactive default is
|
|
26
|
+
Target — non-interactive default is all; with TTY and no target flags, you are prompted [1]/[2]/[3]/[4]:
|
|
24
27
|
--claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
|
|
25
28
|
--cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
|
|
26
|
-
|
|
29
|
+
--codex, --codex-only Only ~/.codex/config.toml (Codex)
|
|
30
|
+
Passing multiple target flags writes each selected target.
|
|
27
31
|
-h, --help Show this help
|
|
28
32
|
|
|
29
33
|
Interactive mode: asks Claude Code vs Cursor vs both if you did not pass target flags; then asks for URL / email / password
|
|
30
|
-
when missing. Existing ./.mcp.json
|
|
34
|
+
when missing. Existing ./.mcp.json, ./.cursor/mcp.json, and ~/.codex/config.toml are used as defaults. Re-run to update.
|
|
31
35
|
|
|
32
36
|
Examples:
|
|
33
37
|
npx @enfyra/mcp-server config
|
|
34
38
|
npx @enfyra/mcp-server config --claude-code
|
|
35
39
|
npx @enfyra/mcp-server config --cursor --yes
|
|
40
|
+
npx @enfyra/mcp-server config --codex --yes
|
|
41
|
+
npx @enfyra/mcp-server config --reconfig
|
|
36
42
|
npx @enfyra/mcp-server config -a http://localhost:3000/api -e admin@x.com -p 'secret'
|
|
37
43
|
npx @enfyra/mcp-server config --yes
|
|
38
44
|
ENFYRA_PASSWORD=secret npx @enfyra/mcp-server config --yes -e admin@x.com
|
|
@@ -46,11 +52,14 @@ function parseArgs(argv) {
|
|
|
46
52
|
password: undefined,
|
|
47
53
|
claude: true,
|
|
48
54
|
cursor: true,
|
|
55
|
+
codex: true,
|
|
49
56
|
help: false,
|
|
50
57
|
yes: false,
|
|
58
|
+
reconfig: false,
|
|
51
59
|
};
|
|
52
60
|
let pickClaude = false;
|
|
53
61
|
let pickCursor = false;
|
|
62
|
+
let pickCodex = false;
|
|
54
63
|
for (let i = 0; i < argv.length; i += 1) {
|
|
55
64
|
const a = argv[i];
|
|
56
65
|
const next = () => {
|
|
@@ -60,26 +69,22 @@ function parseArgs(argv) {
|
|
|
60
69
|
return v;
|
|
61
70
|
};
|
|
62
71
|
if (a === '--help' || a === '-h') out.help = true;
|
|
72
|
+
else if (a === 'help') out.help = true;
|
|
63
73
|
else if (a === '--yes') out.yes = true;
|
|
74
|
+
else if (a === '--reconfig') out.reconfig = true;
|
|
64
75
|
else if (a === '--api-url' || a === '-a') out.apiUrl = next();
|
|
65
76
|
else if (a === '--email' || a === '-e') out.email = next();
|
|
66
77
|
else if (a === '--password' || a === '-p') out.password = next();
|
|
67
78
|
else if (a === '--claude-only' || a === '--claude-code' || a === '--claude') pickClaude = true;
|
|
68
79
|
else if (a === '--cursor-only' || a === '--cursor') pickCursor = true;
|
|
80
|
+
else if (a === '--codex-only' || a === '--codex') pickCodex = true;
|
|
69
81
|
else throw new Error(`Unknown argument: ${a}`);
|
|
70
82
|
}
|
|
71
|
-
out.targetExplicit = pickClaude || pickCursor;
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} else if (pickClaude) {
|
|
77
|
-
out.claude = true;
|
|
78
|
-
out.cursor = false;
|
|
79
|
-
} else {
|
|
80
|
-
out.claude = false;
|
|
81
|
-
out.cursor = true;
|
|
82
|
-
}
|
|
83
|
+
out.targetExplicit = pickClaude || pickCursor || pickCodex;
|
|
84
|
+
if (out.targetExplicit) {
|
|
85
|
+
out.claude = pickClaude;
|
|
86
|
+
out.cursor = pickCursor;
|
|
87
|
+
out.codex = pickCodex;
|
|
83
88
|
}
|
|
84
89
|
return out;
|
|
85
90
|
}
|
|
@@ -115,7 +120,86 @@ async function mergeMcpFile(absPath, serverEntry) {
|
|
|
115
120
|
await writeFile(absPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
function tomlString(value) {
|
|
124
|
+
return JSON.stringify(String(value ?? ''));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildCodexTomlBlock(apiUrl, email, password) {
|
|
128
|
+
return [
|
|
129
|
+
'[mcp_servers.enfyra]',
|
|
130
|
+
'command = "npx"',
|
|
131
|
+
'args = ["-y", "@enfyra/mcp-server"]',
|
|
132
|
+
'',
|
|
133
|
+
'[mcp_servers.enfyra.env]',
|
|
134
|
+
`ENFYRA_API_URL = ${tomlString(apiUrl)}`,
|
|
135
|
+
`ENFYRA_EMAIL = ${tomlString(email)}`,
|
|
136
|
+
`ENFYRA_PASSWORD = ${tomlString(password)}`,
|
|
137
|
+
'',
|
|
138
|
+
].join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function mergeCodexConfig(absPath, apiUrl, email, password) {
|
|
142
|
+
let raw = '';
|
|
143
|
+
try {
|
|
144
|
+
raw = await readFile(absPath, 'utf8');
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (e.code !== 'ENOENT') throw e;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const kept = [];
|
|
150
|
+
let skip = false;
|
|
151
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
152
|
+
const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
153
|
+
if (header) {
|
|
154
|
+
const section = header[1].trim();
|
|
155
|
+
skip = section === 'mcp_servers.enfyra' || section.startsWith('mcp_servers.enfyra.');
|
|
156
|
+
}
|
|
157
|
+
if (!skip) kept.push(line);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const next = `${kept.join('\n').trimEnd()}\n\n${buildCodexTomlBlock(apiUrl, email, password)}`;
|
|
161
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
162
|
+
await writeFile(absPath, next, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseTomlString(value) {
|
|
166
|
+
const trimmed = String(value || '').trim();
|
|
167
|
+
if (!trimmed) return '';
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(trimmed);
|
|
170
|
+
} catch {
|
|
171
|
+
return trimmed.replace(/^['"]|['"]$/g, '');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function readCodexEnfyraEnv(absPath) {
|
|
176
|
+
try {
|
|
177
|
+
const raw = await readFile(absPath, 'utf8');
|
|
178
|
+
const values = { apiUrl: '', email: '', password: '' };
|
|
179
|
+
let inEnv = false;
|
|
180
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
181
|
+
const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
182
|
+
if (header) {
|
|
183
|
+
inEnv = header[1].trim() === 'mcp_servers.enfyra.env';
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!inEnv) continue;
|
|
187
|
+
const pair = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
|
|
188
|
+
if (!pair) continue;
|
|
189
|
+
const key = pair[1];
|
|
190
|
+
const value = parseTomlString(pair[2]);
|
|
191
|
+
if (key === 'ENFYRA_API_URL') values.apiUrl = value;
|
|
192
|
+
if (key === 'ENFYRA_EMAIL') values.email = value;
|
|
193
|
+
if (key === 'ENFYRA_PASSWORD') values.password = value;
|
|
194
|
+
}
|
|
195
|
+
if (values.apiUrl || values.email || values.password) return values;
|
|
196
|
+
} catch {
|
|
197
|
+
/* */
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex) {
|
|
119
203
|
const paths = [];
|
|
120
204
|
if (readClaude) paths.push(join(root, '.mcp.json'));
|
|
121
205
|
if (readCursor) paths.push(join(root, '.cursor', 'mcp.json'));
|
|
@@ -139,6 +223,10 @@ async function loadExistingEnfyraEnv(root, readClaude, readCursor) {
|
|
|
139
223
|
/* */
|
|
140
224
|
}
|
|
141
225
|
}
|
|
226
|
+
if (readCodex) {
|
|
227
|
+
const codex = await readCodexEnfyraEnv(join(homedir(), '.codex', 'config.toml'));
|
|
228
|
+
if (codex) return codex;
|
|
229
|
+
}
|
|
142
230
|
return { apiUrl: '', email: '', password: '' };
|
|
143
231
|
}
|
|
144
232
|
|
|
@@ -148,20 +236,24 @@ async function promptTargetChoice() {
|
|
|
148
236
|
'Where should Enfyra MCP config be written?\n'
|
|
149
237
|
+ ' [1] Claude Code — ./.mcp.json\n'
|
|
150
238
|
+ ' [2] Cursor — ./.cursor/mcp.json\n'
|
|
151
|
-
+ ' [3]
|
|
152
|
-
+ '
|
|
239
|
+
+ ' [3] Codex — ~/.codex/config.toml\n'
|
|
240
|
+
+ ' [4] All [default]\n'
|
|
241
|
+
+ 'Choice [4]: ',
|
|
153
242
|
)).trim().toLowerCase();
|
|
154
243
|
await rl.close();
|
|
155
|
-
if (line === '' || line === '
|
|
156
|
-
return { claude: true, cursor: true };
|
|
244
|
+
if (line === '' || line === '4' || line === 'all' || line === 'a') {
|
|
245
|
+
return { claude: true, cursor: true, codex: true };
|
|
157
246
|
}
|
|
158
247
|
if (line === '1' || line === 'c' || line === 'claude') {
|
|
159
|
-
return { claude: true, cursor: false };
|
|
248
|
+
return { claude: true, cursor: false, codex: false };
|
|
160
249
|
}
|
|
161
250
|
if (line === '2' || line === 'u' || line === 'cursor') {
|
|
162
|
-
return { claude: false, cursor: true };
|
|
251
|
+
return { claude: false, cursor: true, codex: false };
|
|
163
252
|
}
|
|
164
|
-
|
|
253
|
+
if (line === '3' || line === 'x' || line === 'codex') {
|
|
254
|
+
return { claude: false, cursor: false, codex: true };
|
|
255
|
+
}
|
|
256
|
+
return { claude: true, cursor: true, codex: true };
|
|
165
257
|
}
|
|
166
258
|
|
|
167
259
|
async function promptConfig(opts, existing) {
|
|
@@ -237,13 +329,15 @@ export async function runLocalConfig(argv) {
|
|
|
237
329
|
|
|
238
330
|
let writeClaude = opts.claude;
|
|
239
331
|
let writeCursor = opts.cursor;
|
|
240
|
-
|
|
332
|
+
let writeCodex = opts.codex;
|
|
333
|
+
if (usePrompt && (!opts.targetExplicit || opts.reconfig)) {
|
|
241
334
|
const t = await promptTargetChoice();
|
|
242
335
|
writeClaude = t.claude;
|
|
243
336
|
writeCursor = t.cursor;
|
|
337
|
+
writeCodex = t.codex;
|
|
244
338
|
}
|
|
245
339
|
|
|
246
|
-
const existing = await loadExistingEnfyraEnv(root,
|
|
340
|
+
const existing = await loadExistingEnfyraEnv(root, writeClaude, writeCursor, writeCodex);
|
|
247
341
|
|
|
248
342
|
let apiUrl;
|
|
249
343
|
let email;
|
|
@@ -273,10 +367,16 @@ export async function runLocalConfig(argv) {
|
|
|
273
367
|
await mergeMcpFile(p, serverEntry);
|
|
274
368
|
written.push(p);
|
|
275
369
|
}
|
|
370
|
+
if (writeCodex) {
|
|
371
|
+
const p = join(homedir(), '.codex', 'config.toml');
|
|
372
|
+
await mergeCodexConfig(p, apiUrl, email, password);
|
|
373
|
+
written.push(p);
|
|
374
|
+
}
|
|
276
375
|
|
|
277
376
|
console.log('Enfyra MCP — local config updated:\n');
|
|
278
377
|
for (const p of written) console.log(` ${p}`);
|
|
279
378
|
console.log('\nNext steps:');
|
|
379
|
+
console.log(' • Codex: restart Codex or start a new session so ~/.codex/config.toml is reloaded.');
|
|
280
380
|
console.log(' • Claude Code: open this folder; approve project MCP if prompted (`claude mcp reset-project-choices` to reset).');
|
|
281
381
|
console.log(' • Cursor: restart Cursor or reload MCP; confirm server under Settings → MCP.');
|
|
282
382
|
console.log(' • Run `config` again anytime to change values (same files are merged/overwritten for `enfyra`).');
|
|
@@ -39,7 +39,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
39
39
|
'### Capability map (current Enfyra system)',
|
|
40
40
|
'- **Schema/metadata:** `table_definition`, `relation_definition`, and schema tools manage tables, columns, relations, validation, and migrations. `column_definition` is internal/no-route; columns are created/updated through table schema operations.',
|
|
41
41
|
'- **Dynamic REST API:** `route_definition`, `route_handler_definition`, `pre_hook_definition`, `post_hook_definition`, `route_permission_definition`, and `method_definition` define paths, methods, handlers, hooks, and permissions.',
|
|
42
|
-
'- **Auth/OAuth/session:** `user_definition`, `role_definition`, `oauth_config_definition`, `oauth_account_definition`; `session_definition` is internal/no-route. OAuth is browser redirect only; MCP login is email/password.',
|
|
42
|
+
'- **Auth/OAuth/session:** `user_definition`, `role_definition`, `oauth_config_definition`, `oauth_account_definition`; `session_definition` is internal/no-route. OAuth is browser redirect only; MCP login is email/password. `user_definition` is the single source of truth for app users.',
|
|
43
43
|
'- **Guards/permissions/validation:** `guard_definition`, `guard_rule_definition`, `field_permission_definition`, and `column_rule_definition` control route guards, field access, and request body validation.',
|
|
44
44
|
'- **GraphQL:** `gql_definition` enables tables in GraphQL. GraphQL endpoint and schema share `ENFYRA_API_URL`; GraphQL requires Bearer auth.',
|
|
45
45
|
'- **Files/storage/assets:** `file_definition`, `file_permission_definition`, `folder_definition`, `storage_config_definition` plus upload/assets routes and file helpers.',
|
|
@@ -53,6 +53,15 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
53
53
|
'- **Direct to Nest:** `http://localhost:1105` — no `/api` suffix on default Nest. Wrong: `http://localhost:1105/api/table_definition` (404) unless a proxy adds `/api`.',
|
|
54
54
|
'- GraphQL: `{base}/graphql` and `{base}/graphql-schema` always share this same base.',
|
|
55
55
|
'',
|
|
56
|
+
'### When the user asks how to connect a Nuxt/Next/SSR app to Enfyra',
|
|
57
|
+
'- This is guidance for the assistant to answer users and generate app code. It is not a separate MCP tool workflow.',
|
|
58
|
+
'- For real user-facing SSR apps, proxy all Enfyra traffic through the SSR app origin: **`{{ nuxtApp }}/api/**`** (or the equivalent Next route handler prefix). Browser code should call same-origin `/api/...`, not the raw Enfyra backend URL.',
|
|
59
|
+
'- If the SSR app lets Enfyra manage httpOnly cookies, password login is **`POST {{ nuxtApp }}/api/login`**, not `{{ nuxtApp }}/api/auth/login`. The SSR route calls backend `/auth/login`, stores `accessToken`, `refreshToken`, and `expTime` cookies, and returns the backend login response.',
|
|
60
|
+
'- After cookie-managed login, browser fetches use `/api/<resource>` with normal credentials/cookies; the SSR proxy/middleware attaches or refreshes the Bearer token server-side. Do not read JWTs in browser JavaScript for this mode.',
|
|
61
|
+
'- For OAuth in SSR/cookie mode, enable cookie handling on the Enfyra OAuth config (`autoSetCookies` / set-cookies mode). Start OAuth at `{{ nuxtApp }}/api/auth/{provider}?redirect=<returnUrl>`. The proxy forwards to backend `/auth/{provider}` with the app origin, backend redirects to `{{ nuxtApp }}/api/auth/set-cookies`, the SSR route stores cookies, then redirects to `redirect`.',
|
|
62
|
+
'- Token-query OAuth (`appCallbackUrl` receives `accessToken`, `refreshToken`, `expTime`) is only for non-SSR or manually managed token apps. Do not recommend it for Nuxt/Next SSR when Enfyra can set cookies.',
|
|
63
|
+
'- If you are explaining MCP\'s own internal authentication, that is separate: this MCP server logs itself in with email/password against backend `/auth/login`. Do not copy that pattern into generated Nuxt/Next browser app code.',
|
|
64
|
+
'',
|
|
56
65
|
'### Routes vs tables (custom endpoints, handlers, hooks)',
|
|
57
66
|
'- REST-first workflow for any feature: **`inspect_feature`** to locate candidates → **`inspect_table`** for table/field/relation/rule context → **`inspect_route`** for handlers/hooks/guards/permissions → **`test_rest_endpoint`** to verify the actual HTTP behavior.',
|
|
58
67
|
'- Use **`create_column_rule`** for standard request validation, **`create_field_permission`** for per-field read/create/update rules, **`create_route_permission`** for authenticated route access, and **`create_guard`** for pre/post-auth request gates.',
|
|
@@ -67,6 +76,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
67
76
|
'- MCP **`create_table` supports `isSingleRecord` directly**. Set `isSingleRecord: true` in the create call for settings/config tables that should keep only one record; do not create first and then patch only for this flag.',
|
|
68
77
|
'- MCP **`create_table` does not accept `alias`**. Do not invent or send alias during table creation; default route/schema behavior is based on `name`. Use `update_table` later only when alias truly needs to change.',
|
|
69
78
|
'- In `create_table.relations`, each relation uses `targetTable` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
|
|
79
|
+
'- **Use `user_definition` as the only user table.** Do not create app-specific user/profile mapping tables such as `chat_profile`, `app_user`, `customer_user`, or tables that only mirror/link Enfyra users. If an app needs extra user fields or user relations, add columns/relations directly on `user_definition`.',
|
|
80
|
+
'- When modeling features that involve users, relate domain tables directly to `user_definition` through real Enfyra relations. Examples: `chat_conversation_member.member` → `user_definition`, `chat_message.sender` → `user_definition`, `order.customer` → `user_definition`. Do not create duplicate scalar columns like `userId` or separate profile records just to point back to a user.',
|
|
81
|
+
'- **Prefer real relations over relation-shaped columns.** If a field represents another record or list of records, model it as `relations`, not as columns such as `userId`, `author_id`, `categoryIds`, `teamIds`, `itemsJson`, or object/array JSON containing related records. Ask only when the user explicitly wants denormalized snapshot data.',
|
|
82
|
+
'- Common mapping: one owner record → `many-to-one`; one record has many children → define the child `many-to-one` and use inverse/read deep relation; peer/tag lists → `many-to-many`; one profile/settings row per parent → `one-to-one` when supported by the model.',
|
|
83
|
+
'- If the user asks to add a foreign key field, interpret it as a relation request unless they explicitly say they need a plain scalar column. Do not create both a relation and a duplicate scalar FK column for the same concept.',
|
|
70
84
|
'- **Never ask for or provide physical FK column names** when creating/updating relations. Do not include `fkCol`, `fkColumn`, `foreignKeyColumn`, `sourceColumn`, `targetColumn`, `junctionSourceColumn`, or `junctionTargetColumn` in create/update payloads unless you are only displaying existing metadata. Enfyra relation cascade derives physical FK/junction names from `propertyName` and table metadata, then hides FK columns from app form/schema definition.',
|
|
71
85
|
'- For relation CRUD payloads, the public interface is the relation `propertyName`: example create body uses `"author": {"id": 1}`, not `"authorId"` or a physical FK column. Query/deep/filter keys also use relation `propertyName`.',
|
|
72
86
|
'- Enfyra creates a **default** route at `/{table_name}` using the table **name** from `create_table` (not the alias). Prefer **`create_route`** for additional or custom paths instead of new tables.',
|
|
@@ -124,19 +138,21 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
124
138
|
'- Enfyra exposes OAuth callback at **`{ENFYRA_API_URL}/auth/{provider}/callback`**. Example when `ENFYRA_API_URL` is `http://localhost:3000/api`: **Google** callback URL is **`http://localhost:3000/api/auth/google/callback`** — i.e. `{ENFYRA_API_URL}/auth/google/callback` (same pattern for Facebook/GitHub: `.../auth/facebook/callback`, `.../auth/github/callback`).',
|
|
125
139
|
'- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
|
|
126
140
|
'- **Enfyra** (`oauth_config_definition` / OAuth settings): field **`redirectUri`** must be the **same string** as in Google Console — byte-for-byte. If they differ, Google or the server will reject the flow.',
|
|
127
|
-
'-
|
|
141
|
+
'- **SSR/cookie mode:** enable `autoSetCookies` / set-cookies mode in Enfyra OAuth config. Enfyra redirects to the SSR app endpoint `/api/auth/set-cookies`, which stores httpOnly cookies and then redirects to the original `redirect` URL.',
|
|
142
|
+
'- **Manual token mode only:** `appCallbackUrl` is the frontend URL where Enfyra redirects after OAuth with `accessToken`, `refreshToken`, etc. in query. Use this only when the app intentionally manages tokens itself; it is not the preferred Nuxt/Next SSR pattern.',
|
|
128
143
|
'',
|
|
129
144
|
'**Server flow (for answering users or designing FE):**',
|
|
130
|
-
'1. **Start login (redirect user in browser):** `GET {
|
|
145
|
+
'1. **Start login (redirect user in browser):** SSR/cookie apps use `GET {{ nuxtApp }}/api/auth/{provider}?redirect=<URL_ENCODED>`; direct/manual apps may use `GET {base}/auth/{provider}?redirect=<URL_ENCODED>`. `redirect` is required and is where to send the user after the whole flow.',
|
|
131
146
|
'2. Server **302** to Google/Facebook/GitHub authorization page.',
|
|
132
147
|
'3. Provider calls back: `GET {base}/auth/{provider}/callback?code=...&state=...` (server exchanges code, creates/links user, issues JWT).',
|
|
133
|
-
'4.
|
|
148
|
+
'4. In SSR/cookie mode, backend redirects to `{{ nuxtApp }}/api/auth/set-cookies` with token query params; the SSR route stores httpOnly cookies and redirects to `redirect`. In manual token mode, backend redirects to `appCallbackUrl` with token query params. On failure, redirect includes `?error=...`.',
|
|
134
149
|
'',
|
|
135
150
|
'**Frontend build checklist:**',
|
|
136
|
-
'-
|
|
137
|
-
'- **“Login with Google” button:** `location.href =
|
|
151
|
+
'- Nuxt/Next SSR: implement same-origin API proxy at `/api/**`, a password login route at `/api/login`, and OAuth set-cookie route at `/api/auth/set-cookies`. Browser code never stores JWTs.',
|
|
152
|
+
'- **“Login with Google” button in SSR/cookie apps:** `location.href = `/api/auth/google?redirect=${encodeURIComponent(returnUrl)}``.',
|
|
153
|
+
'- Manual token apps only: register `appCallbackUrl`, read token query params there, strip the URL, store tokens, and use `Authorization: Bearer`.',
|
|
138
154
|
'- **Error handling:** If redirected with `?error=`, show message to user.',
|
|
139
|
-
'- **Do not confuse:** Google’s **Authorized redirect URI** = Enfyra **`redirectUri`** = `{
|
|
155
|
+
'- **Do not confuse:** Google’s **Authorized redirect URI** = Enfyra **`redirectUri`** = backend/proxy `{API_BASE}/auth/google/callback`. SSR set-cookie route = app-owned `/api/auth/set-cookies`. `appCallbackUrl` is only for manual token mode.',
|
|
140
156
|
'',
|
|
141
157
|
'### System tables — which have REST routes',
|
|
142
158
|
'- **Not all system tables have a REST route.** `query_table`, `find_one_record`, `create_record`, etc. all go through the dynamic REST API and will return **404** if the table has no registered route.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -138,7 +138,7 @@ function inferPrimaryKeyContext(tables) {
|
|
|
138
138
|
dominantPrimaryKey: dominant,
|
|
139
139
|
counts,
|
|
140
140
|
inferredBackendFamily: dominant === '_id' ? 'mongodb-like' : dominant === 'id' ? 'sql-like' : 'unknown',
|
|
141
|
-
exactDatabaseType: 'not exposed by current public/admin API; infer from metadata or add a backend context endpoint for exact mysql/postgres/mongodb
|
|
141
|
+
exactDatabaseType: 'not exposed by current public/admin API; infer from metadata or add a backend context endpoint for exact mysql/postgres/mongodb',
|
|
142
142
|
sampleTables: primaryColumns.slice(0, 12),
|
|
143
143
|
};
|
|
144
144
|
}
|