@enfyra/mcp-server 0.0.107 → 0.0.108

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
@@ -231,7 +231,7 @@ Do not create custom login/logout/me routes that manually set Enfyra token cooki
231
231
 
232
232
  The MCP server exposes tools for metadata discovery, examples, query/CRUD, method management, route lifecycle, route access audit/grant, routes, handlers, hooks, tables, columns, relations, cache reloads, logs, users, roles, packages, menus, extensions, scripts, flows, websocket, files, and `get_enfyra_api_context`.
233
233
 
234
- Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods`, `set_public_route_methods`, and `private_route_methods` for that access boundary.
234
+ Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods` and `private_route_methods` for that access boundary.
235
235
 
236
236
  For authenticated route access, use `audit_route_access` before changing permissions and `ensure_route_access` to grant access by route path plus role/user. For production script edits, use `trace_metadata_usage`, `get_script_source`, and `patch_script_source` so changes are targeted, hash-checked, and validated.
237
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.107",
3
+ "version": "0.0.108",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
36
36
  '',
37
37
  '### Core Contracts',
38
38
  '- Tool JSON responses use `responseFormat: "json+columnar-v1"`. Large arrays of objects may be encoded as `{ format: "columnar-v1", columns: [...], rows: [[...]], rowCount }` only when that is smaller than raw JSON; read each row value by matching `columns[index]` to `rows[n][index]`. Do not guess object keys inside `rows`. `compressionStats` estimates token savings and includes whether compression was applied; use it only when the user asks about savings.',
39
- '- `query_table` and `get_all_routes` require explicit intent: pass `limit` for bounded reads or `all: true` for a complete list. Do not invent arbitrary limits such as 30 or 50.',
39
+ '- `query_table`, `get_all_routes`, and `get_all_tables` require explicit intent: pass `limit` for bounded reads or `all: true` for a complete list. Do not invent arbitrary limits such as 30 or 50.',
40
40
  '- Read tools are minimal by default. Pass explicit `fields`; use metadata inspection before guessing field/relation names. Field exclusion mode exists: `fields=-compiledCode`, and `fields=id,-compiledCode` still means all readable fields except `compiledCode`.',
41
41
  '- Mutations return ids/status by default. Re-read with `find_one_record` or `query_table` and explicit `fields` when the saved row matters.',
42
42
  '- Dynamic repository reads use `filter`, not `where`: `@REPOS.table.find({ filter: {...} })`, `#table.find({ filter: {...} })`, and `exists(filter)`.',
@@ -72,7 +72,7 @@ export function rejectUnsafeRelationDefinitionPayload(tableName, payload) {
72
72
  if (forbidden.length > 0) {
73
73
  throw new Error(
74
74
  `Do not send physical FK/junction fields to enfyra_relation: ${forbidden.join(', ')}. ` +
75
- 'Use create_relation/add_relation with targetTable/type/propertyName; Enfyra derives physical columns.'
75
+ 'Use create_relation with targetTable/type/propertyName; Enfyra derives physical columns.'
76
76
  );
77
77
  }
78
78
  }
@@ -1047,22 +1047,6 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1047
1047
  })),
1048
1048
  );
1049
1049
 
1050
- server.tool(
1051
- 'set_public_route_methods',
1052
- 'Business operation: replace a route publicMethods list exactly.',
1053
- {
1054
- path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
1055
- routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1056
- methods: z.array(z.string()).describe('Exact HTTP method names that should be public. Use an empty array to make all methods private.'),
1057
- },
1058
- async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
1059
- path,
1060
- routeId,
1061
- methods,
1062
- mode: 'replace',
1063
- })),
1064
- );
1065
-
1066
1050
  server.tool(
1067
1051
  'private_route_methods',
1068
1052
  'Business operation: make specific public route methods private again.',
@@ -381,7 +381,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
381
381
  targetColumn: target,
382
382
  preservedColumnIds: beforeIds.filter((id) => id !== String(columnId)),
383
383
  destructive: true,
384
- next: 'Call delete_column/remove_column again with confirm=true to drop the physical column and metadata.',
384
+ next: 'Call delete_column again with confirm=true to drop the physical column and metadata.',
385
385
  }, null, 2) }],
