@enfyra/mcp-server 0.1.5 → 0.1.6

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
@@ -244,14 +244,14 @@ Use `get_enfyra_examples` from the MCP tool list when asking an LLM to generate
244
244
  - files and storage
245
245
  - Enfyra admin extensions
246
246
 
247
- Use `get_enfyra_required_knowledge` before asking an LLM to save dynamic server code or Enfyra extension code. It returns short required contracts plus acknowledgement keys that code-writing tools verify before saving.
247
+ Use `get_enfyra_required_knowledge` before asking an LLM to mutate metadata, schema, routes, permissions, menus, packages, cache state, dynamic server code, or Enfyra extension code. It returns global rules plus acknowledgement keys that write tools verify before saving. Dynamic server code also requires the dynamic-code acknowledgement key, and extension code also requires the extension acknowledgement key.
248
248
 
249
249
  ## Runtime Safety
250
250
 
251
251
  The MCP server includes safety guards for LLM callers:
252
252
 
253
253
  - Generic record mutations validate fields against live metadata.
254
- - Code-writing tools require `get_enfyra_required_knowledge` acknowledgement before saving dynamic scripts or extension code. Discovery, validation, and preview tools remain available without the acknowledgement so agents can read and plan first.
254
+ - Write tools require `get_enfyra_required_knowledge` acknowledgement before mutating Enfyra state. Discovery, validation, and preview tools remain available without the acknowledgement so agents can read and plan first. If the acknowledgement is missing, the tool error tells the caller to read `get_enfyra_required_knowledge` and pass the required key.
255
255
  - Script-backed records validate `sourceCode` through `/admin/script/validate` before saving.
256
256
  - `validate_dynamic_script` checks handler, hook, flow, websocket, GraphQL, and bootstrap script source without saving.
