@enfyra/mcp-server 0.0.25 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,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** (where you want `.mcp.json` / `.cursor/mcp.json`):
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 only, `[2]` Cursor only, `[3]` both (default) — unless you already passed `--claude-code` / `--cursor` / etc. Then prompts for `ENFYRA_API_URL`, `ENFYRA_EMAIL`, and `ENFYRA_PASSWORD` when missing. Press **Enter** to accept bracketed defaults (env + existing `enfyra` in either local file). Password **Enter** keeps the current saved password when updating.
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 IDE only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json` only. `--cursor` / `--cursor-only` → `./.cursor/mcp.json` only. Pass **both** target flags write both files (same as default).
24
- - **Help:** `npx @enfyra/mcp-server config --help`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
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');
@@ -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 local project MCP config (Claude Code + Cursor)
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 only under the current working directory:
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 both; with TTY and no target flags, you are prompted [1]/[2]/[3]:
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
- Passing both target flags writes both files.
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 and ./.cursor/mcp.json are used as defaults. Re-run to update.
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 (pickClaude || pickCursor) {
73
- if (pickClaude && pickCursor) {
74
- out.claude = true;
75
- out.cursor = true;
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
- async function loadExistingEnfyraEnv(root, readClaude, readCursor) {
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] Both [default]\n'
152
- + 'Choice [3]: ',
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 === '3' || line === 'both' || line === 'b') {
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
- return { claude: true, cursor: true };
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
- if (usePrompt && !opts.targetExplicit) {
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, true, true);
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`).');
@@ -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,9 @@ 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
+ '- **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.',
80
+ '- 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.',
81
+ '- 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
82
  '- **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
83
  '- 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
84
  '- 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 +136,21 @@ export function buildMcpServerInstructions(apiBaseUrl) {
124
136
  '- 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
137
  '- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
126
138
  '- **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
- '- **`appCallbackUrl`** (Enfyra OAuth config) is **different**: it is the **frontend app** URL where Enfyra redirects **after** a successful OAuth (server attaches `accessToken`, `refreshToken`, etc. in query). That is your SPA route (e.g. `https://myapp.com/oauth/callback`), **not** the Google redirect URI.',
139
+ '- **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.',
140
+ '- **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
141
  '',
129
142
  '**Server flow (for answering users or designing FE):**',
130
- '1. **Start login (redirect user in browser):** `GET {base}/auth/{provider}?redirect=<URL_ENCODED>` **`redirect` is required** (where to send the user after the whole flow; encoded in `state`). Example: `?redirect=https%3A%2F%2Fmyapp.com%2Foauth-done`',
143
+ '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
144
  '2. Server **302** to Google/Facebook/GitHub authorization page.',
132
145
  '3. Provider calls back: `GET {base}/auth/{provider}/callback?code=...&state=...` (server exchanges code, creates/links user, issues JWT).',
133
- '4. Server **302** to **`appCallbackUrl`** from OAuth config, with query params: **`accessToken`**, **`refreshToken`**, **`expTime`**, **`loginProvider`**, **`redirect`** (the original `redirect` from step 1). On failure, redirects to `redirect` with **`?error=...`**.',
146
+ '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
147
  '',
135
148
  '**Frontend build checklist:**',
136
- '- Register **`appCallbackUrl`** in Enfyra (SPA route). Implement that page: on load read `accessToken`, `refreshToken`, `expTime` from query (then **strip from URL**), store tokens, use `Authorization: Bearer` for API.',
137
- '- **“Login with Google” button:** `location.href = `${ENFYRA_API_URL}/auth/google?redirect=${encodeURIComponent(appCallbackUrlOrReturn)}`` — `ENFYRA_API_URL` is the API base (e.g. ends with `/api`). `redirect` is where the user should go after tokens are delivered (often matches or relates to `appCallbackUrl`).',
149
+ '- 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.',
150
+ '- **“Login with Google” button in SSR/cookie apps:** `location.href = `/api/auth/google?redirect=${encodeURIComponent(returnUrl)}``.',
151
+ '- Manual token apps only: register `appCallbackUrl`, read token query params there, strip the URL, store tokens, and use `Authorization: Bearer`.',
138
152
  '- **Error handling:** If redirected with `?error=`, show message to user.',
139
- '- **Do not confuse:** Google’s **Authorized redirect URI** = Enfyra **`redirectUri`** = `{ENFYRA_API_URL}/auth/google/callback`. **`appCallbackUrl`** = your SPA only (Enfyra redirects there *after* processing Google’s callback).',
153
+ '- **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
154
  '',
141
155
  '### System tables — which have REST routes',
142
156
  '- **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.',
@@ -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/sqlite',
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
  }