@enfyra/mcp-server 0.0.27 → 0.0.29
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 +11 -10
- package/package.json +1 -1
- package/src/lib/config-local.mjs +117 -18
package/README.md
CHANGED
|
@@ -17,11 +17,12 @@ From your **Enfyra project root**:
|
|
|
17
17
|
npx @enfyra/mcp-server config
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- **Interactive (default in a terminal):** first asks **where** to write config —
|
|
20
|
+
- **Interactive (default in a terminal):** first asks **where** to write config with an arrow-key selector — Claude Code, Cursor, Codex, or all — 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 host only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json`. `--cursor` / `--cursor-only` → `./.cursor/mcp.json`. `--codex` / `--codex-only` →
|
|
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.
|
|
23
|
+
- **One host only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json`. `--cursor` / `--cursor-only` → `./.cursor/mcp.json`. `--codex` / `--codex-only` → `./.codex/config.toml`. Pass multiple target flags to write each selected host.
|
|
24
|
+
- **Reconfigure:** `npx @enfyra/mcp-server config --reconfig` prompts for the target host again, uses existing project values as defaults, and replaces the old project `enfyra` entry for that host.
|
|
25
|
+
- **Global/user config:** add `--global` only when you intentionally want the selected host config under your home directory instead of this project.
|
|
25
26
|
- **Help:** `npx @enfyra/mcp-server -h` or `npx @enfyra/mcp-server config --help`
|
|
26
27
|
|
|
27
28
|
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`.
|
|
@@ -34,18 +35,18 @@ Use this table to see **where** each host stores config. The **`mcpServers.enfyr
|
|
|
34
35
|
|
|
35
36
|
| | **Codex** | **Claude Code** | **Cursor** |
|
|
36
37
|
|---|-----------|-----------------|------------|
|
|
37
|
-
| **
|
|
38
|
-
| **
|
|
39
|
-
| **Typical install** | `npx @enfyra/mcp-server config --codex` | `
|
|
40
|
-
| **Precedence / merge** |
|
|
41
|
-
| **Gotcha** |
|
|
38
|
+
| **Project (repo, default)** | **`.codex/config.toml`** in the project | **`.mcp.json`** at repository root | **`.cursor/mcp.json`** in the project |
|
|
39
|
+
| **Global (explicit `--global`)** | `~/.codex/config.toml` | `~/.mcp.json` from this helper, or Claude's `~/.claude.json` via `claude mcp add --scope user` | `~/.cursor/mcp.json` |
|
|
40
|
+
| **Typical install** | `npx @enfyra/mcp-server config --codex` | `npx @enfyra/mcp-server config --claude-code` | `npx @enfyra/mcp-server config --cursor` |
|
|
41
|
+
| **Precedence / merge** | Project config is merged/replaced for `enfyra` | Project `.mcp.json` is merged/replaced for `enfyra` | Project `.cursor/mcp.json` is merged/replaced for `enfyra` |
|
|
42
|
+
| **Gotcha** | Open this folder in a new Codex session after editing config | Do not put MCP server definitions in `.claude/settings.json` | Root **`.mcp.json`** is for Claude Code project scope, not Cursor — use **`.cursor/mcp.json`** for Cursor |
|
|
42
43
|
|
|
43
44
|
Expand **one** block below for step-by-step setup.
|
|
44
45
|
|
|
45
46
|
<details open>
|
|
46
47
|
<summary><strong>Codex</strong> — setup</summary>
|
|
47
48
|
|
|
48
|
-
The config command
|
|
49
|
+
The config command writes project Codex config to `./.codex/config.toml` by default:
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
52
|
npx @enfyra/mcp-server config --codex
|
|
@@ -73,7 +74,7 @@ ENFYRA_EMAIL = "your-email@example.com"
|
|
|
73
74
|
ENFYRA_PASSWORD = "your-password"
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
The config writer replaces only `[mcp_servers.enfyra]` and `[mcp_servers.enfyra.env]`; other Codex config and other MCP servers are preserved.
|
|
77
|
+
The config writer replaces only `[mcp_servers.enfyra]` and `[mcp_servers.enfyra.env]`; other Codex config and other MCP servers are preserved. Open this folder in a new Codex session after updating `./.codex/config.toml`. Use `--global --codex` only when you intentionally want `~/.codex/config.toml`.
|
|
77
78
|
|
|
78
79
|
</details>
|
|
79
80
|
|
package/package.json
CHANGED
package/src/lib/config-local.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
3
4
|
import { stdin as input, stdout as output, cwd } from 'node:process';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
6
|
import { homedir } from 'node:os';
|
|
@@ -12,32 +13,34 @@ function printHelp() {
|
|
|
12
13
|
Usage:
|
|
13
14
|
npx @enfyra/mcp-server config [options]
|
|
14
15
|
|
|
15
|
-
Writes project config under the current working directory
|
|
16
|
+
Writes project config under the current working directory:
|
|
16
17
|
• ./.mcp.json — Claude Code project scope
|
|
17
18
|
• ./.cursor/mcp.json — Cursor project scope
|
|
18
|
-
•
|
|
19
|
+
• ./.codex/config.toml — Codex project scope
|
|
19
20
|
|
|
20
21
|
Options:
|
|
21
22
|
--api-url, -a <url> ENFYRA_API_URL
|
|
22
23
|
--email, -e <email> ENFYRA_EMAIL
|
|
23
24
|
--password, -p <secret> ENFYRA_PASSWORD
|
|
25
|
+
--global Write global/user config for selected hosts instead of project config
|
|
24
26
|
--reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
|
|
25
27
|
--yes Non-interactive: no prompts (CI / scripts); use CLI, env, existing file, then defaults
|
|
26
|
-
Target — non-interactive default is all; with TTY and no target flags,
|
|
28
|
+
Target — non-interactive default is all; with TTY and no target flags, choose with ↑/↓:
|
|
27
29
|
--claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
|
|
28
30
|
--cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
|
|
29
|
-
--codex, --codex-only Only
|
|
31
|
+
--codex, --codex-only Only ./.codex/config.toml (Codex project scope)
|
|
30
32
|
Passing multiple target flags writes each selected target.
|
|
31
33
|
-h, --help Show this help
|
|
32
34
|
|
|
33
|
-
Interactive mode:
|
|
34
|
-
when missing. Existing
|
|
35
|
+
Interactive mode: lets you choose Claude Code / Cursor / Codex / all if you did not pass target flags; then asks for URL / email / password
|
|
36
|
+
when missing. Existing project config is used as defaults. Re-run to update.
|
|
35
37
|
|
|
36
38
|
Examples:
|
|
37
39
|
npx @enfyra/mcp-server config
|
|
38
40
|
npx @enfyra/mcp-server config --claude-code
|
|
39
41
|
npx @enfyra/mcp-server config --cursor --yes
|
|
40
42
|
npx @enfyra/mcp-server config --codex --yes
|
|
43
|
+
npx @enfyra/mcp-server config --global --codex
|
|
41
44
|
npx @enfyra/mcp-server config --reconfig
|
|
42
45
|
npx @enfyra/mcp-server config -a http://localhost:3000/api -e admin@x.com -p 'secret'
|
|
43
46
|
npx @enfyra/mcp-server config --yes
|
|
@@ -56,6 +59,7 @@ function parseArgs(argv) {
|
|
|
56
59
|
help: false,
|
|
57
60
|
yes: false,
|
|
58
61
|
reconfig: false,
|
|
62
|
+
global: false,
|
|
59
63
|
};
|
|
60
64
|
let pickClaude = false;
|
|
61
65
|
let pickCursor = false;
|
|
@@ -72,6 +76,7 @@ function parseArgs(argv) {
|
|
|
72
76
|
else if (a === 'help') out.help = true;
|
|
73
77
|
else if (a === '--yes') out.yes = true;
|
|
74
78
|
else if (a === '--reconfig') out.reconfig = true;
|
|
79
|
+
else if (a === '--global') out.global = true;
|
|
75
80
|
else if (a === '--api-url' || a === '-a') out.apiUrl = next();
|
|
76
81
|
else if (a === '--email' || a === '-e') out.email = next();
|
|
77
82
|
else if (a === '--password' || a === '-p') out.password = next();
|
|
@@ -199,11 +204,23 @@ async function readCodexEnfyraEnv(absPath) {
|
|
|
199
204
|
return null;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
|
|
207
|
+
function getCodexConfigPath(root, globalScope) {
|
|
208
|
+
return globalScope ? join(homedir(), '.codex', 'config.toml') : join(root, '.codex', 'config.toml');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getClaudeConfigPath(root, globalScope) {
|
|
212
|
+
return globalScope ? join(homedir(), '.mcp.json') : join(root, '.mcp.json');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getCursorConfigPath(root, globalScope) {
|
|
216
|
+
return globalScope ? join(homedir(), '.cursor', 'mcp.json') : join(root, '.cursor', 'mcp.json');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex, globalScope) {
|
|
203
220
|
const paths = [];
|
|
204
|
-
if (readClaude) paths.push(
|
|
205
|
-
if (readCursor) paths.push(
|
|
206
|
-
if (!readClaude && readCursor) paths.push(join(root, '.mcp.json'));
|
|
221
|
+
if (readClaude) paths.push(getClaudeConfigPath(root, globalScope));
|
|
222
|
+
if (readCursor) paths.push(getCursorConfigPath(root, globalScope));
|
|
223
|
+
if (!globalScope && !readClaude && readCursor) paths.push(join(root, '.mcp.json'));
|
|
207
224
|
const seen = new Set();
|
|
208
225
|
for (const p of paths) {
|
|
209
226
|
if (seen.has(p)) continue;
|
|
@@ -224,19 +241,41 @@ async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex) {
|
|
|
224
241
|
}
|
|
225
242
|
}
|
|
226
243
|
if (readCodex) {
|
|
227
|
-
const codex = await readCodexEnfyraEnv(
|
|
244
|
+
const codex = await readCodexEnfyraEnv(getCodexConfigPath(root, globalScope));
|
|
228
245
|
if (codex) return codex;
|
|
229
246
|
}
|
|
230
247
|
return { apiUrl: '', email: '', password: '' };
|
|
231
248
|
}
|
|
232
249
|
|
|
233
250
|
async function promptTargetChoice() {
|
|
251
|
+
const choices = [
|
|
252
|
+
{
|
|
253
|
+
label: 'Claude Code — project ./.mcp.json',
|
|
254
|
+
value: { claude: true, cursor: false, codex: false },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
label: 'Cursor — project ./.cursor/mcp.json',
|
|
258
|
+
value: { claude: false, cursor: true, codex: false },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
label: 'Codex — project ./.codex/config.toml',
|
|
262
|
+
value: { claude: false, cursor: false, codex: true },
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
label: 'All',
|
|
266
|
+
value: { claude: true, cursor: true, codex: true },
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
if (input.setRawMode && output.isTTY) {
|
|
270
|
+
return promptTargetSelect(choices, 3);
|
|
271
|
+
}
|
|
272
|
+
|
|
234
273
|
const rl = createInterface({ input, output });
|
|
235
274
|
const line = (await rl.question(
|
|
236
275
|
'Where should Enfyra MCP config be written?\n'
|
|
237
276
|
+ ' [1] Claude Code — ./.mcp.json\n'
|
|
238
277
|
+ ' [2] Cursor — ./.cursor/mcp.json\n'
|
|
239
|
-
+ ' [3] Codex —
|
|
278
|
+
+ ' [3] Codex — ./.codex/config.toml\n'
|
|
240
279
|
+ ' [4] All [default]\n'
|
|
241
280
|
+ 'Choice [4]: ',
|
|
242
281
|
)).trim().toLowerCase();
|
|
@@ -256,6 +295,66 @@ async function promptTargetChoice() {
|
|
|
256
295
|
return { claude: true, cursor: true, codex: true };
|
|
257
296
|
}
|
|
258
297
|
|
|
298
|
+
async function promptTargetSelect(choices, initialIndex = 0) {
|
|
299
|
+
let selected = Math.max(0, Math.min(initialIndex, choices.length - 1));
|
|
300
|
+
let renderedLines = 0;
|
|
301
|
+
|
|
302
|
+
const render = () => {
|
|
303
|
+
if (renderedLines > 0) {
|
|
304
|
+
output.write(`\x1B[${renderedLines}A\x1B[0J`);
|
|
305
|
+
}
|
|
306
|
+
const lines = [
|
|
307
|
+
'Where should Enfyra MCP config be written?',
|
|
308
|
+
...choices.map((choice, index) => `${index === selected ? '›' : ' '} ${choice.label}`),
|
|
309
|
+
'',
|
|
310
|
+
'Use ↑/↓ to move, Enter to select.',
|
|
311
|
+
];
|
|
312
|
+
renderedLines = lines.length;
|
|
313
|
+
output.write(`${lines.join('\n')}\n`);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
const wasRaw = input.isRaw;
|
|
318
|
+
const cleanup = () => {
|
|
319
|
+
input.off('keypress', onKeypress);
|
|
320
|
+
if (!wasRaw) input.setRawMode(false);
|
|
321
|
+
output.write('\x1B[?25h');
|
|
322
|
+
};
|
|
323
|
+
const finish = () => {
|
|
324
|
+
cleanup();
|
|
325
|
+
resolve(choices[selected].value);
|
|
326
|
+
};
|
|
327
|
+
const onKeypress = (_str, key = {}) => {
|
|
328
|
+
if (key.ctrl && key.name === 'c') {
|
|
329
|
+
cleanup();
|
|
330
|
+
output.write('\n');
|
|
331
|
+
reject(new Error('Cancelled'));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (key.name === 'up') {
|
|
335
|
+
selected = selected <= 0 ? choices.length - 1 : selected - 1;
|
|
336
|
+
render();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (key.name === 'down') {
|
|
340
|
+
selected = selected >= choices.length - 1 ? 0 : selected + 1;
|
|
341
|
+
render();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
345
|
+
finish();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
emitKeypressEvents(input);
|
|
350
|
+
input.setRawMode(true);
|
|
351
|
+
input.resume();
|
|
352
|
+
output.write('\x1B[?25l');
|
|
353
|
+
input.on('keypress', onKeypress);
|
|
354
|
+
render();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
259
358
|
async function promptConfig(opts, existing) {
|
|
260
359
|
let apiUrl = opts.apiUrl;
|
|
261
360
|
let email = opts.email;
|
|
@@ -337,7 +436,7 @@ export async function runLocalConfig(argv) {
|
|
|
337
436
|
writeCodex = t.codex;
|
|
338
437
|
}
|
|
339
438
|
|
|
340
|
-
const existing = await loadExistingEnfyraEnv(root, writeClaude, writeCursor, writeCodex);
|
|
439
|
+
const existing = await loadExistingEnfyraEnv(root, writeClaude, writeCursor, writeCodex, opts.global);
|
|
341
440
|
|
|
342
441
|
let apiUrl;
|
|
343
442
|
let email;
|
|
@@ -358,17 +457,17 @@ export async function runLocalConfig(argv) {
|
|
|
358
457
|
const written = [];
|
|
359
458
|
|
|
360
459
|
if (writeClaude) {
|
|
361
|
-
const p =
|
|
460
|
+
const p = getClaudeConfigPath(root, opts.global);
|
|
362
461
|
await mergeMcpFile(p, serverEntry);
|
|
363
462
|
written.push(p);
|
|
364
463
|
}
|
|
365
464
|
if (writeCursor) {
|
|
366
|
-
const p =
|
|
465
|
+
const p = getCursorConfigPath(root, opts.global);
|
|
367
466
|
await mergeMcpFile(p, serverEntry);
|
|
368
467
|
written.push(p);
|
|
369
468
|
}
|
|
370
469
|
if (writeCodex) {
|
|
371
|
-
const p =
|
|
470
|
+
const p = getCodexConfigPath(root, opts.global);
|
|
372
471
|
await mergeCodexConfig(p, apiUrl, email, password);
|
|
373
472
|
written.push(p);
|
|
374
473
|
}
|
|
@@ -376,9 +475,9 @@ export async function runLocalConfig(argv) {
|
|
|
376
475
|
console.log('Enfyra MCP — local config updated:\n');
|
|
377
476
|
for (const p of written) console.log(` ${p}`);
|
|
378
477
|
console.log('\nNext steps:');
|
|
379
|
-
console.log(' • Codex:
|
|
478
|
+
console.log(' • Codex: open this folder in a new Codex session so project ./.codex/config.toml is loaded.');
|
|
380
479
|
console.log(' • Claude Code: open this folder; approve project MCP if prompted (`claude mcp reset-project-choices` to reset).');
|
|
381
|
-
console.log(' • Cursor: restart Cursor or reload MCP
|
|
480
|
+
console.log(' • Cursor: open this folder, restart Cursor or reload MCP, then confirm server under Settings → MCP.');
|
|
382
481
|
console.log(' • Run `config` again anytime to change values (same files are merged/overwritten for `enfyra`).');
|
|
383
482
|
if (!email || !password) {
|
|
384
483
|
console.log('\nWarning: ENFYRA_EMAIL or ENFYRA_PASSWORD is empty — tools may not authenticate until set.');
|