257
257
  - `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -551,6 +551,7 @@ update_record({
551
551
  name: 'Create a chat conversation table',
552
552
  code: `create_table({
553
553
  name: "chat_conversation",
554
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
554
555
  columns: JSON.stringify([
555
556
  { name: "kind", type: "varchar", isNullable: false, defaultValue: "dm" },
556
557
  { name: "title", type: "varchar", isNullable: true },
@@ -568,6 +569,7 @@ update_record({
568
569
  name: 'Create relations directly to enfyra_user',
569
570
  code: `create_table({
570
571
  name: "chat_message",
572
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
571
573
  columns: JSON.stringify([
572
574
  { name: "text", type: "text", isNullable: false },
573
575
  { name: "persistStatus", type: "varchar", defaultValue: "persisted" }
@@ -607,6 +609,7 @@ update_record({
607
609
  code: `create_relation({
608
610
  sourceTableId: "chat_message",
609
611
  targetTableId: "chat_conversation",
612
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
610
613
  propertyName: "conversation",
611
614
  inversePropertyName: "messages",
612
615
  type: "many-to-one",
@@ -625,6 +628,7 @@ update_record({
625
628
  name: 'Add chat_conversation.lastMessage after chat_message exists',
626
629
  code: `update_table({
627
630
  tableId: "<chat_conversation_id>",
631
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
628
632
  relations: JSON.stringify([
629
633
  {
630
634
  propertyName: "createdBy",
@@ -651,6 +655,7 @@ update_record({
651
655
  name: 'Unread/read table with unique and indexes',
652
656
  code: `create_table({
653
657
  name: "chat_message_read",
658
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
654
659
  columns: JSON.stringify([
655
660
  { name: "isRead", type: "boolean", defaultValue: false },
656
661
  { name: "readAt", type: "datetime", isNullable: true }
@@ -677,6 +682,7 @@ update_record({
677
682
  code: `create_column({
678
683
  tableId: "<enfyra_user_table_id>",
679
684
  name: "emailVerifiedAt",
685
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
680
686
  type: "datetime",
681
687
  isNullable: true,
682
688
  isPublished: true,
@@ -686,6 +692,7 @@ update_record({
686
692
  create_column({
687
693
  tableId: "<enfyra_user_table_id>",
688
694
  name: "emailVerificationStatus",
695
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
689
696
  type: "varchar",
690
697
  isNullable: false,
691
698
  defaultValue: "pending",
@@ -696,6 +703,7 @@ create_column({
696
703
  create_column({
697
704
  tableId: "<integration_secret_table_id>",
698
705
  name: "value",
706
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
699
707
  type: "text",
700
708
  isNullable: false,
701
709
  isPublished: false,
@@ -722,6 +730,7 @@ create_column({
722
730
  create_column({
723
731
  tableId: "<table_id>",
724
732
  name: "api_secret",
733
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
725
734
  type: "text",
726
735
  isPublished: false,
727
736
  isEncrypted: true
@@ -923,6 +932,7 @@ GET /enfyra/integrations?filter={"api_token":{"_eq":"plaintext-token"}}
923
932
  create_column({
924
933
  tableId: "<integrations_table_id>",
925
934
  name: "api_token_lookup_sha256",
935
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
926
936
  type: "varchar",
927
937
  isNullable: false,
928
938
  isPublished: false
@@ -958,6 +968,7 @@ const found = await #integrations.find({
958
968
  routeId: "<route_id>",
959
969
  method: "POST",
960
970
  scriptLanguage: "javascript",
971
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
961
972
  knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
962
973
  sourceCode: \`const email = @BODY.email
963
974
  if (!email) @THROW400("Email is required")
@@ -966,7 +977,7 @@ return { ok: true, email }\`
966
977
  })`,
967
978
  notes: [
968
979
  'Use sourceCode, not logic. The server generates compiledCode.',
969
- 'Call get_enfyra_required_knowledge before saving dynamic code and pass dynamicCodeAckKey as knowledgeAckKey.',
980
+ 'Call get_enfyra_required_knowledge before saving dynamic code, pass globalRulesAckKey as globalRulesAckKey, and pass dynamicCodeAckKey as knowledgeAckKey.',
970
981
  'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
971
982
  'Do not pass name to enfyra_route_handler; one handler is identified by route + method.',
972
983
  ],
@@ -1020,6 +1031,7 @@ const scope = {
1020
1031
  name: 'Encrypted field table definition',
1021
1032
  code: `create_table({
1022
1033
  name: "integrations",
1034
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1023
1035
  columns: JSON.stringify([
1024
1036
  { name: "name", type: "varchar", isNullable: false },
1025
1037
  {
@@ -1047,6 +1059,7 @@ const scope = {
1047
1059
  name: "strip_email_verification_fields",
1048
1060
  methods: ["PATCH"],
1049
1061
  priority: -10,
1062
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1050
1063
  knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
1051
1064
  code: \`delete @BODY.emailVerifiedAt
1052
1065
  delete @BODY.emailVerificationStatus
@@ -1065,6 +1078,7 @@ delete @BODY.emailVerificationSentAt\`
1065
1078
  name: "shape_display_title",
1066
1079
  methods: ["GET"],
1067
1080
  priority: 0,
1081
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1068
1082
  knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
1069
1083
  code: \`if (@ERROR) {
1070
1084
  @LOGS("Request failed", @ERROR.message)
@@ -1558,6 +1572,7 @@ update_method({
1558
1572
  icon: "lucide:bar-chart-3",
1559
1573
  order: 20,
1560
1574
  isEnabled: true,
1575
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1561
1576
  permission: JSON.stringify({
1562
1577
  or: [
1563
1578
  { route: "/reports", methods: ["GET"] },
@@ -1573,6 +1588,7 @@ ensure_page_extension({
1573
1588
  menuId: "<created-menu-id>",
1574
1589
  code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\\"><article class=\\"eapp-surface-card p-4\\"><div class=\\"flex items-start justify-between gap-3\\"><div><p class=\\"text-sm font-medium eapp-text-tertiary\\">Total</p><p class=\\"mt-2 text-2xl font-semibold eapp-text-primary\\">0</p></div><span class=\\"eapp-primary-soft eapp-icon-tile\\"><span class=\\"eapp-primary-text\\">◆</span></span></div><div class=\\"mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted\\"><div class=\\"eapp-primary-solid h-full w-1/2\\"></div></div></article><article class=\\"eapp-primary-surface eapp-radius-panel border p-4\\"><p class=\\"text-sm font-semibold eapp-text-primary\\">Selected report</p><p class=\\"mt-1 text-sm eapp-text-tertiary\\">Only selected/current identity blocks use identity surface.</p></article></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); const { register: registerHeaderActions } = useHeaderActionRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); registerHeaderActions([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', color: 'neutral', variant: 'outline', onClick: () => {}, order: 80 }])</script>",
1575
1590
  isEnabled: true,
1591
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1576
1592
  extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1577
1593
  })`,
1578
1594
  notes: [
@@ -1582,7 +1598,7 @@ ensure_page_extension({
1582
1598
  'Sensitive admin menus should include a permission condition at creation time.',
1583
1599
  'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
1584
1600
  'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
1585
- 'Call get_enfyra_required_knowledge before saving extension code and pass extensionAckKey as extensionKnowledgeAckKey.',
1601
+ 'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
1586
1602
  'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
1587
1603
  'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
1588
1604
  'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
@@ -1648,6 +1664,7 @@ ensure_widget_extension({
1648
1664
  description: "Report status summary cards",
1649
1665
  code: reportStatusWidgetCode,
1650
1666
  isEnabled: true,
1667
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1651
1668
  extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1652
1669
  })
1653
1670
 
@@ -1657,6 +1674,7 @@ ensure_page_extension({
1657
1674
  menuId: "<reports-menu-id>",
1658
1675
  code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
1659
1676
  isEnabled: true,
1677
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1660
1678
  extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1661
1679
  })`,
1662
1680
  notes: [
@@ -1751,6 +1769,7 @@ ensure_global_extension({
1751
1769
  description: "Registers the app-wide notification bell in the account panel",
1752
1770
  code: notificationBellCode,
1753
1771
  isEnabled: true,
1772
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1754
1773
  extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1755
1774
  })`,
1756
1775
  notes: [
@@ -1909,6 +1928,7 @@ ensure_global_extension({
1909
1928
  description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
1910
1929
  code: signalBridgeCode,
1911
1930
  isEnabled: true,
1931
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
1912
1932
  extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
1913
1933
  })`,
1914
1934
  notes: [
@@ -2166,7 +2186,8 @@ console.log(ok, requiredTerms.has('terms'), loaded, label, date)
2166
2186
  name: 'Install and use an app package in an extension',
2167
2187
  code: `install_package({
2168
2188
  name: "dayjs",
2169
- type: "App"
2189
+ type: "App",
2190
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>"
2170
2191
  })
2171
2192
 
2172
2193
  // Then in extension code:
@@ -29,7 +29,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
29
29
  '- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
30
30
  '- Load examples only when needed. Use `get_enfyra_examples` by category. Before extension UI, call `get_extension_theme_contract`; call `get_theme_class_reference` for exact eapp/Nuxt UI theme classes.',
31
31
  '- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
32
- '- Before writing dynamic server or extension code, call `get_enfyra_required_knowledge` and pass the matching acknowledgement key into write tools.',
32
+ '- Before mutating metadata, schema, routes, permissions, menus, packages, cache state, dynamic code, or extension UI, call `get_enfyra_required_knowledge`, read the global rules, and pass `globalRulesAckKey` into write tools. Dynamic server code also requires `dynamicCodeAckKey`; extension code also requires `extensionAckKey`.',
33
33
  '- With non-root API tokens, call `get_permission_profile` before relying on admin helper tools or when debugging 403s. MCP admin helpers require ordinary route permissions for static admin routes such as `/admin/script/validate`, `/admin/test/run`, `/admin/flow/trigger/:id`, and `/admin/reload/*`.',
34
34
  '- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow`, `create_api_endpoint`, `enable_route`, `add_route_methods`, `public_route_methods`, `set_table_graphql`, guard/permission/rule tools, websocket tools, flow tools, and `ensure_page_extension`/menu tools.',
35
35
  '- Before saving standalone dynamic script or extension code, call `validate_dynamic_script` or `validate_extension_code` unless the chosen ensure/update tool already validates the code.',
@@ -6,8 +6,10 @@ import {
6
6
  assertDynamicCodeKnowledgeAck,
7
7
  assertDynamicCodeKnowledgeAckIf,
8
8
  assertExtensionKnowledgeAck,
9
+ assertGlobalRulesAck,
9
10
  dynamicCodeKnowledgeAckParam,
10
11
  extensionKnowledgeAckParam,
12
+ globalRulesAckParam,
11
13
  } from './required-knowledge.js';
12
14
 
13
15
  function unwrapData(result) {
@@ -111,7 +113,8 @@ async function resolveRoute(apiUrl, { path, routeId }) {
111
113
  return { route, routes, path: route.path };
112
114
  }
113
115
 
114
- async function updateRouteMethods(apiUrl, { path, routeId, methods, mode, isEnabled }) {
116
+ async function updateRouteMethods(apiUrl, { path, routeId, methods, mode, isEnabled, globalRulesAckKey }) {
117
+ assertGlobalRulesAck(globalRulesAckKey);
115
118
  const [{ route }, { methodMap, methodIdNameMap }] = await Promise.all([
116
119
  resolveRoute(apiUrl, { path, routeId }),
117
120
  getMethodContext(apiUrl),
@@ -141,7 +144,8 @@ async function updateRouteMethods(apiUrl, { path, routeId, methods, mode, isEnab
141
144
  };
142
145
  }
143
146
 
144
- async function updateRoutePublicMethods(apiUrl, { path, routeId, methods, mode }) {
147
+ async function updateRoutePublicMethods(apiUrl, { path, routeId, methods, mode, globalRulesAckKey }) {
148
+ assertGlobalRulesAck(globalRulesAckKey);
145
149
  const [{ route }, { methodMap, methodIdNameMap }] = await Promise.all([
146
150
  resolveRoute(apiUrl, { path, routeId }),
147
151
  getMethodContext(apiUrl),
@@ -171,7 +175,8 @@ async function updateRoutePublicMethods(apiUrl, { path, routeId, methods, mode }
171
175
  };
172
176
  }
173
177
 
174
- async function setRouteEnabled(apiUrl, { path, routeId, isEnabled }) {
178
+ async function setRouteEnabled(apiUrl, { path, routeId, isEnabled, globalRulesAckKey }) {
179
+ assertGlobalRulesAck(globalRulesAckKey);
175
180
  const { route } = await resolveRoute(apiUrl, { path, routeId });
176
181
  const before = route?.isEnabled !== false;
177
182
  if (before === isEnabled) {
@@ -235,7 +240,7 @@ async function deleteRows(apiUrl, tableName, rows) {
235
240
  return deleted;
236
241
  }
237
242
 
238
- async function deleteRoute(apiUrl, { path, routeId, expectedPath, confirm }) {
243
+ async function deleteRoute(apiUrl, { path, routeId, expectedPath, confirm, globalRulesAckKey }) {
239
244
  const { route } = await resolveRoute(apiUrl, { path, routeId });
240
245
  if (expectedPath && route.path !== normalizeRestPath(expectedPath)) {
241
246
  throw new Error(`Route path mismatch: resolved ${route.path}, expected ${normalizeRestPath(expectedPath)}.`);
@@ -255,6 +260,7 @@ async function deleteRoute(apiUrl, { path, routeId, expectedPath, confirm }) {
255
260
  next: 'Call delete_route again with confirm=true and expectedPath set to this route path to delete the route and related handlers/hooks/permissions/guards.',
256
261
  };
257
262
  }
263
+ assertGlobalRulesAck(globalRulesAckKey);
258
264
 
259
265
  await deleteRows(apiUrl, 'enfyra_route_handler', dependencies.handlers);
260
266
  await deleteRows(apiUrl, 'enfyra_pre_hook', dependencies.preHooks);
@@ -641,7 +647,9 @@ async function ensureMenu(apiUrl, {
641
647
  permission,
642
648
  description,
643
649
  isEnabled = true,
650
+ globalRulesAckKey,
644
651
  }) {
652
+ assertGlobalRulesAck(globalRulesAckKey);
645
653
  const normalizedPath = path ? normalizeRestPath(path) : undefined;
646
654
  const existing = normalizedPath
647
655
  ? await findRecord(apiUrl, 'enfyra_menu', { path: { _eq: normalizedPath } }, 'id,_id,path,label')
@@ -673,8 +681,10 @@ async function ensureExtension(apiUrl, {
673
681
  description,
674
682
  isEnabled = true,
675
683
  version = '1.0.0',
684
+ globalRulesAckKey,
676
685
  extensionKnowledgeAckKey,
677
686
  }) {
687
+ assertGlobalRulesAck(globalRulesAckKey);
678
688
  assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
679
689
  if (type === 'page' && !menuId) {
680
690
  throw new Error('menuId is required for page extensions. Use ensure_menu first, then ensure_page_extension.');
@@ -711,7 +721,9 @@ async function ensureFlow(apiUrl, {
711
721
  maxExecutions = 100,
712
722
  isEnabled = true,
713
723
  description,
724
+ globalRulesAckKey,
714
725
  }) {
726
+ assertGlobalRulesAck(globalRulesAckKey);
715
727
  const existing = await findRecord(apiUrl, 'enfyra_flow', { name: { _eq: name } }, 'id,_id,name');
716
728
  const operation = await createOrPatch(apiUrl, 'enfyra_flow', existing, {
717
729
  name,
@@ -737,8 +749,10 @@ async function ensureFlowStep(apiUrl, {
737
749
  scriptLanguage,
738
750
  timeout,
739
751
  isEnabled,
752
+ globalRulesAckKey,
740
753
  knowledgeAckKey,
741
754
  }) {
755
+ assertGlobalRulesAck(globalRulesAckKey);
742
756
  if (!flowName && !flowId) throw new Error('Provide flowName or flowId.');
743
757
  if (flowName && flowId) throw new Error('Provide flowName or flowId, not both.');
744
758
  const flow = flowId
@@ -1124,6 +1138,7 @@ async function runApiEndpointWorkflow(apiUrl, opts) {
1124
1138
  const operations = [];
1125
1139
  let completedEphemeralStepId = null;
1126
1140
  if (opts.apply || opts.applyAll) {
1141
+ assertGlobalRulesAck(opts.globalRulesAckKey);
1127
1142
  if (opts.applyAll && state.steps.some((item) => item.id === 'save_handler' && ['pending', 'waiting'].includes(item.status))) {
1128
1143
  assertDynamicCodeKnowledgeAck(opts.knowledgeAckKey);
1129
1144
  }
@@ -1227,8 +1242,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1227
1242
  {
1228
1243
  tableName: z.string().describe('Table name, alias, or id.'),
1229
1244
  isEnabled: z.boolean().describe('Desired GraphQL enabled state for the table.'),
1245
+ globalRulesAckKey: globalRulesAckParam(z),
1230
1246
  },
1231
- async ({ tableName, isEnabled }) => {
1247
+ async ({ tableName, isEnabled, globalRulesAckKey }) => {
1248
+ assertGlobalRulesAck(globalRulesAckKey);
1232
1249
  const table = resolveTable(await getMetadataTables(ENFYRA_API_URL), tableName);
1233
1250
  const existing = await findRecord(ENFYRA_API_URL, 'enfyra_graphql', { table: { id: { _eq: getId(table) } } }, 'id,_id,table.id,isEnabled');
1234
1251
  const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_graphql', existing, {
@@ -1254,13 +1271,15 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1254
1271
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1255
1272
  methods: z.array(z.string()).min(1).describe('HTTP method names to add.'),
1256
1273
  isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
1274
+ globalRulesAckKey: globalRulesAckParam(z),
1257
1275
  },
1258
- async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1276
+ async ({ path, routeId, methods, isEnabled, globalRulesAckKey }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1259
1277
  path,
1260
1278
  routeId,
1261
1279
  methods,
1262
1280
  mode: 'merge',
1263
1281
  isEnabled,
1282
+ globalRulesAckKey,
1264
1283
  })),
1265
1284
  );
1266
1285
 
@@ -1272,13 +1291,15 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1272
1291
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1273
1292
  methods: z.array(z.string()).min(1).describe('Exact HTTP method names for availableMethods.'),
1274
1293
  isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
1294
+ globalRulesAckKey: globalRulesAckParam(z),
1275
1295
  },
1276
- async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1296
+ async ({ path, routeId, methods, isEnabled, globalRulesAckKey }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1277
1297
  path,
1278
1298
  routeId,
1279
1299
  methods,
1280
1300
  mode: 'replace',
1281
1301
  isEnabled,
1302
+ globalRulesAckKey,
1282
1303
  })),
1283
1304
  );
1284
1305
 
@@ -1290,13 +1311,15 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1290
1311
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1291
1312
  methods: z.array(z.string()).min(1).describe('HTTP method names to remove.'),
1292
1313
  isEnabled: z.boolean().optional().describe('Optionally enable/disable the route in the same safe patch.'),
1314
+ globalRulesAckKey: globalRulesAckParam(z),
1293
1315
  },
1294
- async ({ path, routeId, methods, isEnabled }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1316
+ async ({ path, routeId, methods, isEnabled, globalRulesAckKey }) => jsonText(await updateRouteMethods(ENFYRA_API_URL, {
1295
1317
  path,
1296
1318
  routeId,
1297
1319
  methods,
1298
1320
  mode: 'remove',
1299
1321
  isEnabled,
1322
+ globalRulesAckKey,
1300
1323
  })),
1301
1324
  );
1302
1325
 
@@ -1306,11 +1329,13 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1306
1329
  {
1307
1330
  path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
1308
1331
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1332
+ globalRulesAckKey: globalRulesAckParam(z),
1309
1333
  },
1310
- async ({ path, routeId }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
1334
+ async ({ path, routeId, globalRulesAckKey }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
1311
1335
  path,
1312
1336
  routeId,
1313
1337
  isEnabled: true,
1338
+ globalRulesAckKey,
1314
1339
  })),
1315
1340
  );
1316
1341
 
@@ -1320,11 +1345,13 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1320
1345
  {
1321
1346
  path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
1322
1347
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1348
+ globalRulesAckKey: globalRulesAckParam(z),
1323
1349
  },
1324
- async ({ path, routeId }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
1350
+ async ({ path, routeId, globalRulesAckKey }) => jsonText(await setRouteEnabled(ENFYRA_API_URL, {
1325
1351
  path,
1326
1352
  routeId,
1327
1353
  isEnabled: false,
1354
+ globalRulesAckKey,
1328
1355
  })),
1329
1356
  );
1330
1357
 
@@ -1336,6 +1363,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1336
1363
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1337
1364
  expectedPath: z.string().optional().describe('Optional safety check. When confirm=true, pass the path returned by the preview.'),
1338
1365
  confirm: z.boolean().optional().default(false).describe('false returns a dependency preview only; true deletes the route and related route-owned records.'),
1366
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1339
1367
  },
1340
1368
  async (input) => jsonText(await deleteRoute(ENFYRA_API_URL, input)),
1341
1369
  );
@@ -1347,12 +1375,14 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1347
1375
  path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
1348
1376
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1349
1377
  methods: z.array(z.string()).min(1).describe('HTTP method names to make public. They must already be available on the route.'),
1378
+ globalRulesAckKey: globalRulesAckParam(z),
1350
1379
  },
1351
- async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
1380
+ async ({ path, routeId, methods, globalRulesAckKey }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
1352
1381
  path,
1353
1382
  routeId,
1354
1383
  methods,
1355
1384
  mode: 'merge',
1385
+ globalRulesAckKey,
1356
1386
  })),
1357
1387
  );
1358
1388
 
@@ -1363,12 +1393,14 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1363
1393
  path: z.string().optional().describe('Route path, e.g. /sum. Use either path or routeId.'),
1364
1394
  routeId: z.union([z.string(), z.number()]).optional().describe('Route id. Use either path or routeId.'),
1365
1395
  methods: z.array(z.string()).min(1).describe('HTTP method names to remove from publicMethods.'),
1396
+ globalRulesAckKey: globalRulesAckParam(z),
1366
1397
  },
1367
- async ({ path, routeId, methods }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
1398
+ async ({ path, routeId, methods, globalRulesAckKey }) => jsonText(await updateRoutePublicMethods(ENFYRA_API_URL, {
1368
1399
  path,
1369
1400
  routeId,
1370
1401
  methods,
1371
1402
  mode: 'remove',
1403
+ globalRulesAckKey,
1372
1404
  })),
1373
1405
  );
1374
1406
 
@@ -1399,6 +1431,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1399
1431
  apply: z.boolean().optional().default(false).describe('false returns plan only; true applies exactly the next pending step.'),
1400
1432
  applyAll: z.boolean().optional().default(false).describe('true applies all safe pending steps in order. Prefer apply=true for production changes.'),
1401
1433
  stepId: z.string().optional().describe('Optional pending step id to apply. Omit to apply the next pending step.'),
1434
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when apply/applyAll mutates metadata. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1402
1435
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply/applyAll reaches the save_handler step. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1403
1436
  },
1404
1437
  async (input) => jsonText(await runApiEndpointWorkflow(ENFYRA_API_URL, input)),
@@ -1423,9 +1456,11 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1423
1456
  overwrite: z.boolean().optional().default(false).describe('If a handler already exists for route+method, false fails; true updates its sourceCode.'),
1424
1457
  smokeTestQuery: z.string().optional().describe('Optional query JSON object for a smoke test after save, e.g. {"a":"1","b":"2"}.'),
1425
1458
  smokeTestBody: z.string().optional().describe('Optional body JSON object for a smoke test after save.'),
1459
+ globalRulesAckKey: globalRulesAckParam(z),
1426
1460
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1427
1461
  },
1428
- async ({ path, method, sourceCode, scriptLanguage, public: makePublic, description, timeout, overwrite, smokeTestQuery, smokeTestBody, knowledgeAckKey }) => {
1462
+ async ({ path, method, sourceCode, scriptLanguage, public: makePublic, description, timeout, overwrite, smokeTestQuery, smokeTestBody, globalRulesAckKey, knowledgeAckKey }) => {
1463
+ assertGlobalRulesAck(globalRulesAckKey);
1429
1464
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1430
1465
  const normalizedPath = normalizeRestPath(path);
1431
1466
  const methodName = normalizeMethodName(method);
@@ -1733,9 +1768,11 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1733
1768
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for connection handler.'),
1734
1769
  isEnabled: z.boolean().optional().default(true).describe('Enable gateway.'),
1735
1770
  description: z.string().optional().describe('Admin note.'),
1771
+ globalRulesAckKey: globalRulesAckParam(z),
1736
1772
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when sourceCode is provided. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1737
1773
  },
1738
- async ({ path, sourceCode, scriptLanguage, isEnabled, description, knowledgeAckKey }) => {
1774
+ async ({ path, sourceCode, scriptLanguage, isEnabled, description, globalRulesAckKey, knowledgeAckKey }) => {
1775
+ assertGlobalRulesAck(globalRulesAckKey);
1739
1776
  assertDynamicCodeKnowledgeAckIf(sourceCode !== undefined, knowledgeAckKey);
1740
1777
  const normalizedPath = normalizeRestPath(path);
1741
1778
  const validation = sourceCode === undefined
@@ -1765,9 +1802,11 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1765
1802
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
1766
1803
  isEnabled: z.boolean().optional().default(true).describe('Enable event.'),
1767
1804
  description: z.string().optional().describe('Admin note.'),
1805
+ globalRulesAckKey: globalRulesAckParam(z),
1768
1806
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1769
1807
  },
1770
- async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description, knowledgeAckKey }) => {
1808
+ async ({ gatewayPath, gatewayId, eventName, sourceCode, scriptLanguage, isEnabled, description, globalRulesAckKey, knowledgeAckKey }) => {
1809
+ assertGlobalRulesAck(globalRulesAckKey);
1771
1810
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1772
1811
  if (!gatewayPath && !gatewayId) throw new Error('Provide gatewayPath or gatewayId.');
1773
1812
  if (gatewayPath && gatewayId) throw new Error('Provide gatewayPath or gatewayId, not both.');
@@ -1802,14 +1841,16 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1802
1841
  maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
1803
1842
  isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
1804
1843
  description: z.string().optional().describe('Admin note.'),
1844
+ globalRulesAckKey: globalRulesAckParam(z),
1805
1845
  },
1806
- async ({ name, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
1846
+ async ({ name, timeout, maxExecutions, isEnabled, description, globalRulesAckKey }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
1807
1847
  name,
1808
1848
  triggerType: 'manual',
1809
1849
  timeout,
1810
1850
  maxExecutions,
1811
1851
  isEnabled,
1812
1852
  description,
1853
+ globalRulesAckKey,
1813
1854
  })),
1814
1855
  );
1815
1856
 
@@ -1823,8 +1864,9 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1823
1864
  maxExecutions: z.number().int().positive().optional().default(100).describe('Execution history cap.'),
1824
1865
  isEnabled: z.boolean().optional().default(true).describe('Enable flow.'),
1825
1866
  description: z.string().optional().describe('Admin note.'),
1867
+ globalRulesAckKey: globalRulesAckParam(z),
1826
1868
  },
1827
- async ({ name, triggerConfig, timeout, maxExecutions, isEnabled, description }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
1869
+ async ({ name, triggerConfig, timeout, maxExecutions, isEnabled, description, globalRulesAckKey }) => jsonText(await ensureFlow(ENFYRA_API_URL, {
1828
1870
  name,
1829
1871
  triggerType: 'schedule',
1830
1872
  triggerConfig,
@@ -1832,6 +1874,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1832
1874
  maxExecutions,
1833
1875
  isEnabled,
1834
1876
  description,
1877
+ globalRulesAckKey,
1835
1878
  })),
1836
1879
  );
1837
1880
 
@@ -1870,6 +1913,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1870
1913
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
1871
1914
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1872
1915
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
1916
+ globalRulesAckKey: globalRulesAckParam(z),
1873
1917
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1874
1918
  },
1875
1919
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
@@ -1891,6 +1935,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1891
1935
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language.'),
1892
1936
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1893
1937
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
1938
+ globalRulesAckKey: globalRulesAckParam(z),
1894
1939
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1895
1940
  },
1896
1941
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
@@ -1910,6 +1955,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1910
1955
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
1911
1956
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1912
1957
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
1958
+ globalRulesAckKey: globalRulesAckParam(z),
1913
1959
  },
1914
1960
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
1915
1961
  ...input,
@@ -1928,6 +1974,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1928
1974
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
1929
1975
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1930
1976
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
1977
+ globalRulesAckKey: globalRulesAckParam(z),
1931
1978
  },
1932
1979
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
1933
1980
  ...input,
@@ -1946,6 +1993,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1946
1993
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
1947
1994
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1948
1995
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
1996
+ globalRulesAckKey: globalRulesAckParam(z),
1949
1997
  },
1950
1998
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
1951
1999
  ...input,
@@ -1964,6 +2012,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1964
2012
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
1965
2013
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1966
2014
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
2015
+ globalRulesAckKey: globalRulesAckParam(z),
1967
2016
  },
1968
2017
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
1969
2018
  ...input,
@@ -1982,6 +2031,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
1982
2031
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
1983
2032
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
1984
2033
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
2034
+ globalRulesAckKey: globalRulesAckParam(z),
1985
2035
  },
1986
2036
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
1987
2037
  ...input,
@@ -2000,6 +2050,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2000
2050
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
2001
2051
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
2002
2052
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
2053
+ globalRulesAckKey: globalRulesAckParam(z),
2003
2054
  },
2004
2055
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
2005
2056
  ...input,
@@ -2018,6 +2069,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2018
2069
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
2019
2070
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
2020
2071
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
2072
+ globalRulesAckKey: globalRulesAckParam(z),
2021
2073
  },
2022
2074
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
2023
2075
  ...input,
@@ -2036,6 +2088,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2036
2088
  order: z.number().optional().default(0).describe('Step order. Saved as enfyra_flow_step.stepOrder.'),
2037
2089
  timeout: z.number().int().positive().optional().describe('Step timeout in ms.'),
2038
2090
  isEnabled: z.boolean().optional().default(true).describe('Enable step.'),
2091
+ globalRulesAckKey: globalRulesAckParam(z),
2039
2092
  },
2040
2093
  async (input) => jsonText(await ensureFlowStep(ENFYRA_API_URL, {
2041
2094
  ...input,
@@ -2055,6 +2108,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2055
2108
  permission: z.string().optional().describe('Menu permission JSON object.'),
2056
2109
  description: z.string().optional().describe('Admin note.'),
2057
2110
  isEnabled: z.boolean().optional().default(true).describe('Enable menu.'),
2111
+ globalRulesAckKey: globalRulesAckParam(z),
2058
2112
  },
2059
2113
  async (input) => jsonText({
2060
2114
  action: 'menu_ensured',
@@ -2072,6 +2126,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2072
2126
  description: z.string().optional().describe('Extension description.'),
2073
2127
  isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
2074
2128
  version: z.string().optional().default('1.0.0').describe('Extension version.'),
2129
+ globalRulesAckKey: globalRulesAckParam(z),
2075
2130
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
2076
2131
  },
2077
2132
  async (input) => jsonText({
@@ -2089,6 +2144,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2089
2144
  description: z.string().optional().describe('Extension description.'),
2090
2145
  isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
2091
2146
  version: z.string().optional().default('1.0.0').describe('Extension version.'),
2147
+ globalRulesAckKey: globalRulesAckParam(z),
2092
2148
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
2093
2149
  },
2094
2150
  async (input) => jsonText({
@@ -2106,6 +2162,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2106
2162
  description: z.string().optional().describe('Extension description.'),
2107
2163
  isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
2108
2164
  version: z.string().optional().default('1.0.0').describe('Extension version.'),
2165
+ globalRulesAckKey: globalRulesAckParam(z),
2109
2166
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
2110
2167
  },
2111
2168
  async (input) => jsonText({
@@ -1,7 +1,12 @@
1
+ export const GLOBAL_RULES_ACK_KEY = 'EFYRA::GLOBAL-RULES::READ-LIVE-CONTEXT-FIRST::2bW-20260701';
1
2
  export const DYNAMIC_CODE_KNOWLEDGE_ACK_KEY = 'EFYRA::SECURE-REPO-CONTRACT::R9x-kelp-42Q::NO-RAW-TRUSTED';
2
3
  export const EXTENSION_KNOWLEDGE_ACK_KEY = 'EFYRA::EXTENSION-THEME-CONTRACT::VIOLET-IS-NOT-A-PLAN::7mQ';
3
4
 
4
- const REQUIRED_KNOWLEDGE_VERSION = '2026-06-30.secure-repo-v1';
5
+ const REQUIRED_KNOWLEDGE_VERSION = '2026-07-01.global-rules-v1';
6
+
7
+ export function globalRulesAckParam(z) {
8
+ return z.string().describe('Required global-rules acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the global Enfyra MCP rules, then pass globalRulesAckKey exactly.');
9
+ }
5
10
 
6
11
  export function dynamicCodeKnowledgeAckParam(z) {
7
12
  return z.string().describe('Required dynamic-code acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the dynamic server code knowledge, then pass dynamicCodeAckKey exactly.');
@@ -11,6 +16,16 @@ export function extensionKnowledgeAckParam(z) {
11
16
  return z.string().describe('Required extension acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the extension/theme knowledge, then pass extensionAckKey exactly.');
12
17
  }
13
18
 
19
+ export function assertGlobalRulesAck(key) {
20
+ if (key !== GLOBAL_RULES_ACK_KEY) {
21
+ throw new Error('Missing or invalid global-rules acknowledgement. Call get_enfyra_required_knowledge, read the global Enfyra MCP rules, then pass globalRulesAckKey as globalRulesAckKey.');
22
+ }
23
+ }
24
+
25
+ export function assertGlobalRulesAckIf(condition, key) {
26
+ if (condition) assertGlobalRulesAck(key);
27
+ }
28
+
14
29
  export function assertDynamicCodeKnowledgeAck(key) {
15
30
  if (key !== DYNAMIC_CODE_KNOWLEDGE_ACK_KEY) {
16
31
  throw new Error('Missing or invalid dynamic-code knowledge acknowledgement. Call get_enfyra_required_knowledge, read the dynamic server code contracts, then pass dynamicCodeAckKey as knowledgeAckKey.');
@@ -34,13 +49,59 @@ export function assertExtensionKnowledgeAckIf(condition, key) {
34
49
  export function buildRequiredKnowledgePayload() {
35
50
  return {
36
51
  version: REQUIRED_KNOWLEDGE_VERSION,
37
- purpose: 'Read this before generating or saving Enfyra dynamic server code through MCP.',
52
+ purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, dynamic server code, or extension UI through MCP.',
53
+ globalRulesAckKey: GLOBAL_RULES_ACK_KEY,
38
54
  dynamicCodeAckKey: DYNAMIC_CODE_KNOWLEDGE_ACK_KEY,
39
55
  extensionAckKey: EXTENSION_KNOWLEDGE_ACK_KEY,
40
56
  usage: [
57
+ 'Pass globalRulesAckKey exactly as globalRulesAckKey when calling MCP tools that mutate Enfyra metadata, schema, routes, permissions, menus, packages, cache state, dynamic code, or extension UI.',
41
58
  'Pass dynamicCodeAckKey exactly as knowledgeAckKey when calling MCP tools that create or update dynamic server code.',
42
59
  'Pass extensionAckKey exactly as extensionKnowledgeAckKey when calling MCP tools that create or update Enfyra extension code.',
43
60
  ],
61
+ globalRules: [
62
+ {
63
+ id: 'examples-are-reasoning-anchors',
64
+ rules: [
65
+ 'Examples explain transferable decisions, not copy-paste mandates.',
66
+ 'Preserve platform contracts and safety boundaries, then adapt names, routes, fields, menus, labels, and lifecycle to live metadata and the user goal.',
67
+ 'When examples use chat, order, report, cloud, email, or support domains, treat those as analogies unless the current task is exactly that domain.',
68
+ ],
69
+ },
70
+ {
71
+ id: 'discover-before-changing',
72
+ rules: [
73
+ 'Inspect live metadata/routes/features before schema, route, permission, extension, flow, or handler changes.',
74
+ 'Use narrow inspection tools for the table, route, feature, or script being changed instead of broad discovery after the target is known.',
75
+ 'Read sourceCode, not compiledCode, for editable dynamic scripts.',
76
+ ],
77
+ },
78
+ {
79
+ id: 'mutations-are-intentional',
80
+ rules: [
81
+ 'Prefer business operation tools over generic CRUD when a specific tool exists.',
82
+ 'Destructive operations are preview-first; pass confirm=true only after explicit user approval.',
83
+ 'Do not manually reload caches unless natural partial reload is proven stale or a concrete reload error requires it.',
84
+ 'Never fabricate ids, field names, relation names, paths, package names, or permission scopes.',
85
+ ],
86
+ },
87
+ {
88
+ id: 'security-first',
89
+ rules: [
90
+ 'Treat permission and owner/tenant scope as the first design step for any route, handler, hook, flow, extension, websocket, or data surface.',
91
+ 'Route permission only lets authenticated users reach a route after RoleGuard; handlers, hooks, RLS, and scripts still enforce record ownership and tenant/project scope.',
92
+ 'Do not expose unpublished fields, private relation facts, secret values, token hashes, stack traces, SQL, provider payloads, or generated passwords to user-facing clients.',
93
+ ],
94
+ },
95
+ {
96
+ id: 'shell-signals',
97
+ rules: [
98
+ 'For app shell menu/account-panel notifications, decide the signal source before choosing count or dot.',
99
+ 'Use a count only when the shell receives an exact or bounded count from a notification/summary source.',
100
+ 'Use a dot when realtime only proves new attention exists.',
101
+ 'Do not fetch destination domain lists such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
102
+ ],
103
+ },
104
+ ],
44
105
  dynamicServerCode: [
45
106
  {
46
107
  id: 'secure-vs-trusted-repositories',
@@ -4,6 +4,7 @@
4
4
  import { z } from 'zod';
5
5
  import { fetchAPI } from './fetch.js';
6
6
  import { jsonContent } from './response-format.js';
7
+ import { assertGlobalRulesAck, globalRulesAckParam } from './required-knowledge.js';
7
8
 
8
9
  let schemaQueue = Promise.resolve();
9
10
 
@@ -308,6 +309,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
308
309
  const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
309
310
 
310
311
  async function appendColumnToTable(args) {
312
+ assertGlobalRulesAck(args.globalRulesAckKey);
311
313
  return withSchemaQueue(async () => {
312
314
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
313
315
  if (!tableData) {
@@ -330,6 +332,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
330
332
  }
331
333
 
332
334
  async function appendRelationToTable(args) {
335
+ assertGlobalRulesAck(args.globalRulesAckKey);
333
336
  return withSchemaQueue(async () => {
334
337
  assertNoForbiddenRelationKeys(args);
335
338
  const { sourceTableId, targetTableId, type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description } = args;
@@ -359,7 +362,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
359
362
  });
360
363
  }
361
364
 
362
- async function removeColumnFromTable({ tableId, columnId, confirm }) {
365
+ async function removeColumnFromTable({ tableId, columnId, confirm, globalRulesAckKey }) {
363
366
  return withSchemaQueue(async () => {
364
367
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
365
368
  if (!tableData) {
@@ -385,6 +388,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
385
388
  }, null, 2) }],
386
389
  };
387
390
  }
391
+ assertGlobalRulesAck(globalRulesAckKey);
388
392
 
389
393
  const columns = existingColumns
390
394
  .filter(col => String(getId(col)) !== String(columnId))
@@ -401,7 +405,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
401
405
  });
402
406
  }
403
407
 
404
- async function removeRelationFromTable({ tableId, relationId, confirm }) {
408
+ async function removeRelationFromTable({ tableId, relationId, confirm, globalRulesAckKey }) {
405
409
  return withSchemaQueue(async () => {
406
410
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
407
411
  if (!tableData) {
@@ -427,6 +431,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
427
431
  }, null, 2) }],
428
432
  };
429
433
  }
434
+ assertGlobalRulesAck(globalRulesAckKey);
430
435
 
431
436
  const relations = existingRelations
432
437
  .filter(rel => String(getId(rel)) !== String(relationId))
@@ -458,6 +463,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
458
463
  defaultValue: z.string().optional().describe('Default value as JSON string or backend-supported literal.'),
459
464
  description: z.string().optional().describe('Column description.'),
460
465
  options: z.string().optional().describe('Column options as JSON string (e.g., enum values).'),
466
+ globalRulesAckKey: globalRulesAckParam(z),
461
467
  };
462
468
 
463
469
  const relationCreateSchema = {
@@ -477,18 +483,21 @@ export function registerTableTools(server, ENFYRA_API_URL) {
477
483
  targetColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
478
484
  junctionSourceColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
479
485
  junctionTargetColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
486
+ globalRulesAckKey: globalRulesAckParam(z),
480
487
  };
481
488
 
482
489
  const columnDeleteSchema = {
483
490
  tableId: z.string().describe('Table definition ID.'),
484
491
  columnId: z.string().describe('Column definition ID to delete.'),
485
492
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
493
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
486
494
  };
487
495
 
488
496
  const relationDeleteSchema = {
489
497
  tableId: z.string().describe('Table definition ID (source table of the relation).'),
490
498
  relationId: z.string().describe('Relation definition ID to delete.'),
491
499
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
500
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
492
501
  };
493
502
 
494
503
  // ─── READ ───
@@ -565,8 +574,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
565
574
  relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id, {"id": <id>}, or an exact table name that MCP resolves to an id before mutation. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Omit inversePropertyName unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal needs the reverse field. Example only when parent posts are queried: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
566
575
  indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
567
576
  uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
577
+ globalRulesAckKey: globalRulesAckParam(z),
568
578
  },
569
- async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
579
+ async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson, globalRulesAckKey }) => withSchemaQueue(async () => {
580
+ assertGlobalRulesAck(globalRulesAckKey);
570
581
  const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
571
582
  const userColumns = parseJsonArrayParam('columns', columnsJson);
572
583
  const parsedRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
@@ -627,8 +638,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
627
638
  graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL table data still requires Bearer auth; anonymous root or schema probes may return 200.'),
628
639
  indexes: z.string().optional().describe('Complete JSON array of logical index field groups to store on enfyra_table.indexes. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current indexes; pass [] to clear.'),
629
640
  uniques: z.string().optional().describe('Complete JSON array of logical unique field groups to store on enfyra_table.uniques. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current uniques; pass [] to clear.'),
641
+ globalRulesAckKey: globalRulesAckParam(z),
630
642
  },
631
- async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled, indexes: indexesJson, uniques: uniquesJson }) => withSchemaQueue(async () => {
643
+ async ({ tableId, name, alias, description, isSingleRecord, graphqlEnabled, indexes: indexesJson, uniques: uniquesJson, globalRulesAckKey }) => withSchemaQueue(async () => {
644
+ assertGlobalRulesAck(globalRulesAckKey);
632
645
  const body = {};
633
646
  if (name !== undefined) body.name = name;
634
647
  if (alias !== undefined) body.alias = alias;
@@ -657,8 +670,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
657
670
  {
658
671
  tableId: z.string().describe('Table definition ID to delete.'),
659
672
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
673
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
660
674
  },
661
- async ({ tableId, confirm }) => withSchemaQueue(async () => {
675
+ async ({ tableId, confirm, globalRulesAckKey }) => withSchemaQueue(async () => {
662
676
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
663
677
  if (!confirm) {
664
678
  return {
@@ -673,6 +687,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
673
687
  }, null, 2) }],
674
688
  };
675
689
  }
690
+ assertGlobalRulesAck(globalRulesAckKey);
676
691
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
677
692
  method: 'DELETE',
678
693
  });
@@ -720,8 +735,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
720
735
  defaultValue: z.string().optional().describe('New default value as JSON string.'),
721
736
  description: z.string().optional().describe('New description.'),
722
737
  options: z.string().optional().describe('New options as JSON string.'),
738
+ globalRulesAckKey: globalRulesAckParam(z),
723
739
  },
724
- async ({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options }) => withSchemaQueue(async () => {
740
+ async ({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options, globalRulesAckKey }) => withSchemaQueue(async () => {
741
+ assertGlobalRulesAck(globalRulesAckKey);
725
742
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
726
743
  if (!tableData) {
727
744
  return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
@@ -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';
@@ -1471,9 +1473,11 @@ server.tool('create_record', 'Create a new record in any route-backed table. The
1471
1473
  tableName: z.string().describe('Table name to insert into'),
1472
1474
  data: z.string().describe('Record data as JSON string'),
1473
1475
  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.'),
1476
+ globalRulesAckKey: globalRulesAckParam(z),
1474
1477
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1475
1478
  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 }) => {
1479
+ }, async ({ tableName, data, queryParams, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1480
+ assertGlobalRulesAck(globalRulesAckKey);
1477
1481
  validateTableName(tableName);
1478
1482
  assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
1479
1483
  const prepared = await prepareGenericMutation(tableName, data);
@@ -1490,9 +1494,11 @@ server.tool('update_record', 'Update an existing record by ID using PATCH. The t
1490
1494
  id: z.string().describe('Record ID to update'),
1491
1495
  data: z.string().describe('Fields to update as JSON string'),
1492
1496
  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.'),
1497
+ globalRulesAckKey: globalRulesAckParam(z),
1493
1498
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1494
1499
  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 }) => {
1500
+ }, async ({ tableName, id, data, queryParams, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1501
+ assertGlobalRulesAck(globalRulesAckKey);
1496
1502
  validateTableName(tableName);
1497
1503
  assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
1498
1504
  const prepared = await prepareGenericMutation(tableName, data);
@@ -1548,9 +1554,10 @@ server.tool(
1548
1554
  expectedSourceSha256: z.string().optional().describe('Optional SHA-256 from get_script_source; fails if source changed.'),
1549
1555
  scriptLanguage: z.string().optional().describe('Script language to save. Defaults to existing scriptLanguage or javascript.'),
1550
1556
  apply: z.boolean().optional().default(false).describe('false returns preview only; true validates and saves.'),
1557
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when apply=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1551
1558
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required when apply=true. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1552
1559
  },
1553
- async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply, knowledgeAckKey }) => {
1560
+ async ({ tableName, id, oldText, newText, occurrence, expectedSourceSha256, scriptLanguage, apply, globalRulesAckKey, knowledgeAckKey }) => {
1554
1561
  const { record, sourceField, sourceCode } = await fetchScriptRecord(tableName, id);
1555
1562
  if (sourceField !== 'sourceCode') {
1556
1563
  throw new Error(`patch_script_source only saves sourceCode records. Record uses "${sourceField}"; use update_record intentionally for this legacy field.`);
@@ -1581,6 +1588,7 @@ server.tool(
1581
1588
  if (!apply) {
1582
1589
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
1583
1590
  }
1591
+ assertGlobalRulesAck(globalRulesAckKey);
1584
1592
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1585
1593
  const language = scriptLanguage || record.scriptLanguage || 'javascript';
1586
1594
  const prepared = await prepareGenericMutation(
@@ -1623,9 +1631,11 @@ server.tool(
1623
1631
  id: z.string().describe('Record ID to update'),
1624
1632
  sourceCode: z.string().describe('Editable script sourceCode. Pass the raw code string; do not JSON-escape it yourself.'),
1625
1633
  scriptLanguage: z.string().optional().default('javascript').describe('Script language, usually javascript or typescript'),
1634
+ globalRulesAckKey: globalRulesAckParam(z),
1626
1635
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
1627
1636
  },
1628
- async ({ tableName, id, sourceCode, scriptLanguage, knowledgeAckKey }) => {
1637
+ async ({ tableName, id, sourceCode, scriptLanguage, globalRulesAckKey, knowledgeAckKey }) => {
1638
+ assertGlobalRulesAck(globalRulesAckKey);
1629
1639
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
1630
1640
  validateTableName(tableName);
1631
1641
  const prepared = await prepareGenericMutation(
@@ -1652,7 +1662,8 @@ server.tool('delete_record', 'Delete a record by ID', {
1652
1662
  id: z.string().describe('Record ID to delete'),
1653
1663
  queryParams: z.string().optional().describe('Optional query params as JSON object string for route-specific confirmation contracts.'),
1654
1664
  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 }) => {
1665
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1666
+ }, async ({ tableName, id, queryParams, confirm, globalRulesAckKey }) => {
1656
1667
  validateTableName(tableName);
1657
1668
  const primaryKey = await getPrimaryFieldName(tableName);
1658
1669
  if (!confirm) {
@@ -1673,6 +1684,7 @@ server.tool('delete_record', 'Delete a record by ID', {
1673
1684
  next: 'Call delete_record again with confirm=true to delete this route-backed record.',
1674
1685
  }, null, 2) }] };
1675
1686
  }
1687
+ assertGlobalRulesAck(globalRulesAckKey);
1676
1688
  const query = parseQueryParamsArg(queryParams);
1677
1689
  const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${id}`, query), { method: 'DELETE' });
1678
1690
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -1713,8 +1725,10 @@ server.tool(
1713
1725
  buttonColor: z.string().describe('Badge background color as full hex, e.g. #dbeafe.'),
1714
1726
  textColor: z.string().describe('Badge text color as full hex, e.g. #1d4ed8.'),
1715
1727
  isSystem: z.boolean().optional().default(false).describe('Set true only for built-in/runtime-owned methods. Normal app methods should leave this false.'),
1728
+ globalRulesAckKey: globalRulesAckParam(z),
1716
1729
  },
1717
- async ({ method, buttonColor, textColor, isSystem }) => {
1730
+ async ({ method, buttonColor, textColor, isSystem, globalRulesAckKey }) => {
1731
+ assertGlobalRulesAck(globalRulesAckKey);
1718
1732
  const normalizedMethod = normalizeMethodNameInput(method);
1719
1733
  const existing = await findMethodRecordByName(normalizedMethod);
1720
1734
  if (existing) {
@@ -1747,8 +1761,10 @@ server.tool(
1747
1761
  method: z.string().optional().describe('Existing method name to find, or new name when id is provided.'),
1748
1762
  buttonColor: z.string().optional().describe('Badge background color as full hex, e.g. #dbeafe.'),
1749
1763
  textColor: z.string().optional().describe('Badge text color as full hex, e.g. #1d4ed8.'),
1764
+ globalRulesAckKey: globalRulesAckParam(z),
1750
1765
  },
1751
- async ({ id, method, buttonColor, textColor }) => {
1766
+ async ({ id, method, buttonColor, textColor, globalRulesAckKey }) => {
1767
+ assertGlobalRulesAck(globalRulesAckKey);
1752
1768
  let targetId = id;
1753
1769
  let existing = null;
1754
1770
  if (!targetId) {
@@ -1793,8 +1809,9 @@ server.tool(
1793
1809
  id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
1794
1810
  method: z.string().optional().describe('Method name to find when id is omitted.'),
1795
1811
  confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
1812
+ globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1796
1813
  },
1797
- async ({ id, method, confirm }) => {
1814
+ async ({ id, method, confirm, globalRulesAckKey }) => {
1798
1815
  let targetId = id;
1799
1816
  let target = null;
1800
1817
  if (!targetId) {
@@ -1820,6 +1837,7 @@ server.tool(
1820
1837
  next: 'Call delete_method again with confirm=true to delete.',
1821
1838
  }, null, 2) }] };
1822
1839
  }
1840
+ assertGlobalRulesAck(globalRulesAckKey);
1823
1841
  const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_method/${encodeURIComponent(String(targetId))}`, { method: 'DELETE' });
1824
1842
  _methodMap = null;
1825
1843
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -2449,8 +2467,10 @@ server.tool(
2449
2467
  .describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
2450
2468
  isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
2451
2469
  description: z.string().optional().describe('Route description'),
2470
+ globalRulesAckKey: globalRulesAckParam(z),
2452
2471
  },
2453
- async ({ path: routePath, mainTableId, methods, publicMethods, isEnabled, description }) => {
2472
+ async ({ path: routePath, mainTableId, methods, publicMethods, isEnabled, description, globalRulesAckKey }) => {
2473
+ assertGlobalRulesAck(globalRulesAckKey);
2454
2474
  const methodMap = await getMethodMap();
2455
2475
  const normalizedPath = normalizeRestPath(routePath);
2456
2476
 
@@ -2513,9 +2533,11 @@ server.tool(
2513
2533
  sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
2514
2534
  scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
2515
2535
  timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
2536
+ globalRulesAckKey: globalRulesAckParam(z),
2516
2537
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2517
2538
  },
2518
- async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, knowledgeAckKey }) => {
2539
+ async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout, globalRulesAckKey, knowledgeAckKey }) => {
2540
+ assertGlobalRulesAck(globalRulesAckKey);
2519
2541
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2520
2542
  const methodNames = methods && methods.length > 0 ? methods : method ? [method] : [];
2521
2543
  if (methodNames.length === 0) throw new Error('Provide method or methods');
@@ -2576,9 +2598,11 @@ server.tool(
2576
2598
  .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
2577
2599
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
2578
2600
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
2601
+ globalRulesAckKey: globalRulesAckParam(z),
2579
2602
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2580
2603
  },
2581
- async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
2604
+ async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, globalRulesAckKey, knowledgeAckKey }) => {
2605
+ assertGlobalRulesAck(globalRulesAckKey);
2582
2606
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2583
2607
  const methodMap = await getMethodMap();
2584
2608
  const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
@@ -2632,9 +2656,11 @@ server.tool(
2632
2656
  .describe('Method names this hook applies to. Default: built-in REST methods GET, POST, PATCH, DELETE.'),
2633
2657
  priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
2634
2658
  isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
2659
+ globalRulesAckKey: globalRulesAckParam(z),
2635
2660
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z),
2636
2661
  },
2637
- async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, knowledgeAckKey }) => {
2662
+ async ({ routeId, name, code, scriptLanguage, methods, priority, isEnabled, globalRulesAckKey, knowledgeAckKey }) => {
2663
+ assertGlobalRulesAck(globalRulesAckKey);
2638
2664
  assertDynamicCodeKnowledgeAck(knowledgeAckKey);
2639
2665
  const methodMap = await getMethodMap();
2640
2666
  const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
@@ -2751,8 +2777,10 @@ server.tool(
2751
2777
  mode: z.enum(['merge', 'replace']).optional().default('merge').describe('merge adds methods to an existing permission; replace overwrites methods on the matched permission.'),
2752
2778
  description: z.string().optional().describe('Admin note'),
2753
2779
  isEnabled: z.boolean().optional().default(true).describe('Enable the permission'),
2780
+ globalRulesAckKey: globalRulesAckParam(z),
2754
2781
  },
2755
- async ({ path, routeId, methods, roleId, roleName, allowedUserIds, mode, description, isEnabled }) => {
2782
+ async ({ path, routeId, methods, roleId, roleName, allowedUserIds, mode, description, isEnabled, globalRulesAckKey }) => {
2783
+ assertGlobalRulesAck(globalRulesAckKey);
2756
2784
  if (!path && !routeId) throw new Error('Provide path or routeId.');
2757
2785
  if (path && routeId) throw new Error('Provide path or routeId, not both.');
2758
2786
  if (roleId && roleName) throw new Error('Provide roleId or roleName, not both.');
@@ -2852,22 +2880,34 @@ registerPlatformOperationTools(server, ENFYRA_API_URL);
2852
2880
  // CACHE & SYSTEM TOOLS
2853
2881
  // ============================================================================
2854
2882
 
2855
- server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {}, async () => {
2883
+ server.tool('reload_all', 'Reload all caches (metadata, routes, GraphQL)', {
2884
+ globalRulesAckKey: globalRulesAckParam(z),
2885
+ }, async ({ globalRulesAckKey }) => {
2886
+ assertGlobalRulesAck(globalRulesAckKey);
2856
2887
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
2857
2888
  return { content: [{ type: 'text', text: `System reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2858
2889
  });
2859
2890
 
2860
- server.tool('reload_metadata', 'Reload metadata cache only', {}, async () => {
2891
+ server.tool('reload_metadata', 'Reload metadata cache only', {
2892
+ globalRulesAckKey: globalRulesAckParam(z),
2893
+ }, async ({ globalRulesAckKey }) => {
2894
+ assertGlobalRulesAck(globalRulesAckKey);
2861
2895
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
2862
2896
  return { content: [{ type: 'text', text: `Metadata reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2863
2897
  });
2864
2898
 
2865
- server.tool('reload_routes', 'Reload routes cache only', {}, async () => {
2899
+ server.tool('reload_routes', 'Reload routes cache only', {
2900
+ globalRulesAckKey: globalRulesAckParam(z),
2901
+ }, async ({ globalRulesAckKey }) => {
2902
+ assertGlobalRulesAck(globalRulesAckKey);
2866
2903
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
2867
2904
  return { content: [{ type: 'text', text: `Routes reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2868
2905
  });
2869
2906
 
2870
- server.tool('reload_graphql', 'Reload GraphQL schema', {}, async () => {
2907
+ server.tool('reload_graphql', 'Reload GraphQL schema', {
2908
+ globalRulesAckKey: globalRulesAckParam(z),
2909
+ }, async ({ globalRulesAckKey }) => {
2910
+ assertGlobalRulesAck(globalRulesAckKey);
2871
2911
  const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
2872
2912
  return { content: [{ type: 'text', text: `GraphQL reloaded:\n${JSON.stringify(result, null, 2)}` }] };
2873
2913
  });
@@ -3015,8 +3055,10 @@ server.tool(
3015
3055
  name: z.string().describe('Exact NPM package name (e.g., "node-ssh", "axios")'),
3016
3056
  type: z.enum(['Server', 'App']).default('Server').describe('Where to install: Server (handlers/hooks) or App (extensions)'),
3017
3057
  version: z.string().optional().describe('Specific version. If omitted, fetches latest from NPM.'),
3058
+ globalRulesAckKey: globalRulesAckParam(z),
3018
3059
  },
3019
- async ({ name, type, version }) => {
3060
+ async ({ name, type, version, globalRulesAckKey }) => {
3061
+ assertGlobalRulesAck(globalRulesAckKey);
3020
3062
  // Step 1: Get package info from NPM if version not specified
3021
3063
  let pkgVersion = version;
3022
3064
  let pkgDescription = '';