@enfyra/mcp-server 0.1.5 → 0.1.7

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.
@@ -27,9 +27,11 @@ import {
27
27
  assertDynamicCodeKnowledgeAck,
28
28
  assertDynamicCodeKnowledgeAckIf,
29
29
  assertExtensionKnowledgeAckIf,
30
+ assertGlobalRulesAck,
30
31
  buildRequiredKnowledgePayload,
31
32
  dynamicCodeKnowledgeAckParam,
32
33
  extensionKnowledgeAckParam,
34
+ globalRulesAckParam,
33
35
  } from './lib/required-knowledge.js';
34
36
  import { validateMainTableRoutePath } from './lib/route-guards.js';
35
37
  import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
@@ -329,7 +331,7 @@ function summarizeRoutes(routesResult) {
329
331
  }));
330
332
  }
331
333
 
332
- function summarizeMetadata(metadata, { search, limit } = {}) {
334
+ function summarizeMetadata(metadata, { search, limit, all = false } = {}) {
333
335
  const tables = normalizeTables(metadata);
334
336
  const q = search ? search.toLowerCase() : null;
335
337
  const summarized = tables.map((table) => ({
@@ -344,11 +346,13 @@ function summarizeMetadata(metadata, { search, limit } = {}) {
344
346
  const matched = q
345
347
  ? summarized.filter((table) => JSON.stringify(table).toLowerCase().includes(q))
346
348
  : summarized;
347
- const outputLimit = limit || 30;
349
+ const outputLimit = all ? matched.length : (limit || 30);
348
350
  return {
349
351
  tableCount: tables.length,
350
352
  matchedTableCount: matched.length,
351
353
  returnedTableCount: Math.min(matched.length, outputLimit),
354
+ complete: all || outputLimit >= matched.length,
355
+ hardCap: all ? null : outputLimit,
352
356
  search: search || null,
353
357
  tables: matched.slice(0, outputLimit),
354
358
  };
@@ -805,15 +809,21 @@ server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Us
805
809
  includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
806
810
  search: z.string().optional().describe('Optional table-name/alias substring filter.'),
807
811
  limit: z.number().optional().describe('Maximum tables returned after search. Default 30.'),
808
- }, async ({ includeFull, search, limit }) => {
812
+ all: z.boolean().optional().default(false).describe('Return every matched table summary. Use when a complete table list is required.'),
813
+ }, async ({ includeFull, search, limit, all }) => {
814
+ if (all && limit !== undefined) {
815
+ throw new Error('get_all_metadata accepts either all=true or limit, not both.');
816
+ }
809
817
  const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
810
818
  const payload = includeFull
811
819
  ? result
812
820
  : {
813
821
  statusCode: result?.statusCode,
814
822
  success: result?.success,
815
- ...summarizeMetadata(result, { search, limit }),
816
- detailHint: 'Default response is capped and minimal. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.',
823
+ ...summarizeMetadata(result, { search, limit, all }),
824
+ detailHint: all
825
+ ? 'Complete summary returned. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.'
826
+ : 'Default response is capped and minimal. Pass all=true for a complete summary, or call get_table_metadata({ tableName }) / inspect_table({ tableName }) for detail.',
817
827
  };
818
828
  return jsonContent(payload);
819
829
  });
@@ -1471,9 +1481,11 @@ server.tool('create_record', 'Create a new record in any route-backed table. The
1471
1481
  tableName: z.string().describe('Table name to insert into'),
1472
1482
  data: z.string().describe('Record data as JSON string'),
1473
1483
  queryParams: z.string().optional().describe('Optional query params as JSON object string, e.g. {"expired_at":"2026-09-20"}. Use for route contracts that intentionally keep workflow fields out of the validated body.'),
1484
+ globalRulesAckKey: globalRulesAckParam(z),
1474
1485
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1475
1486
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and data contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
1476
- }, async ({ tableName, data, queryParams, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1487
+ }, async ({ tableName, data, queryParams, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1488
+ assertGlobalRulesAck(globalRulesAckKey);
1477
1489
  validateTableName(tableName);
1478
1490
  assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
1479
1491
  const prepared = await prepareGenericMutation(tableName, data);
@@ -1490,9 +1502,11 @@ server.tool('update_record', 'Update an existing record by ID using PATCH. The t
1490
1502
  id: z.string().describe('Record ID to update'),
1491
1503
  data: z.string().describe('Fields to update as JSON string'),
1492
1504
  queryParams: z.string().optional().describe('Optional query params as JSON object string for route contracts that intentionally keep workflow fields out of the validated body.'),
1505
+ globalRulesAckKey: globalRulesAckParam(z),
1493
1506
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1494
1507
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and data contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
1495
- }, async ({ tableName, id, data, queryParams, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1508
+ }, async ({ tableName, id, data, queryParams, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1509
+ assertGlobalRulesAck(globalRulesAckKey);
1496
1510
  validateTableName(tableName);
1497
1511
  assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
1498
1512
  const prepared = await prepareGenericMutation(tableName, data);
@@ -1548,9 +1562,10 @@ server.tool(
1548
1562
  expectedSourceSha256: z.string().optional().describe('Optional SHA-256 from get_script_source; fails if source changed.'),
1549
1563
  scriptLanguage: z.string().optional().describe('Script language to save. Defaults to existing scriptLanguage or javascript.'),
1550
1564
  apply: z.boolean().optional().default(false).describe('false returns preview only; true validates and saves.'),
1565
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when apply=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1551
1566
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply=true. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1552
1567
  },
1553
- async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply, knowledgeAckKey }) => {
1568
+ async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply, globalRulesAckKey, knowledgeAckKey }) => {
1554
1569
  const { record, sourceField, sourceCode } = await fetchScriptRecord(tableName, id);
1555
1570
  if (sourceField !== 'sourceCode') {
1556
1571
  throw new Error(`patch_script_source only saves sourceCode records. Record uses "${sourceField}"; use update_record intentionally for this legacy field.`);
@@ -1581,6 +1596,7 @@ server.tool(
1581
1596
  if (!apply) {
1582
1597
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
1583
1598
  }
1599
+ assertGlobalRulesAck(globalRulesAckKey);
1584
1600
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1585
1601
  const language = scriptLanguage || record.scriptLanguage || 'javascript';
1586
1602
  const prepared = await prepareGenericMutation(
@@ -1623,9 +1639,11 @@ server.tool(
1623
1639
  id: z.string().describe('Record ID to update'),
1624
1640
  sourceCode: z.string().describe('Editable script sourceCode. Pass the raw code string; do not JSON-escape it yourself.'),
1625
1641
  scriptLanguage: z.string().optional().default('javascript').describe('Script language, usually javascript or typescript'),
1642
+ globalRulesAckKey: globalRulesAckParam(z),
1626
1643
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1627
1644
  },
1628
- async ({ tableName, id, sourceCode, scriptLanguage, knowledgeAckKey }) => {
1645
+ async ({ tableName, id, sourceCode, scriptLanguage, globalRulesAckKey, knowledgeAckKey }) => {
1646
+ assertGlobalRulesAck(globalRulesAckKey);
1629
1647
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1630
1648
  validateTableName(tableName);
1631
1649
  const prepared = await prepareGenericMutation(
@@ -1652,7 +1670,8 @@ server.tool('delete_record', 'Delete a record by ID', {
1652
1670
  id: z.string().describe('Record ID to delete'),
1653
1671
  queryParams: z.string().optional().describe('Optional query params as JSON object string for route-specific confirmation contracts.'),
1654
1672
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
1655
- }, async ({ tableName, id, queryParams, confirm }) => {
1673
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1674
+ }, async ({ tableName, id, queryParams, confirm, globalRulesAckKey }) => {
1656
1675
  validateTableName(tableName);
1657
1676
  const primaryKey = await getPrimaryFieldName(tableName);
1658
1677
  if (!confirm) {
@@ -1673,6 +1692,7 @@ server.tool('delete_record', 'Delete a record by ID', {
1673
1692
  next: 'Call delete_record again with confirm=true to delete this route-backed record.',
1674
1693
  }, null, 2) }] };
1675
1694
  }
1695
+ assertGlobalRulesAck(globalRulesAckKey);
1676
1696
  const query = parseQueryParamsArg(queryParams);
1677
1697
  const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${id}`, query), { method: 'DELETE' });
1678
1698
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -1713,8 +1733,10 @@ server.tool(
1713
1733
  buttonColor: z.string().describe('Badge background color as full hex, e.g. #dbeafe.'),
1714
1734
  textColor: z.string().describe('Badge text color as full hex, e.g. #1d4ed8.'),
1715
1735
  isSystem: z.boolean().optional().default(false).describe('Set true only for built-in/runtime-owned methods. Normal app methods should leave this false.'),
1736
+ globalRulesAckKey: globalRulesAckParam(z),
1716
1737
  },
1717
- async ({ method, buttonColor, textColor, isSystem }) => {
1738
+ async ({ method, buttonColor, textColor, isSystem, globalRulesAckKey }) => {
1739
+ assertGlobalRulesAck(globalRulesAckKey);
1718
1740
  const normalizedMethod = normalizeMethodNameInput(method);
1719
1741
  const existing = await findMethodRecordByName(normalizedMethod);
1720
1742
  if (existing) {
@@ -1747,8 +1769,10 @@ server.tool(
1747
1769
  method: z.string().optional().describe('Existing method name to find, or new name when id is provided.'),
1748
1770
  buttonColor: z.string().optional().describe('Badge background color as full hex, e.g. #dbeafe.'),
1749
1771
  textColor: z.string().optional().describe('Badge text color as full hex, e.g. #1d4ed8.'),
1772
+ globalRulesAckKey: globalRulesAckParam(z),
1750
1773
  },
1751
- async ({ id, method, buttonColor, textColor }) => {
1774
+ async ({ id, method, buttonColor, textColor, globalRulesAckKey }) => {
1775
+ assertGlobalRulesAck(globalRulesAckKey);
1752
1776
  let targetId = id;
1753
1777
  let existing = null;
1754
1778
  if (!targetId) {
@@ -1793,8 +1817,9 @@ server.tool(
1793
1817
  id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
1794
1818
  method: z.string().optional().describe('Method name to find when id is omitted.'),
1795
1819
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
1820
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1796
1821
  },
1797
- async ({ id, method, confirm }) => {
1822
+ async ({ id, method, confirm, globalRulesAckKey }) => {
1798
1823
  let targetId = id;
1799
1824
  let target = null;
1800
1825
  if (!targetId) {
@@ -1820,6 +1845,7 @@ server.tool(
1820
1845
  next: 'Call delete_method again with confirm=true to delete.',
1821
1846
  }, null, 2) }] };
1822
1847
  }
1848
+ assertGlobalRulesAck(globalRulesAckKey);
1823
1849
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method/${encodeURIComponent(String(targetId))}`, { method: 'DELETE' });
1824
1850
  _methodMap = null;
1825
1851
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -2091,16 +2117,11 @@ server.tool(
2091
2117
  tableName: z.string().describe('Table name or alias to inspect'),
2092
2118
  },
2093
2119
  async ({ tableName }) => {
2094
- let state = await collectRestDefinitionState();
2095
- let table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2120
+ const state = await collectRestDefinitionState();
2121
+ const table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2096
2122
  if (!table) {
2097
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' }).catch(() => {});
2098
- await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
2099
- await new Promise((resolve) => setTimeout(resolve, 150));
2100
- state = await collectRestDefinitionState();
2101
- table = state.tables.find((item) => item?.name === tableName || item?.alias === tableName);
2123
+ throw new Error(`Unknown table "${tableName}". Use get_all_tables({ search, limit }) or get_all_metadata({ search, all: true }) to confirm the table name. If a just-created table is missing, verify the create response/reload event before calling manual reload tools.`);
2102
2124
  }
2103
- if (!table) throw new Error(`Unknown table "${tableName}"`);
2104
2125
  const tableId = getId(table);
2105
2126
  const columnIds = new Set((table.columns || []).map((column) => String(getId(column))));
2106
2127
  const relationIds = new Set((table.relations || []).map((relation) => String(getId(relation))));
@@ -2402,7 +2423,7 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2402
2423
  const queryParams = new URLSearchParams({
2403
2424
  filter: JSON.stringify(filter),
2404
2425
  fields: 'id,path,mainTable.name,availableMethods.*,publicMethods.*,isEnabled',
2405
- limit: '1000',
2426
+ limit: all ? '0' : '1000',
2406
2427
  });
2407
2428
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_route?${queryParams.toString()}`);
2408
2429
  const q = search ? search.toLowerCase() : null;
@@ -2421,6 +2442,8 @@ server.tool('get_all_routes', 'List route definitions with minimal fields. Every
2421
2442
  matchedRouteCount: matchedRoutes.length,
2422
2443
  returnedRouteCount: Math.min(matchedRoutes.length, routeLimit),
2423
2444
  all: !!all,
2445
+ complete: all || routeLimit >= matchedRoutes.length,
2446
+ hardCap: all ? null : routeLimit,
2424
2447
  search: search || null,
2425
2448
  routes: matchedRoutes.slice(0, routeLimit),
2426
2449
  detailHint: matchedRoutes.length > routeLimit
@@ -2449,8 +2472,10 @@ server.tool(
2449
2472
  .describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
2450
2473
  isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
2451
2474
  description: z.string().optional().describe('Route description'),
2475
+ globalRulesAckKey: globalRulesAckParam(z),
2452
2476
  },
2453
- async ({ path: routePath, mainTableId, methods, publicMethods, isEnabled, description }) => {
2477
+ async ({ path: routePath, mainTableId, methods, publicMethods, isEnabled, description, globalRulesAckKey }) => {
2478
+ assertGlobalRulesAck(globalRulesAckKey);
2454
2479
  const methodMap = await getMethodMap();
2455
2480
  const normalizedPath = normalizeRestPath(routePath);
2456
2481
 
@@ -2513,9 +2538,11 @@ server.tool(
2513
2538
  sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
2514
2539
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
2515
2540
  timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
2541
+ globalRulesAckKey: globalRulesAckParam(z),
2516
2542
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2517
2543
  },
2518
- async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, knowledgeAckKey }) => {
2544
+ async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, globalRulesAckKey, knowledgeAckKey }) => {
2545
+ assertGlobalRulesAck(globalRulesAckKey);
2519
2546
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2520
2547
  const methodNames = methods && methods.length > 0 ? methods : method ? [method] : [];
2521
2548
  if (methodNames.length === 0) throw new Error('Provide method or methods');
@@ -2576,9 +2603,11 @@ server.tool(
2576
2603
  .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
2577
2604
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
2578
2605
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
2606
+ globalRulesAckKey: globalRulesAckParam(z),
2579
2607
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2580
2608
  },
2581
- async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
2609
+ async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, globalRulesAckKey, knowledgeAckKey }) => {
2610
+ assertGlobalRulesAck(globalRulesAckKey);
2582
2611
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2583
2612
  const methodMap = await getMethodMap();
2584
2613
  const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
@@ -2632,9 +2661,11 @@ server.tool(
2632
2661
  .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
2633
2662
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
2634
2663
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
2664
+ globalRulesAckKey: globalRulesAckParam(z),
2635
2665
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2636
2666
  },
2637
- async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
2667
+ async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, globalRulesAckKey, knowledgeAckKey }) => {
2668
+ assertGlobalRulesAck(globalRulesAckKey);
2638
2669
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2639
2670
  const methodMap = await getMethodMap();
2640
2671
  const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
@@ -2751,8 +2782,10 @@ server.tool(
2751
2782
  mode: z.enum(['merge', 'replace']).optional().default('merge').describe('merge adds methods to an existing permission; replace overwrites methods on the matched permission.'),
2752
2783
  description: z.string().optional().describe('Admin note'),
2753
2784
  isEnabled: z.boolean().optional().default(true).describe('Enable the permission'),
2785
+ globalRulesAckKey: globalRulesAckParam(z),
2754
2786
  },
2755
- async ({ path, routeId, methods, roleId, roleName, allowedUserIds, mode, description, isEnabled }) => {
2787
+ async ({ path, routeId, methods, roleId, roleName, allowedUserIds, mode, description, isEnabled, globalRulesAckKey }) => {
2788
+ assertGlobalRulesAck(globalRulesAckKey);
2756
2789
  if (!path && !routeId) throw new Error('Provide path or routeId.');
2757
2790
  if (path && routeId) throw new Error('Provide path or routeId, not both.');
2758
2791
  if (roleId && roleName) throw new Error('Provide roleId or roleName, not both.');
@@ -2852,24 +2885,36 @@ registerPlatformOperationTools(server, ENFYRA_API_URL);
2852
2885
  // CACHE & SYSTEM TOOLS
2853
2886
  // ============================================================================
2854
2887
 
2855
- server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {}, async () => {
2888
+ server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {
2889
+ globalRulesAckKey: globalRulesAckParam(z),
2890
+ }, async ({ globalRulesAckKey }) => {
2891
+ assertGlobalRulesAck(globalRulesAckKey);
2856
2892
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
2857
- return { content: [{ type: 'text', text: `System reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2893
+ return jsonContent({ action: 'reloaded_all', result });
2858
2894
  });
2859
2895
 
2860
- server.tool('reload_metadata', 'Reload metadata cache only', {}, async () => {
2896
+ server.tool('reload_metadata', 'Reload metadata cache only', {
2897
+ globalRulesAckKey: globalRulesAckParam(z),
2898
+ }, async ({ globalRulesAckKey }) => {
2899
+ assertGlobalRulesAck(globalRulesAckKey);
2861
2900
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
2862
- return { content: [{ type: 'text', text: `Metadata reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2901
+ return jsonContent({ action: 'reloaded_metadata', result });
2863
2902
  });
2864
2903
 
2865
- server.tool('reload_routes', 'Reload routes cache only', {}, async () => {
2904
+ server.tool('reload_routes', 'Reload routes cache only', {
2905
+ globalRulesAckKey: globalRulesAckParam(z),
2906
+ }, async ({ globalRulesAckKey }) => {
2907
+ assertGlobalRulesAck(globalRulesAckKey);
2866
2908
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
2867
- return { content: [{ type: 'text', text: `Routes reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2909
+ return jsonContent({ action: 'reloaded_routes', result });
2868
2910
  });
2869
2911
 
2870
- server.tool('reload_graphql', 'Reload GraphQL schema', {}, async () => {
2912
+ server.tool('reload_graphql', 'Reload GraphQL schema', {
2913
+ globalRulesAckKey: globalRulesAckParam(z),
2914
+ }, async ({ globalRulesAckKey }) => {
2915
+ assertGlobalRulesAck(globalRulesAckKey);
2871
2916
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
2872
- return { content: [{ type: 'text', text: `GraphQL reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2917
+ return jsonContent({ action: 'reloaded_graphql', result });
2873
2918
  });
2874
2919
 
2875
2920
  // ============================================================================
@@ -3015,8 +3060,10 @@ server.tool(
3015
3060
  name: z.string().describe('Exact NPM package name (e.g., "node-ssh", "axios")'),
3016
3061
  type: z.enum(['Server', 'App']).default('Server').describe('Where to install: Server (handlers/hooks) or App (extensions)'),
3017
3062
  version: z.string().optional().describe('Specific version. If omitted, fetches latest from NPM.'),
3063
+ globalRulesAckKey: globalRulesAckParam(z),
3018
3064
  },
3019
- async ({ name, type, version }) => {
3065
+ async ({ name, type, version, globalRulesAckKey }) => {
3066
+ assertGlobalRulesAck(globalRulesAckKey);
3020
3067
  // Step 1: Get package info from NPM if version not specified
3021
3068
  let pkgVersion = version;
3022
3069
  let pkgDescription = '';
@@ -3038,12 +3085,15 @@ server.tool(
3038
3085
  const checkFilter = JSON.stringify({ name: { _eq: name }, type: { _eq: type } });
3039
3086
  const existing = await fetchAPI(ENFYRA_API_URL, `/enfyra_package?filter=${encodeURIComponent(checkFilter)}&limit=1`);
3040
3087
  if (existing.data && existing.data.length > 0) {
3041
- return {
3042
- content: [{
3043
- type: 'text',
3044
- text: `Package "${name}" is already installed (version: ${existing.data[0].version}, type: ${existing.data[0].type}).\n${JSON.stringify(existing.data[0], null, 2)}`,
3045
- }],
3046
- };
3088
+ return jsonContent({
3089
+ action: 'package_already_installed',
3090
+ package: {
3091
+ name,
3092
+ version: existing.data[0].version,
3093
+ type: existing.data[0].type,
3094
+ },
3095
+ record: existing.data[0],
3096
+ });
3047
3097
  }
3048
3098
 
3049
3099
  // Step 3: Get current user for installedBy
@@ -3065,12 +3115,11 @@ server.tool(
3065
3115
  body: JSON.stringify(body),
3066
3116
  });
3067
3117
 
3068
- return {
3069
- content: [{
3070
- type: 'text',
3071
- text: `Package "${name}@${pkgVersion}" installed successfully (type: ${type}).\n${JSON.stringify(result, null, 2)}`,
3072
- }],
3073
- };
3118
+ return jsonContent({
3119
+ action: 'package_installed',
3120
+ package: { name, version: pkgVersion, type },
3121
+ result,
3122
+ });
3074
3123
  },
3075
3124
  );
3076
3125