@enfyra/mcp-server 0.1.4 → 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 +2 -2
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +53 -7
- package/src/lib/mcp-instructions.js +1 -1
- package/src/lib/platform-operation-tools.js +74 -17
- package/src/lib/required-knowledge.js +63 -2
- package/src/lib/table-tools.js +23 -6
- package/src/mcp-server-entry.mjs +60 -18
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
|
|
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
|
-
-
|
|
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
package/src/lib/mcp-examples.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export const EXAMPLE_REASONING_GUIDE = [
|
|
2
|
+
'Examples are reasoning anchors, not templates to copy blindly. Preserve the platform contract, then adapt table names, route paths, relation names, fields, UI labels, and lifecycle triggers to the live app.',
|
|
3
|
+
'First identify the invariant being demonstrated: security boundary, query shape, shell registry contract, schema relation direction, runtime lifecycle, or browser proxy pattern.',
|
|
4
|
+
'Then identify what is illustrative: chat/order/report/cloud paths, sample field names, icons, labels, menu order, and specific notification kinds.',
|
|
5
|
+
'When a note says do not, treat it as a contract or safety boundary unless live metadata proves a different supported contract. When a note says for example, map the idea to the current domain instead of copying the literal names.',
|
|
6
|
+
'Before applying an example, inspect live metadata/routes/features and choose the closest supported tool. Use the smallest example that proves the decision, then compose with other examples only when the task truly needs multiple contracts.',
|
|
7
|
+
];
|
|
8
|
+
|
|
1
9
|
export const EXAMPLE_CATEGORIES = {
|
|
2
10
|
'ssr-app-auth': {
|
|
3
11
|
title: 'SSR app auth, OAuth, refresh, and proxy setup',
|
|
4
|
-
useWhen: 'Use when building Nuxt, Next, or another browser app that should rely on Enfyra cookies through an app-origin proxy.',
|
|
12
|
+
useWhen: 'Use when building Nuxt, Next, or another browser app that should rely on Enfyra cookies through an app-origin proxy; adapt the framework-specific wrapper while preserving the same-origin proxy and cookie boundary.',
|
|
5
13
|
examples: [
|
|
6
14
|
{
|
|
7
15
|
name: 'Nuxt routeRules for REST and Socket.IO',
|
|
@@ -543,6 +551,7 @@ update_record({
|
|
|
543
551
|
name: 'Create a chat conversation table',
|
|
544
552
|
code: `create_table({
|
|
545
553
|
name: "chat_conversation",
|
|
554
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
546
555
|
columns: JSON.stringify([
|
|
547
556
|
{ name: "kind", type: "varchar", isNullable: false, defaultValue: "dm" },
|
|
548
557
|
{ name: "title", type: "varchar", isNullable: true },
|
|
@@ -550,6 +559,7 @@ update_record({
|
|
|
550
559
|
])
|
|
551
560
|
})`,
|
|
552
561
|
notes: [
|
|
562
|
+
'Chat is the illustrative domain here. For another domain, keep the same modeling question: what is the parent entity, what is stored on the parent, and what belongs on child rows?',
|
|
553
563
|
'create_table creates the default route for /chat_conversation.',
|
|
554
564
|
'Keep the latest message as a relation named lastMessage after chat_message exists; do not duplicate last message text/date columns.',
|
|
555
565
|
'Do not create tables just to get custom paths; use create_route for that.',
|
|
@@ -559,6 +569,7 @@ update_record({
|
|
|
559
569
|
name: 'Create relations directly to enfyra_user',
|
|
560
570
|
code: `create_table({
|
|
561
571
|
name: "chat_message",
|
|
572
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
562
573
|
columns: JSON.stringify([
|
|
563
574
|
{ name: "text", type: "text", isNullable: false },
|
|
564
575
|
{ name: "persistStatus", type: "varchar", defaultValue: "persisted" }
|
|
@@ -584,6 +595,7 @@ update_record({
|
|
|
584
595
|
])
|
|
585
596
|
})`,
|
|
586
597
|
notes: [
|
|
598
|
+
'The relation names conversation and sender are examples of domain language; choose relation property names that match the entity model users reason about.',
|
|
587
599
|
'Use enfyra_user as the user table.',
|
|
588
600
|
'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "enfyra_user" before schema mutation.',
|
|
589
601
|
'Do not add inverse relations on enfyra_user unless a concrete user-to-record response, UI, or deep query will use it.',
|
|
@@ -597,6 +609,7 @@ update_record({
|
|
|
597
609
|
code: `create_relation({
|
|
598
610
|
sourceTableId: "chat_message",
|
|
599
611
|
targetTableId: "chat_conversation",
|
|
612
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
600
613
|
propertyName: "conversation",
|
|
601
614
|
inversePropertyName: "messages",
|
|
602
615
|
type: "many-to-one",
|
|
@@ -606,6 +619,7 @@ update_record({
|
|
|
606
619
|
notes: [
|
|
607
620
|
'Use inversePropertyName only when the parent table will actually expose, deep-load, count, or sort by that child collection.',
|
|
608
621
|
'For example, conversation.messages is justified if a conversation detail response loads the latest message page with deep.messages limit/sort, or if a list sorts by _max(messages.createdAt).',
|
|
622
|
+
'Translate this to the current domain by asking whether the parent screen needs a child collection or aggregate; if not, keep the relation one-directional.',
|
|
609
623
|
'If the app only filters chat_message by conversation.id, omit inversePropertyName and keep the schema one-directional.',
|
|
610
624
|
'Before creating an inverse, inspect existing relations and state why the reverse traversal is needed.',
|
|
611
625
|
],
|
|
@@ -614,6 +628,7 @@ update_record({
|
|
|
614
628
|
name: 'Add chat_conversation.lastMessage after chat_message exists',
|
|
615
629
|
code: `update_table({
|
|
616
630
|
tableId: "<chat_conversation_id>",
|
|
631
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
617
632
|
relations: JSON.stringify([
|
|
618
633
|
{
|
|
619
634
|
propertyName: "createdBy",
|
|
@@ -640,6 +655,7 @@ update_record({
|
|
|
640
655
|
name: 'Unread/read table with unique and indexes',
|
|
641
656
|
code: `create_table({
|
|
642
657
|
name: "chat_message_read",
|
|
658
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
643
659
|
columns: JSON.stringify([
|
|
644
660
|
{ name: "isRead", type: "boolean", defaultValue: false },
|
|
645
661
|
{ name: "readAt", type: "datetime", isNullable: true }
|
|
@@ -666,6 +682,7 @@ update_record({
|
|
|
666
682
|
code: `create_column({
|
|
667
683
|
tableId: "<enfyra_user_table_id>",
|
|
668
684
|
name: "emailVerifiedAt",
|
|
685
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
669
686
|
type: "datetime",
|
|
670
687
|
isNullable: true,
|
|
671
688
|
isPublished: true,
|
|
@@ -675,6 +692,7 @@ update_record({
|
|
|
675
692
|
create_column({
|
|
676
693
|
tableId: "<enfyra_user_table_id>",
|
|
677
694
|
name: "emailVerificationStatus",
|
|
695
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
678
696
|
type: "varchar",
|
|
679
697
|
isNullable: false,
|
|
680
698
|
defaultValue: "pending",
|
|
@@ -685,6 +703,7 @@ create_column({
|
|
|
685
703
|
create_column({
|
|
686
704
|
tableId: "<integration_secret_table_id>",
|
|
687
705
|
name: "value",
|
|
706
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
688
707
|
type: "text",
|
|
689
708
|
isNullable: false,
|
|
690
709
|
isPublished: false,
|
|
@@ -711,6 +730,7 @@ create_column({
|
|
|
711
730
|
create_column({
|
|
712
731
|
tableId: "<table_id>",
|
|
713
732
|
name: "api_secret",
|
|
733
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
714
734
|
type: "text",
|
|
715
735
|
isPublished: false,
|
|
716
736
|
isEncrypted: true
|
|
@@ -754,6 +774,7 @@ create_column({
|
|
|
754
774
|
'Use a conversation read pre-hook/RLS boundary so the route only returns conversations visible to @USER.',
|
|
755
775
|
'lastMessage is a relation to chat_message; do not duplicate preview fields on chat_conversation.',
|
|
756
776
|
'all: true tells MCP to send REST limit=0 and load all matching conversation rows.',
|
|
777
|
+
'This is a small bounded user inbox example. For larger inventories, prefer pagination even when RLS scopes the records.',
|
|
757
778
|
'Do not fetch messages for every conversation on initial list load; load messages after selecting a conversation.',
|
|
758
779
|
],
|
|
759
780
|
},
|
|
@@ -807,6 +828,7 @@ GET /enfyra/post?filter={"<primaryKeyFromMetadata>":{"_eq":123}}&limit=1`,
|
|
|
807
828
|
notes: [
|
|
808
829
|
'Use fields with dotted relation paths when you only need scalar fields from related records.',
|
|
809
830
|
'This is enough for simple many-to-one or one-to-one relation display such as owner.email, customer.name, or lastMessage.text.',
|
|
831
|
+
'Treat order/customer as placeholders; the transferable idea is "show parent rows with a few scalar relation fields".',
|
|
810
832
|
'Do not add deep when fields alone can express the relation data you need.',
|
|
811
833
|
],
|
|
812
834
|
},
|
|
@@ -894,6 +916,7 @@ query_table({
|
|
|
894
916
|
notes: [
|
|
895
917
|
'Use _max(relation.field) for latest-child ordering, _min(relation.field) for earliest-child ordering, and _count(relation) for child-count ordering.',
|
|
896
918
|
'Aggregate sort helpers only work on direct one-to-many or many-to-many list relations.',
|
|
919
|
+
'Support tickets and messages are illustrative. Apply this when a parent list must be ordered by child recency or child volume.',
|
|
897
920
|
'The aggregate field must be a real published, non-encrypted scalar field on the related table for user-facing APIs.',
|
|
898
921
|
'Do not use _max, _min, or _count on private relations or unpublished fields unless the endpoint intentionally exposes that fact.',
|
|
899
922
|
'Do not use raw sort=-messages.createdAt for parent ordering; it is ambiguous and rejected.',
|
|
@@ -909,6 +932,7 @@ GET /enfyra/integrations?filter={"api_token":{"_eq":"plaintext-token"}}
|
|
|
909
932
|
create_column({
|
|
910
933
|
tableId: "<integrations_table_id>",
|
|
911
934
|
name: "api_token_lookup_sha256",
|
|
935
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
912
936
|
type: "varchar",
|
|
913
937
|
isNullable: false,
|
|
914
938
|
isPublished: false
|
|
@@ -944,6 +968,7 @@ const found = await #integrations.find({
|
|
|
944
968
|
routeId: "<route_id>",
|
|
945
969
|
method: "POST",
|
|
946
970
|
scriptLanguage: "javascript",
|
|
971
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
947
972
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
948
973
|
sourceCode: \`const email = @BODY.email
|
|
949
974
|
if (!email) @THROW400("Email is required")
|
|
@@ -952,7 +977,7 @@ return { ok: true, email }\`
|
|
|
952
977
|
})`,
|
|
953
978
|
notes: [
|
|
954
979
|
'Use sourceCode, not logic. The server generates compiledCode.',
|
|
955
|
-
'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.',
|
|
956
981
|
'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
|
|
957
982
|
'Do not pass name to enfyra_route_handler; one handler is identified by route + method.',
|
|
958
983
|
],
|
|
@@ -1006,6 +1031,7 @@ const scope = {
|
|
|
1006
1031
|
name: 'Encrypted field table definition',
|
|
1007
1032
|
code: `create_table({
|
|
1008
1033
|
name: "integrations",
|
|
1034
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1009
1035
|
columns: JSON.stringify([
|
|
1010
1036
|
{ name: "name", type: "varchar", isNullable: false },
|
|
1011
1037
|
{
|
|
@@ -1033,6 +1059,7 @@ const scope = {
|
|
|
1033
1059
|
name: "strip_email_verification_fields",
|
|
1034
1060
|
methods: ["PATCH"],
|
|
1035
1061
|
priority: -10,
|
|
1062
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1036
1063
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1037
1064
|
code: \`delete @BODY.emailVerifiedAt
|
|
1038
1065
|
delete @BODY.emailVerificationStatus
|
|
@@ -1051,6 +1078,7 @@ delete @BODY.emailVerificationSentAt\`
|
|
|
1051
1078
|
name: "shape_display_title",
|
|
1052
1079
|
methods: ["GET"],
|
|
1053
1080
|
priority: 0,
|
|
1081
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1054
1082
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1055
1083
|
code: \`if (@ERROR) {
|
|
1056
1084
|
@LOGS("Request failed", @ERROR.message)
|
|
@@ -1510,7 +1538,7 @@ return saved`,
|
|
|
1510
1538
|
},
|
|
1511
1539
|
extensions: {
|
|
1512
1540
|
title: 'Dynamic app extensions and menus',
|
|
1513
|
-
useWhen: 'Use when adding custom UI pages
|
|
1541
|
+
useWhen: 'Use when adding custom Enfyra admin UI pages, widgets, global shell integrations, menu entries, account-panel rows, or shell attention signals.',
|
|
1514
1542
|
examples: [
|
|
1515
1543
|
{
|
|
1516
1544
|
name: 'Create or update HTTP method colors',
|
|
@@ -1544,6 +1572,7 @@ update_method({
|
|
|
1544
1572
|
icon: "lucide:bar-chart-3",
|
|
1545
1573
|
order: 20,
|
|
1546
1574
|
isEnabled: true,
|
|
1575
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1547
1576
|
permission: JSON.stringify({
|
|
1548
1577
|
or: [
|
|
1549
1578
|
{ route: "/reports", methods: ["GET"] },
|
|
@@ -1559,15 +1588,17 @@ ensure_page_extension({
|
|
|
1559
1588
|
menuId: "<created-menu-id>",
|
|
1560
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>",
|
|
1561
1590
|
isEnabled: true,
|
|
1591
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1562
1592
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1563
1593
|
})`,
|
|
1564
1594
|
notes: [
|
|
1595
|
+
'Reports is an illustrative page. Keep the shell/page contracts, but choose the real route, menu label, icon, permissions, and body layout from the operator workflow.',
|
|
1565
1596
|
'Menu provides navigation; extension provides content.',
|
|
1566
1597
|
'Use enfyra_menu.label, not title.',
|
|
1567
1598
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1568
1599
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1569
1600
|
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
|
|
1570
|
-
'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.',
|
|
1571
1602
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1572
1603
|
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1573
1604
|
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
@@ -1633,6 +1664,7 @@ ensure_widget_extension({
|
|
|
1633
1664
|
description: "Report status summary cards",
|
|
1634
1665
|
code: reportStatusWidgetCode,
|
|
1635
1666
|
isEnabled: true,
|
|
1667
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1636
1668
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1637
1669
|
})
|
|
1638
1670
|
|
|
@@ -1642,9 +1674,11 @@ ensure_page_extension({
|
|
|
1642
1674
|
menuId: "<reports-menu-id>",
|
|
1643
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>",
|
|
1644
1676
|
isEnabled: true,
|
|
1677
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1645
1678
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1646
1679
|
})`,
|
|
1647
1680
|
notes: [
|
|
1681
|
+
'This shows composition mechanics. Replace reports/status/table with domain sections that are independently reusable or complex enough to deserve widgets.',
|
|
1648
1682
|
'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
|
|
1649
1683
|
'Embed widgets by their numeric enfyra_extension id, not by extensionId/name.',
|
|
1650
1684
|
'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
|
|
@@ -1735,11 +1769,13 @@ ensure_global_extension({
|
|
|
1735
1769
|
description: "Registers the app-wide notification bell in the account panel",
|
|
1736
1770
|
code: notificationBellCode,
|
|
1737
1771
|
isEnabled: true,
|
|
1772
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1738
1773
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1739
1774
|
})`,
|
|
1740
1775
|
notes: [
|
|
1741
1776
|
'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
|
|
1742
1777
|
'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
|
|
1778
|
+
'The notification center is only one possible shell integration. The transferable shape is invisible global extension -> shell registry -> cleanup on unmount.',
|
|
1743
1779
|
'Use useMenuNotificationRegistry for sidebar menu counts/dots when notification state should be visible in the menu as well as the notification center.',
|
|
1744
1780
|
'Choose value only when the signal source already owns an exact count. Omit value for a dot when realtime only proves that something new exists.',
|
|
1745
1781
|
'Do not fetch the destination domain list just to decorate a menu. A mail page fetches mail; a support page fetches tickets; the shell should use notification or summary signals.',
|
|
@@ -1892,14 +1928,16 @@ ensure_global_extension({
|
|
|
1892
1928
|
description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
|
|
1893
1929
|
code: signalBridgeCode,
|
|
1894
1930
|
isEnabled: true,
|
|
1931
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1895
1932
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1896
1933
|
})`,
|
|
1897
1934
|
notes: [
|
|
1898
|
-
'Use this pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
|
|
1935
|
+
'Use this reasoning pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
|
|
1899
1936
|
'This example fetches only the notification source of truth, not the email, support, order, or job tables. Substitute your own notification or summary endpoint when available.',
|
|
1900
1937
|
'Omitting value on registerMenuNotification renders a dot. That is the right promise when the shell knows "new work exists" but not an exact count.',
|
|
1901
1938
|
'If a backend summary event already includes an exact unread count, use value for a count chip. If the event only says one record changed, use a dot and let the page fetch details.',
|
|
1902
1939
|
'Map notification kinds to menu targets by product meaning, not by copying these paths. For example, approval_required could target /reviews, failed_job could target /operations/jobs, and quota_warning could target /billing.',
|
|
1940
|
+
'Generalize the lifecycle: seed from a bounded signal source, react to realtime events, clear local attention when the user reaches the owning page, and avoid duplicating that page\'s data fetch.',
|
|
1903
1941
|
'Clear local dot signals when the user enters the destination route or when the notification center marks the underlying notification as read.',
|
|
1904
1942
|
],
|
|
1905
1943
|
},
|
|
@@ -1939,6 +1977,7 @@ register({
|
|
|
1939
1977
|
</script>`,
|
|
1940
1978
|
notes: [
|
|
1941
1979
|
'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
|
|
1980
|
+
'Notifications is illustrative. Account-panel rows can represent any account-scoped attention or shortcut, such as approvals, billing, deployments, or personal tasks.',
|
|
1942
1981
|
'Use count for the primary visible badge value. badge remains supported as a legacy alias, but count is what the account trigger aggregates.',
|
|
1943
1982
|
'Do not draw a custom full row with page-scale cards, hero headings, large whitespace, or nested buttons unless the shell contract cannot express the UI.',
|
|
1944
1983
|
'Let the Enfyra admin UI handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
|
|
@@ -1993,6 +2032,7 @@ registerHeaderActions([
|
|
|
1993
2032
|
</script>`,
|
|
1994
2033
|
notes: [
|
|
1995
2034
|
'Use PageHeader for the title strip; do not render a duplicate header inside extension body.',
|
|
2035
|
+
'The exact actions are illustrative. Choose action prominence from user intent: navigation, secondary utility, primary mutation, or destructive confirmation.',
|
|
1996
2036
|
'Use gradient: "none" for generated operational pages; hardcoded named gradients are decorative and should be explicit user intent.',
|
|
1997
2037
|
'Back/navigation actions should be neutral ghost so they read as navigation, not a primary operation.',
|
|
1998
2038
|
'Visible secondary operations should be neutral outline; soft is only for low-emphasis chrome actions.',
|
|
@@ -2032,7 +2072,7 @@ registerHeaderActions([
|
|
|
2032
2072
|
},
|
|
2033
2073
|
{
|
|
2034
2074
|
name: 'Plan an admin dashboard as multiple pages',
|
|
2035
|
-
code: `//
|
|
2075
|
+
code: `// Illustrative menu shape for an operations surface:
|
|
2036
2076
|
ensure_menu({
|
|
2037
2077
|
type: "Dropdown Menu",
|
|
2038
2078
|
label: "Operations",
|
|
@@ -2058,6 +2098,7 @@ ensure_menu({
|
|
|
2058
2098
|
// For admin record management, link to /data/<table>, e.g. /data/report, not public website paths.`,
|
|
2059
2099
|
notes: [
|
|
2060
2100
|
'Design the menu/page split before generating dashboard code.',
|
|
2101
|
+
'Operations/jobs/orders/reports/settings are examples of separating mental models. Replace them with the real domains users navigate between.',
|
|
2061
2102
|
'Permission-gate sensitive parent dropdown menus too, using any child page route or backing route that represents read access.',
|
|
2062
2103
|
'Keep /dashboard as a summary and distribution page, not a detailed operations table.',
|
|
2063
2104
|
'Use focused pages for operational domains.',
|
|
@@ -2089,6 +2130,7 @@ onMounted(() => fetchOrders())
|
|
|
2089
2130
|
notes: [
|
|
2090
2131
|
'Use app-provided composables in extensions.',
|
|
2091
2132
|
'useApi does not auto-run; call execute() on mounted or through an action.',
|
|
2133
|
+
'The /order path is illustrative; inspect routes and fetch the smallest data shape the extension needs.',
|
|
2092
2134
|
'Keep extension UI focused; move backend logic into handlers/hooks when needed.',
|
|
2093
2135
|
],
|
|
2094
2136
|
},
|
|
@@ -2144,7 +2186,8 @@ console.log(ok, requiredTerms.has('terms'), loaded, label, date)
|
|
|
2144
2186
|
name: 'Install and use an app package in an extension',
|
|
2145
2187
|
code: `install_package({
|
|
2146
2188
|
name: "dayjs",
|
|
2147
|
-
type: "App"
|
|
2189
|
+
type: "App",
|
|
2190
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>"
|
|
2148
2191
|
})
|
|
2149
2192
|
|
|
2150
2193
|
// Then in extension code:
|
|
@@ -2215,6 +2258,7 @@ onMounted(() => Promise.all([flowStats.execute(), orderStats.execute()]))
|
|
|
2215
2258
|
'Aggregate keys must be real fields or relations.',
|
|
2216
2259
|
'Read results from response.meta.aggregate.',
|
|
2217
2260
|
'Use top-level filter for time windows and cross-field conditions.',
|
|
2261
|
+
'The flow/order pair is illustrative. Choose aggregates that answer the page question, such as failed work, pending approvals, unread support, quota pressure, or revenue.',
|
|
2218
2262
|
'Only aggregate fields and relations that the dashboard is allowed to expose; aggregate values can reveal hidden data even when rows omit that field.',
|
|
2219
2263
|
'sum/avg require numeric fields; amount_usd must be a real float/numeric SQL column, not metadata-only float over a varchar physical column.',
|
|
2220
2264
|
],
|
|
@@ -2234,6 +2278,7 @@ export function listExampleCategories() {
|
|
|
2234
2278
|
export function getExamples(category) {
|
|
2235
2279
|
if (!category) {
|
|
2236
2280
|
return {
|
|
2281
|
+
reasoningGuide: EXAMPLE_REASONING_GUIDE,
|
|
2237
2282
|
categories: listExampleCategories(),
|
|
2238
2283
|
hint: 'Call get_enfyra_examples with one category key to retrieve concrete examples for that area.',
|
|
2239
2284
|
};
|
|
@@ -2249,6 +2294,7 @@ export function getExamples(category) {
|
|
|
2249
2294
|
|
|
2250
2295
|
return {
|
|
2251
2296
|
category,
|
|
2297
|
+
reasoningGuide: EXAMPLE_REASONING_GUIDE,
|
|
2252
2298
|
...entry,
|
|
2253
2299
|
};
|
|
2254
2300
|
}
|
|
@@ -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
|
|
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-
|
|
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
|
|
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',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -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.` }] };
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -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
|
-
|
|
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)', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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 = '';
|