@enfyra/mcp-server 0.1.30 → 0.1.32

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.
@@ -0,0 +1,9 @@
1
+ export declare const MCP_TOOLSETS: readonly ["guided", "full"];
2
+ export type McpToolset = typeof MCP_TOOLSETS[number];
3
+ export declare function normalizeMcpToolset(value: unknown): McpToolset;
4
+ export declare function isToolVisibleInToolset(toolName: string, toolset: McpToolset): boolean;
5
+ export declare function installToolsetFilter(server: any, toolset: McpToolset): {
6
+ toolset: "full" | "guided";
7
+ hiddenTools: string[];
8
+ };
9
+ export declare function summarizeToolsetForInstructions(toolset: McpToolset): string;
@@ -0,0 +1,126 @@
1
+ export const MCP_TOOLSETS = ['guided', 'full'];
2
+ const GUIDED_TOOL_NAMES = new Set([
3
+ 'get_enfyra_required_knowledge',
4
+ 'get_enfyra_examples',
5
+ 'discover_enfyra_workflows',
6
+ 'discover_enfyra_system',
7
+ 'discover_runtime_context',
8
+ 'discover_query_capabilities',
9
+ 'discover_script_contexts',
10
+ 'get_enfyra_api_context',
11
+ 'get_permission_profile',
12
+ 'search_admin_extensions',
13
+ 'search_runtime_zone',
14
+ 'debug_field_exposure',
15
+ 'inspect_table',
16
+ 'inspect_route',
17
+ 'inspect_feature',
18
+ 'trace_metadata_usage',
19
+ 'get_table_metadata',
20
+ 'get_all_tables',
21
+ 'get_all_routes',
22
+ 'get_schema_design_context',
23
+ 'query_table',
24
+ 'count_records',
25
+ 'find_one_record',
26
+ 'create_records',
27
+ 'update_records',
28
+ 'delete_records',
29
+ 'create_tables',
30
+ 'update_tables',
31
+ 'delete_tables',
32
+ 'create_columns',
33
+ 'update_columns',
34
+ 'delete_columns',
35
+ 'create_relations',
36
+ 'delete_relations',
37
+ 'get_script_source',
38
+ 'patch_script_source',
39
+ 'update_script_source',
40
+ 'validate_dynamic_script',
41
+ 'validate_extension_code',
42
+ 'get_extension_theme_contract',
43
+ 'get_theme_class_reference',
44
+ 'extension_workflow',
45
+ 'update_extension_code',
46
+ 'patch_extension_code',
47
+ 'ensure_menu',
48
+ 'reorder_menus',
49
+ 'ensure_page_extension',
50
+ 'ensure_global_extension',
51
+ 'ensure_widget_extension',
52
+ 'api_endpoint_workflow',
53
+ 'create_pre_hook',
54
+ 'test_rest_endpoint',
55
+ 'run_admin_test',
56
+ 'test_flow_step',
57
+ 'trigger_flow',
58
+ 'audit_route_access',
59
+ 'ensure_route_access',
60
+ 'enable_route',
61
+ 'disable_route',
62
+ 'delete_route',
63
+ 'public_route_methods',
64
+ 'private_route_methods',
65
+ 'set_table_graphql',
66
+ 'ensure_column_rule',
67
+ 'ensure_field_permission',
68
+ 'ensure_guard',
69
+ 'ensure_websocket_gateway',
70
+ 'ensure_websocket_event',
71
+ 'ensure_manual_flow',
72
+ 'ensure_scheduled_flow',
73
+ 'plan_flow_steps',
74
+ 'choose_flow_step_tool',
75
+ 'ensure_script_flow_step',
76
+ 'ensure_condition_flow_step',
77
+ 'ensure_query_flow_step',
78
+ 'ensure_http_flow_step',
79
+ 'ensure_create_flow_step',
80
+ 'ensure_update_flow_step',
81
+ 'ensure_delete_flow_step',
82
+ 'ensure_sleep_flow_step',
83
+ 'ensure_log_flow_step',
84
+ 'ensure_trigger_flow_step',
85
+ 'search_logs',
86
+ 'tail_log',
87
+ 'search_npm',
88
+ 'install_package',
89
+ ]);
90
+ export function normalizeMcpToolset(value) {
91
+ const raw = String(value || '').trim().toLowerCase();
92
+ if (raw === 'full')
93
+ return 'full';
94
+ return 'guided';
95
+ }
96
+ export function isToolVisibleInToolset(toolName, toolset) {
97
+ if (toolset === 'full')
98
+ return true;
99
+ return GUIDED_TOOL_NAMES.has(toolName);
100
+ }
101
+ export function installToolsetFilter(server, toolset) {
102
+ const registerTool = server.tool.bind(server);
103
+ const hiddenTools = [];
104
+ server.tool = (name, description, schema, handler) => {
105
+ if (!isToolVisibleInToolset(name, toolset)) {
106
+ hiddenTools.push(name);
107
+ return undefined;
108
+ }
109
+ return registerTool(name, description, schema, handler);
110
+ };
111
+ return {
112
+ toolset,
113
+ hiddenTools,
114
+ };
115
+ }
116
+ export function summarizeToolsetForInstructions(toolset) {
117
+ if (toolset === 'full') {
118
+ return 'Toolset mode: full. All Enfyra MCP tools are visible, including low-level escape hatches.';
119
+ }
120
+ return [
121
+ 'Toolset mode: guided. The visible tool surface is curated for weak and medium LLMs.',
122
+ 'Prefer discover_enfyra_workflows, search_runtime_zone, inspect_* tools, and operation-level ensure/workflow tools.',
123
+ 'Low-level escape hatches are hidden by default; set ENFYRA_MCP_TOOLSET=full only for expert debugging or compatibility work.',
124
+ ].join(' ');
125
+ }
126
+ //# sourceMappingURL=toolset-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolset-filter.js","sourceRoot":"","sources":["../../src/lib/toolset-filter.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAU,CAAC;AAIxD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,+BAA+B;IAC/B,qBAAqB;IACrB,2BAA2B;IAC3B,wBAAwB;IACxB,0BAA0B;IAC1B,6BAA6B;IAC7B,0BAA0B;IAC1B,wBAAwB;IACxB,wBAAwB;IAExB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;IACtB,eAAe;IACf,eAAe;IACf,iBAAiB;IACjB,sBAAsB;IACtB,oBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,2BAA2B;IAE3B,aAAa;IACb,eAAe;IACf,iBAAiB;IACjB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAEhB,eAAe;IACf,eAAe;IACf,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,kBAAkB;IAClB,kBAAkB;IAElB,mBAAmB;IACnB,qBAAqB;IACrB,sBAAsB;IACtB,yBAAyB;IACzB,yBAAyB;IAEzB,8BAA8B;IAC9B,2BAA2B;IAC3B,oBAAoB;IACpB,uBAAuB;IACvB,sBAAsB;IACtB,aAAa;IACb,eAAe;IACf,uBAAuB;IACvB,yBAAyB;IACzB,yBAAyB;IAEzB,uBAAuB;IACvB,iBAAiB;IACjB,oBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,cAAc;IAEd,oBAAoB;IACpB,qBAAqB;IACrB,cAAc;IACd,eAAe;IACf,cAAc;IACd,sBAAsB;IACtB,uBAAuB;IAEvB,mBAAmB;IACnB,oBAAoB;IACpB,yBAAyB;IACzB,cAAc;IAEd,0BAA0B;IAC1B,wBAAwB;IAExB,oBAAoB;IACpB,uBAAuB;IACvB,iBAAiB;IACjB,uBAAuB;IACvB,yBAAyB;IACzB,4BAA4B;IAC5B,wBAAwB;IACxB,uBAAuB;IACvB,yBAAyB;IACzB,yBAAyB;IACzB,yBAAyB;IACzB,wBAAwB;IACxB,sBAAsB;IACtB,0BAA0B;IAE1B,aAAa;IACb,UAAU;IACV,YAAY;IACZ,iBAAiB;CAClB,CAAC,CAAC;AAEH,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrD,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAgB,EAAE,OAAmB;IAC1E,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAW,EAAE,OAAmB;IACnE,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAa,EAAE,CAAC;IAEjC,MAAM,CAAC,IAAI,GAAG,CAAC,IAAY,EAAE,WAAgB,EAAE,MAAW,EAAE,OAAY,EAAE,EAAE;QAC1E,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;YAC3C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1D,CAAC,CAAC;IAEF,OAAO;QACL,OAAO;QACP,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,OAAmB;IACjE,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,2FAA2F,CAAC;IACrG,CAAC;IACD,OAAO;QACL,qFAAqF;QACrF,oHAAoH;QACpH,8HAA8H;KAC/H,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
@@ -37,9 +37,11 @@ import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertE
37
37
  import { validateMainTableRoutePath } from './lib/route-guards.js';