386
386
  };
387
387
  }
@@ -423,7 +423,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
423
423
  targetRelation: target,
424
424
  preservedRelationIds: beforeIds.filter((id) => id !== String(relationId)),
425
425
  destructive: true,
426
- next: 'Call delete_relation/remove_relation again with confirm=true to drop relation metadata and any derived FK/junction structures.',
426
+ next: 'Call delete_relation again with confirm=true to drop relation metadata and any derived FK/junction structures.',
427
427
  }, null, 2) }],
428
428
  };
429
429
  }
@@ -699,18 +699,6 @@ export function registerTableTools(server, ENFYRA_API_URL) {
699
699
  appendColumnToTable
700
700
  );
701
701
 
702
- server.tool(
703
- 'add_column',
704
- [
705
- 'Alias for create_column. Add a column to an existing table through the canonical enfyra_table cascade.',
706
- 'Use this for schema additions, including hidden secret fields with isPublished=false.',
707
- 'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
708
- 'Run schema changes sequentially — migration locks DB per operation.',
709
- ].join(' '),
710
- columnCreateSchema,
711
- appendColumnToTable
712
- );
713
-
714
702
  // ─── UPDATE COLUMN ───
715
703
 
716
704
  server.tool(
@@ -788,18 +776,6 @@ export function registerTableTools(server, ENFYRA_API_URL) {
788
776
  removeColumnFromTable
789
777
  );
790
778
 
791
- server.tool(
792
- 'remove_column',
793
- [
794
- 'Alias for delete_column. Remove a column through the canonical enfyra_table cascade.',
795
- 'This drops the physical column. Confirm destructive schema changes before calling.',
796
- 'Reads full table metadata and skips non-persisted generated/derived column metadata without id/_id when rebuilding the table columns payload.',
797
- 'Run schema changes sequentially — migration locks DB per operation.',
798
- ].join(' '),
799
- columnDeleteSchema,
800
- removeColumnFromTable
801
- );
802
-
803
779
  // ─── CREATE RELATION ───
804
780
 
805
781
  server.tool(
@@ -817,18 +793,6 @@ export function registerTableTools(server, ENFYRA_API_URL) {
817
793
  appendRelationToTable
818
794
  );
819
795
 
820
- server.tool(
821
- 'add_relation',
822
- [
823
- 'Alias for create_relation. Add a relation through the canonical enfyra_table cascade.',
824
- 'sourceTableId and targetTableId may be table ids, exact table names, or aliases.',
825
- 'Use relation propertyName only; never provide physical FK or junction column names.',
826
- 'Run schema changes sequentially — migration locks DB per operation.',
827
- ].join(' '),
828
- relationCreateSchema,
829
- appendRelationToTable
830
- );
831
-
832
796
  // ─── DELETE RELATION ───
833
797
 
834
798
  server.tool(
@@ -844,13 +808,4 @@ export function registerTableTools(server, ENFYRA_API_URL) {
844
808
  removeRelationFromTable
845
809
  );
846
810
 
847
- server.tool(
848
- 'remove_relation',
849
- [
850
- 'Alias for delete_relation. Remove a relation through the canonical enfyra_table cascade.',
851
- 'This can drop FK columns or junction tables. Confirm destructive schema changes before calling.',
852
- ].join(' '),
853
- relationDeleteSchema,
854
- removeRelationFromTable
855
- );
856
811
  }
@@ -2468,137 +2468,6 @@ server.tool(
2468
2468
  },
2469
2469
  );
2470
2470
 
2471
- server.tool(
2472
- 'create_column_rule',
2473
- [
2474
- 'Create a REST body validation rule for a table column.',
2475
- 'Use inspect_table first to confirm validateBody, column type, and existing rules. Rule value is JSON; common shape is {"v": ...}.',
2476
- ].join(' '),
2477
- {
2478
- tableName: z.string().describe('Table name or alias'),
2479
- columnName: z.string().describe('Column name'),
2480
- ruleType: z.enum(['min', 'max', 'minLength', 'maxLength', 'pattern', 'format', 'minItems', 'maxItems', 'custom']).describe('Validation rule type'),
2481
- value: z.string().optional().describe('Rule payload JSON, e.g. {"v":10} or {"v":"email"}'),
2482
- message: z.string().optional().describe('Custom validation error message'),
2483
- description: z.string().optional().describe('Admin note'),
2484
- isEnabled: z.boolean().optional().default(true).describe('Enable the rule immediately'),
2485
- },
2486
- async ({ tableName, columnName, ruleType, value, message, description, isEnabled }) => {
2487
- const { tables } = await getMetadataTables();
2488
- const table = resolveTableOrThrow(tables, tableName);
2489
- const column = resolveFieldOrThrow(table, columnName, 'column');
2490
- const body = {
2491
- ruleType,
2492
- value: parseJsonArg(value, null),
2493
- message,
2494
- description,
2495
- isEnabled,
2496
- column: { id: getId(column) },
2497
- };
2498
- const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_column_rule', {
2499
- method: 'POST',
2500
- body: JSON.stringify(body),
2501
- });
2502
- return { content: [{ type: 'text', text: `Column rule created for ${table.name}.${column.name}.\n${JSON.stringify(result, null, 2)}` }] };
2503
- },
2504
- );
2505
-
2506
- server.tool(
2507
- 'create_field_permission',
2508
- [
2509
- 'Create a field permission for one column or relation.',
2510
- 'Exactly one of columnName or relationName is required. Scope requires roleId or allowedUserIds. Conditions use the field permission condition DSL, not the full query DSL.',
2511
- ].join(' '),
2512
- {
2513
- tableName: z.string().describe('Table name or alias'),
2514
- columnName: z.string().optional().describe('Column name to protect'),
2515
- relationName: z.string().optional().describe('Relation propertyName to protect'),
2516
- action: z.enum(['read', 'create', 'update']).default('read').describe('Action this permission applies to'),
2517
- effect: z.enum(['allow', 'deny']).default('allow').describe('Allow or deny this field action'),
2518
- roleId: z.union([z.string(), z.number()]).optional().describe('Role id scope'),
2519
- allowedUserIds: z.array(z.union([z.string(), z.number()])).optional().describe('Specific user ids scope'),
2520
- condition: z.string().optional().describe('Optional condition JSON using field permission condition DSL'),
2521
- description: z.string().optional().describe('Admin note'),
2522
- isEnabled: z.boolean().optional().default(true).describe('Enable immediately'),
2523
- },
2524
- async ({ tableName, columnName, relationName, action, effect, roleId, allowedUserIds, condition, description, isEnabled }) => {
2525
- if (!!columnName === !!relationName) throw new Error('Provide exactly one of columnName or relationName');
2526
- if (!roleId && (!allowedUserIds || allowedUserIds.length === 0)) {
2527
- throw new Error('Provide roleId or allowedUserIds');
2528
- }
2529
- const { tables } = await getMetadataTables();
2530
- const table = resolveTableOrThrow(tables, tableName);
2531
- const body = {
2532
- isEnabled,
2533
- description,
2534
- action,
2535
- effect,
2536
- condition: parseJsonArg(condition, null),
2537
- ...(roleId ? { role: { id: roleId } } : {}),
2538
- ...(allowedUserIds?.length ? { allowedUsers: allowedUserIds.map((id) => ({ id })) } : {}),
2539
- };
2540
- if (columnName) {
2541
- body.column = { id: getId(resolveFieldOrThrow(table, columnName, 'column')) };
2542
- } else {
2543
- body.relation = { id: getId(resolveFieldOrThrow(table, relationName, 'relation')) };
2544
- }
2545
- const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_field_permission', {
2546
- method: 'POST',
2547
- body: JSON.stringify(body),
2548
- });
2549
- return { content: [{ type: 'text', text: `Field permission created on ${table.name}.${columnName || relationName}.\n${JSON.stringify(result, null, 2)}` }] };
2550
- },
2551
- );
2552
-
2553
- server.tool(
2554
- 'create_route_permission',
2555
- [
2556
- 'Create route access permission for a route and REST methods.',
2557
- 'Use this when a non-root role/user should access an authenticated route. publicMethods are for public access; route permissions are for authenticated role/user access.',
2558
- ].join(' '),
2559
- {
2560
- path: z.string().optional().describe('Route path, e.g. /enfyra_user'),
2561
- routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
2562
- methods: z.array(z.string()).describe('REST method names this permission allows. Each value must exist in enfyra_method.name.'),
2563
- roleId: z.union([z.string(), z.number()]).optional().describe('Role id scope'),
2564
- allowedUserIds: z.array(z.union([z.string(), z.number()])).optional().describe('Specific user ids scope'),
2565
- description: z.string().optional().describe('Admin note'),
2566
- isEnabled: z.boolean().optional().default(true).describe('Enable immediately'),
2567
- },
2568
- async ({ path, routeId, methods, roleId, allowedUserIds, description, isEnabled }) => {
2569
- if (!path && !routeId) throw new Error('Provide path or routeId');
2570
- if (!roleId && (!allowedUserIds || allowedUserIds.length === 0)) {
2571
- throw new Error('Provide roleId or allowedUserIds');
2572
- }
2573
- const routes = await fetchAll('/enfyra_route?limit=1000');
2574
- const route = routes.find((item) => (
2575
- routeId ? sameId(getId(item), routeId) : item.path === normalizeRestPath(path)
2576
- ));
2577
- if (!route) throw new Error(`Route not found: ${routeId || path}`);
2578
- const methodMap = await getMethodMap();
2579
- const body = {
2580
- isEnabled,
2581
- description,
2582
- route: { id: getId(route) },
2583
- methods: resolveMethodIds(methodMap, methods),
2584
- ...(roleId ? { role: { id: roleId } } : {}),
2585
- ...(allowedUserIds?.length ? { allowedUsers: allowedUserIds.map((id) => ({ id })) } : {}),
2586
- };
2587
- const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_route_permission', {
2588
- method: 'POST',
2589
- body: JSON.stringify(body),
2590
- });
2591
- const routeReload = await reloadRoutesResult();
2592
- return { content: [{ type: 'text', text: JSON.stringify({
2593
- action: 'created',
2594
- kind: 'route_permission',
2595
- route: route.path,
2596
- routeReload,
2597
- result,
2598
- }, null, 2) }] };
2599
- },
2600
- );
2601
-
2602
2471
  server.tool(
2603
2472
  'audit_route_access',
2604
2473
  [
@@ -2772,74 +2641,6 @@ server.tool(
2772
2641
  },
2773
2642
  );
2774
2643
 
2775
- server.tool(
2776
- 'create_guard',
2777
- [
2778
- 'Create a metadata guard with optional rules for REST request gating.',
2779
- 'Root guards attach to one route by path/routeId or globally with isGlobal. pre_auth runs before JWT and only has IP/route context; post_auth runs after auth and can use user id.',
2780
- 'Rule types: rate_limit_by_ip, rate_limit_by_user, rate_limit_by_route, ip_whitelist, ip_blacklist. Rate limits use {"maxRequests":number,"perSeconds":number}; IP lists use {"ips":["127.0.0.1","10.0.0.0/24"]}.',
2781
- 'Do not use rate_limit_by_user or userIds on pre_auth guards. Create risky global/IP whitelist guards disabled first, then inspect and test before enabling.',
2782
- ].join(' '),
2783
- {
2784
- name: z.string().describe('Guard name'),
2785
- position: z.enum(['pre_auth', 'post_auth']).default('pre_auth').describe('Execution position for root guard. pre_auth has only IP/route context; post_auth also has authenticated user id.'),
2786
- routeId: z.union([z.string(), z.number()]).optional().describe('Optional route id'),
2787
- path: z.string().optional().describe('Optional route path'),
2788
- methods: z.array(z.string()).optional().describe('Method names this guard applies to. Empty means all configured behavior for route/global.'),
2789
- combinator: z.enum(['and', 'or']).default('and').describe('How child guards/rules combine'),
2790
- priority: z.number().optional().default(0).describe('Lower runs first'),
2791
- isGlobal: z.boolean().optional().default(false).describe('Apply globally instead of one route'),
2792
- isEnabled: z.boolean().optional().default(false).describe('Enable immediately. Default false to avoid accidental lockout.'),
2793
- description: z.string().optional().describe('Admin note'),
2794
- rules: z.string().optional().describe('Optional rules JSON array: [{type, config, priority?, isEnabled?, description?, userIds?}]. Supported types: rate_limit_by_ip, rate_limit_by_user, rate_limit_by_route, ip_whitelist, ip_blacklist.'),
2795
- },
2796
- async ({ name, position, routeId, path, methods, combinator, priority, isGlobal, isEnabled, description, rules }) => {
2797
- let route = null;
2798
- if (!isGlobal && (routeId || path)) {
2799
- const routes = await fetchAll('/enfyra_route?limit=1000');
2800
- route = routes.find((item) => (
2801
- routeId ? sameId(getId(item), routeId) : item.path === normalizeRestPath(path)
2802
- ));
2803
- if (!route) throw new Error(`Route not found: ${routeId || path}`);
2804
- }
2805
- const methodMap = await getMethodMap();
2806
- const guardBody = {
2807
- name,
2808
- position,
2809
- combinator,
2810
- priority,
2811
- isGlobal,
2812
- isEnabled,
2813
- description,
2814
- ...(route ? { route: { id: getId(route) } } : {}),
2815
- ...(methods?.length ? { methods: resolveMethodIds(methodMap, methods) } : {}),
2816
- };
2817
- const guard = await fetchAPI(ENFYRA_API_URL, '/enfyra_guard', {
2818
- method: 'POST',
2819
- body: JSON.stringify(guardBody),
2820
- });
2821
- const ruleInputs = parseJsonArg(rules, []);
2822
- const createdRules = [];
2823
- for (const rule of ruleInputs) {
2824
- const ruleBody = {
2825
- type: rule.type,
2826
- config: rule.config,
2827
- priority: rule.priority ?? 0,
2828
- isEnabled: rule.isEnabled ?? true,
2829
- description: rule.description,
2830
- guard: { id: resultRecordId(guard) },
2831
- ...(Array.isArray(rule.userIds) && rule.userIds.length ? { users: rule.userIds.map((id) => ({ id })) } : {}),
2832
- };
2833
- createdRules.push(await fetchAPI(ENFYRA_API_URL, '/enfyra_guard_rule', {
2834
- method: 'POST',
2835
- body: JSON.stringify(ruleBody),
2836
- }));
2837
- }
2838
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/guards', { method: 'POST' }).catch(() => {});
2839
- return { content: [{ type: 'text', text: `Guard created. Guard cache reloaded.\n${JSON.stringify({ guard, rules: createdRules }, null, 2)}` }] };
2840
- },
2841
- );
2842
-
2843
2644
  // Register table tools
2844
2645
  registerTableTools(server, ENFYRA_API_URL);
2845
2646
  registerPlatformOperationTools(server, ENFYRA_API_URL);