@enfyra/mcp-server 0.0.26 → 0.0.28
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 +1 -1
- package/package.json +1 -1
- package/src/lib/config-local.mjs +85 -2
- package/src/lib/mcp-instructions.js +3 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ 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
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.
|
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';
|
|
@@ -23,14 +24,14 @@ Options:
|
|
|
23
24
|
--password, -p <secret> ENFYRA_PASSWORD
|
|
24
25
|
--reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
|
|
25
26
|
--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,
|
|
27
|
+
Target — non-interactive default is all; with TTY and no target flags, choose with ↑/↓:
|
|
27
28
|
--claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
|
|
28
29
|
--cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
|
|
29
30
|
--codex, --codex-only Only ~/.codex/config.toml (Codex)
|
|
30
31
|
Passing multiple target flags writes each selected target.
|
|
31
32
|
-h, --help Show this help
|
|
32
33
|
|
|
33
|
-
Interactive mode:
|
|
34
|
+
Interactive mode: lets you choose Claude Code / Cursor / Codex / all if you did not pass target flags; then asks for URL / email / password
|
|
34
35
|
when missing. Existing ./.mcp.json, ./.cursor/mcp.json, and ~/.codex/config.toml are used as defaults. Re-run to update.
|
|
35
36
|
|
|
36
37
|
Examples:
|
|
@@ -231,6 +232,28 @@ async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex) {
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
async function promptTargetChoice() {
|
|
235
|
+
const choices = [
|
|
236
|
+
{
|
|
237
|
+
label: 'Claude Code — ./.mcp.json',
|
|
238
|
+
value: { claude: true, cursor: false, codex: false },
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: 'Cursor — ./.cursor/mcp.json',
|
|
242
|
+
value: { claude: false, cursor: true, codex: false },
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
label: 'Codex — ~/.codex/config.toml',
|
|
246
|
+
value: { claude: false, cursor: false, codex: true },
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
label: 'All',
|
|
250
|
+
value: { claude: true, cursor: true, codex: true },
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
if (input.setRawMode && output.isTTY) {
|
|
254
|
+
return promptTargetSelect(choices, 3);
|
|
255
|
+
}
|
|
256
|
+
|
|
234
257
|
const rl = createInterface({ input, output });
|
|
235
258
|
const line = (await rl.question(
|
|
236
259
|
'Where should Enfyra MCP config be written?\n'
|
|
@@ -256,6 +279,66 @@ async function promptTargetChoice() {
|
|
|
256
279
|
return { claude: true, cursor: true, codex: true };
|
|
257
280
|
}
|
|
258
281
|
|
|
282
|
+
async function promptTargetSelect(choices, initialIndex = 0) {
|
|
283
|
+
let selected = Math.max(0, Math.min(initialIndex, choices.length - 1));
|
|
284
|
+
let renderedLines = 0;
|
|
285
|
+
|
|
286
|
+
const render = () => {
|
|
287
|
+
if (renderedLines > 0) {
|
|
288
|
+
output.write(`\x1B[${renderedLines}A\x1B[0J`);
|
|
289
|
+
}
|
|
290
|
+
const lines = [
|
|
291
|
+
'Where should Enfyra MCP config be written?',
|
|
292
|
+
...choices.map((choice, index) => `${index === selected ? '›' : ' '} ${choice.label}`),
|
|
293
|
+
'',
|
|
294
|
+
'Use ↑/↓ to move, Enter to select.',
|
|
295
|
+
];
|
|
296
|
+
renderedLines = lines.length;
|
|
297
|
+
output.write(`${lines.join('\n')}\n`);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const wasRaw = input.isRaw;
|
|
302
|
+
const cleanup = () => {
|
|
303
|
+
input.off('keypress', onKeypress);
|
|
304
|
+
if (!wasRaw) input.setRawMode(false);
|
|
305
|
+
output.write('\x1B[?25h');
|
|
306
|
+
};
|
|
307
|
+
const finish = () => {
|
|
308
|
+
cleanup();
|
|
309
|
+
resolve(choices[selected].value);
|
|
310
|
+
};
|
|
311
|
+
const onKeypress = (_str, key = {}) => {
|
|
312
|
+
if (key.ctrl && key.name === 'c') {
|
|
313
|
+
cleanup();
|
|
314
|
+
output.write('\n');
|
|
315
|
+
reject(new Error('Cancelled'));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (key.name === 'up') {
|
|
319
|
+
selected = selected <= 0 ? choices.length - 1 : selected - 1;
|
|
320
|
+
render();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (key.name === 'down') {
|
|
324
|
+
selected = selected >= choices.length - 1 ? 0 : selected + 1;
|
|
325
|
+
render();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
329
|
+
finish();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
emitKeypressEvents(input);
|
|
334
|
+
input.setRawMode(true);
|
|
335
|
+
input.resume();
|
|
336
|
+
output.write('\x1B[?25l');
|
|
337
|
+
input.on('keypress', onKeypress);
|
|
338
|
+
render();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
259
342
|
async function promptConfig(opts, existing) {
|
|
260
343
|
let apiUrl = opts.apiUrl;
|
|
261
344
|
let email = opts.email;
|
|
@@ -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.',
|
|
@@ -76,6 +76,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
76
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.',
|
|
77
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.',
|
|
78
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.',
|
|
79
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.',
|
|
80
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.',
|
|
81
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.',
|