38
38
  import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
39
39
  import { compactSourceFields, writeSourceArtifact } from './lib/source-artifacts.js';
40
+ import { installToolsetFilter, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
40
41
  import { findRoutePermission, mergeMethodNames, normalizeMethodNames, resolveRoleByNameOrId, routeAvailableMethodNames, routePublicMethodNames, summarizeRouteAccess, summarizeRoutePermission, validateMethodsForRoute, } from './lib/route-permission-tools.js';
41
42
  // Initialize auth module
42
43
  initAuth(ENFYRA_API_URL, ENFYRA_API_TOKEN);
44
+ const MCP_TOOLSET = normalizeMcpToolset(process.env.ENFYRA_MCP_TOOLSET);
43
45
  const CAPABILITY_AREAS = [
44
46
  {
45
47
  area: 'Schema and metadata',
@@ -817,9 +819,12 @@ const server = new McpServer({
817
819
  name: 'enfyra-mcp',
818
820
  version: '1.0.0',
819
821
  }, {
820
- instructions: buildMcpServerInstructions(ENFYRA_API_URL),
822
+ instructions: buildMcpServerInstructions(ENFYRA_API_URL, {
823
+ toolsetSummary: summarizeToolsetForInstructions(MCP_TOOLSET),
824
+ }),
821
825
  });
822
826
  installColumnarToolFormatter(server);
827
+ installToolsetFilter(server, MCP_TOOLSET);
823
828
  // ============================================================================
824
829
  // METADATA TOOLS
825
830
  // ============================================================================
@@ -1308,7 +1313,7 @@ server.tool('get_enfyra_api_context', [
1308
1313
  };
1309
1314
  return jsonContent(payload);
1310
1315
  });
1311
- server.tool('query_table', 'Query any route-backed table. Response is minimal unless fields is explicit. Every call must pass either limit or all=true.', {
1316
+ server.tool('query_table', 'Query any route-backed table. Response is minimal unless fields is explicit. Every call must pass either limit or all=true. Use count_records or meta=filterCount/totalCount for counts; call discover_query_capabilities before using aggregate objects instead of guessing _sum/_count operators.', {
1312
1317
  tableName: z.string().describe('Table name to query'),
1313
1318
  filter: jsonObjectParam(z, 'Filter object').optional().describe('Filter object. Example: {"status": {"_eq": "active"}}.'),
1314
1319
  sort: z.string().optional().describe('Sort field. Prefix with - for descending (e.g., "createdAt", "-id")'),
@@ -1318,7 +1323,7 @@ server.tool('query_table', 'Query any route-backed table. Response is minimal un
1318
1323
  fields: z.array(z.string()).optional().describe('Fields to select. If omitted, MCP selects only the table primary key to avoid oversized responses.'),
1319
1324
  meta: z.string().optional().describe('Optional REST meta request, e.g. "totalCount", "filterCount", or aggregate modes supported by the route. Use count_records for simple counts.'),
1320
1325
  deep: jsonObjectParam(z, 'Deep relation fetch object').optional().describe('Optional deep relation fetch object. Keys must be relation propertyName values.'),
1321
- aggregate: jsonObjectParam(z, 'Aggregate object').optional().describe('Optional aggregate object keyed by real fields/relations. Results are returned in response.meta.aggregate when supported. Do not request aggregates over hidden fields/private relations in user-facing APIs.'),
1326
+ aggregate: jsonObjectParam(z, 'Aggregate object').optional().describe('Optional aggregate object keyed by real fields/relations, only after discover_query_capabilities confirms the supported operator shape for this table/route. Results are returned in response.meta.aggregate when supported. Do not guess _sum/_count; use count_records or meta=filterCount/totalCount for counts. Do not request aggregates over hidden fields/private relations in user-facing APIs.'),
1322
1327
  }, async ({ tableName, filter, sort, page, limit, all, fields, meta, deep, aggregate }) => {
1323
1328
  if (!all && limit === undefined) {
1324
1329
  throw new Error('query_table requires either limit or all=true. Do not rely on implicit default page sizes.');
@@ -1673,13 +1678,21 @@ server.tool('update_script_source', [
1673
1678
  scriptValidation: prepared.scriptValidation,
1674
1679
  }, null, 2) }] };
1675
1680
  });
1676
- server.tool('delete_records', 'Delete one or more route-backed records in one MCP call. Pass items as a native JSON array; for one delete, pass one item. The tool previews every target when confirm=false, rejects duplicate ids, and deletes sequentially when confirm=true.', {
1681
+ function isNotFoundDeleteError(error) {
1682
+ const message = String(error?.message || error || '').toLowerCase();
1683
+ return message.includes('api error (404)')
1684
+ || message.includes('not found')
1685
+ || message.includes('not exists')
1686
+ || message.includes('does not exist');
1687
+ }
1688
+ server.tool('delete_records', 'Delete one or more route-backed records in one MCP call. Pass items as a native JSON array; for one delete, pass one item. The tool previews every target when confirm=false, rejects duplicate ids, and deletes sequentially when confirm=true. By default, confirm=true skips records that were already removed by cascade or a previous cleanup step.', {
1677
1689
  tableName: z.string().describe('Table name'),
1678
1690
  items: bulkObjectArrayParam(z, 'Delete items').describe('Native JSON array of delete items: [{ "id": "...", "queryParams": { ... }? }].'),
1679
1691
  maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one MCP batch. Default/max is 100.'),
1680
1692
  confirm: z.boolean().optional().default(false).describe('Required true to apply destructive deletes. Omit/false returns previews only.'),
1693
+ skipNotFound: z.boolean().optional().default(true).describe('When confirm=true, continue if a target is already gone, for example because a previous delete cascaded child records. Default true.'),
1681
1694
  globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1682
- }, async ({ tableName, items, maxItems, confirm, globalRulesAckKey }) => {
1695
+ }, async ({ tableName, items, maxItems, confirm, skipNotFound, globalRulesAckKey }) => {
1683
1696
  validateTableName(tableName);
1684
1697
  const parsedItems = parseBulkItemsArg('items', items);
1685
1698
  assertMaxBulkItems('delete_records', parsedItems, maxItems);
@@ -1718,24 +1731,42 @@ server.tool('delete_records', 'Delete one or more route-backed records in one MC
1718
1731
  }
1719
1732
  assertGlobalRulesAck(globalRulesAckKey);
1720
1733
  const deleted = [];
1734
+ const skippedNotFound = [];
1721
1735
  for (const [index, item] of parsedItems.entries()) {
1722
1736
  const query = parseQueryParamsArg(JSON.stringify(item.queryParams || {}));
1723
- const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${encodeURIComponent(String(item.id))}`, query), { method: 'DELETE' });
1724
- deleted.push({
1725
- index,
1726
- id: item.id,
1727
- statusCode: result?.statusCode,
1728
- success: result?.success,
1729
- });
1737
+ try {
1738
+ const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${encodeURIComponent(String(item.id))}`, query), { method: 'DELETE' });
1739
+ deleted.push({
1740
+ index,
1741
+ id: item.id,
1742
+ statusCode: result?.statusCode,
1743
+ success: result?.success,
1744
+ });
1745
+ }
1746
+ catch (error) {
1747
+ if (skipNotFound && isNotFoundDeleteError(error)) {
1748
+ skippedNotFound.push({
1749
+ index,
1750
+ id: item.id,
1751
+ skipped: true,
1752
+ reason: 'not_found',
1753
+ });
1754
+ continue;
1755
+ }
1756
+ throw error;
1757
+ }
1730
1758
  }
1731
1759
  return { content: [{ type: 'text', text: JSON.stringify({
1732
1760
  action: 'deleted_records',
1733
1761
  tableName,
1734
1762
  requested: parsedItems.length,
1735
1763
  deletedCount: deleted.length,
1764
+ skippedNotFoundCount: skippedNotFound.length,
1736
1765
  sequential: true,
1737
1766
  duplicateIdsRejected: true,
1767
+ skipNotFound,
1738
1768
  deleted,
1769
+ skippedNotFound,
1739
1770
  }, null, 2) }] };
1740
1771
  });
1741
1772
  server.tool('list_methods', 'List enfyra_method records with their UI colors. Use this before creating route methods or method-colored UI.', {}, async () => {
@@ -2321,13 +2352,13 @@ server.tool('test_rest_endpoint', [
2321
2352
  };
2322
2353
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
2323
2354
  });
2324
- server.tool('get_all_routes', 'List route definitions with minimal fields. Every call must pass either limit or all=true. Call inspect_route for handlers/hooks/permissions detail.', {
2355
+ server.tool('get_all_routes', 'List route definitions with minimal fields. Complete route lists must pass either limit or all=true. If search is provided without limit, the tool returns a bounded lookup window of 10 matches. Call inspect_route for handlers/hooks/permissions detail.', {
2325
2356
  includeDisabled: z.boolean().optional().default(false).describe('Include disabled routes'),
2326
2357
  search: z.string().optional().describe('Optional path or table substring filter. Use this before creating a route to check duplicates.'),
2327
- limit: z.number().int().positive().optional().describe('Maximum routes returned after search. Required unless all=true. Do not invent arbitrary limits for "all"; use all=true instead.'),
2358
+ limit: z.number().int().positive().optional().describe('Maximum routes returned after search. Required unless all=true or search is provided. Do not invent arbitrary limits for "all"; use all=true instead.'),
2328
2359
  all: z.boolean().optional().default(false).describe('Return all matched routes. Use this when the user asks for all routes or a complete route list.'),
2329
2360
  }, async ({ includeDisabled, search, limit, all }) => {
2330
- if (!all && limit === undefined) {
2361
+ if (!all && limit === undefined && !search?.trim()) {
2331
2362
  throw new Error('get_all_routes requires either limit or all=true. Do not rely on implicit default page sizes.');
2332
2363
  }
2333
2364
  if (all && limit !== undefined) {
@@ -2348,7 +2379,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2348
2379
  mainTable: route.mainTable,
2349
2380
  }).toLowerCase().includes(q))
2350
2381
  : allRoutes;
2351
- const routeLimit = all ? matchedRoutes.length : limit;
2382
+ const routeLimit = all ? matchedRoutes.length : (limit ?? 10);
2352
2383
  const payload = {
2353
2384
  statusCode: result?.statusCode,
2354
2385
  success: result?.success,
@@ -2356,6 +2387,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2356
2387
  matchedRouteCount: matchedRoutes.length,
2357
2388
  returnedRouteCount: Math.min(matchedRoutes.length, routeLimit),
2358
2389
  all: !!all,
2390
+ implicitSearchLimit: Boolean(!all && limit === undefined && search?.trim()),
2359
2391
  complete: all || routeLimit >= matchedRoutes.length,
2360
2392
  hardCap: all ? null : routeLimit,
2361
2393
  search: search || null,
@@ -2481,9 +2513,10 @@ server.tool('create_handler', [
2481
2513
  }, null, 2) }] };
2482
2514
  });
2483
2515
  server.tool('create_pre_hook', [
2484
- 'Create a pre-hook that runs BEFORE the handler. Use to validate, transform, or inject data.',
2516
+ 'Create a pre-hook that runs BEFORE the handler. Use to validate, transform, inject data, or enforce owner/tenant row filters (RLS).',
2485
2517
  'Use `routeId` from `create_route` or `get_all_routes` — do not create a new table just to get a route id.',
2486
2518
  'Macros: @BODY, @QUERY, @PARAMS, @USER, @REPOS, @HELPERS, @THROW400..@THROW503.',
2519
+ 'For canonical table reads, merge security filters into @QUERY.filter and preserve @QUERY.fields/deep/sort/limit/page/meta/aggregate.',
2487
2520
  'If the hook returns a value, that value becomes the response (handler is skipped).',
2488
2521
  ].join(' '), {
2489
2522
  routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),