@enfyra/mcp-server 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +29 -20
- package/src/lib/mcp-instructions.js +1 -1
- package/src/lib/platform-operation-tools.js +85 -21
- package/src/lib/required-knowledge.js +63 -2
- package/src/lib/table-tools.js +86 -35
- package/src/mcp-server-entry.mjs +97 -48
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
|
@@ -551,6 +551,7 @@ update_record({
|
|
|
551
551
|
name: 'Create a chat conversation table',
|
|
552
552
|
code: `create_table({
|
|
553
553
|
name: "chat_conversation",
|
|
554
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
554
555
|
columns: JSON.stringify([
|
|
555
556
|
{ name: "kind", type: "varchar", isNullable: false, defaultValue: "dm" },
|
|
556
557
|
{ name: "title", type: "varchar", isNullable: true },
|
|
@@ -568,6 +569,7 @@ update_record({
|
|
|
568
569
|
name: 'Create relations directly to enfyra_user',
|
|
569
570
|
code: `create_table({
|
|
570
571
|
name: "chat_message",
|
|
572
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
571
573
|
columns: JSON.stringify([
|
|
572
574
|
{ name: "text", type: "text", isNullable: false },
|
|
573
575
|
{ name: "persistStatus", type: "varchar", defaultValue: "persisted" }
|
|
@@ -607,6 +609,7 @@ update_record({
|
|
|
607
609
|
code: `create_relation({
|
|
608
610
|
sourceTableId: "chat_message",
|
|
609
611
|
targetTableId: "chat_conversation",
|
|
612
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
610
613
|
propertyName: "conversation",
|
|
611
614
|
inversePropertyName: "messages",
|
|
612
615
|
type: "many-to-one",
|
|
@@ -625,6 +628,7 @@ update_record({
|
|
|
625
628
|
name: 'Add chat_conversation.lastMessage after chat_message exists',
|
|
626
629
|
code: `update_table({
|
|
627
630
|
tableId: "<chat_conversation_id>",
|
|
631
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
628
632
|
relations: JSON.stringify([
|
|
629
633
|
{
|
|
630
634
|
propertyName: "createdBy",
|
|
@@ -651,6 +655,7 @@ update_record({
|
|
|
651
655
|
name: 'Unread/read table with unique and indexes',
|
|
652
656
|
code: `create_table({
|
|
653
657
|
name: "chat_message_read",
|
|
658
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
654
659
|
columns: JSON.stringify([
|
|
655
660
|
{ name: "isRead", type: "boolean", defaultValue: false },
|
|
656
661
|
{ name: "readAt", type: "datetime", isNullable: true }
|
|
@@ -677,6 +682,7 @@ update_record({
|
|
|
677
682
|
code: `create_column({
|
|
678
683
|
tableId: "<enfyra_user_table_id>",
|
|
679
684
|
name: "emailVerifiedAt",
|
|
685
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
680
686
|
type: "datetime",
|
|
681
687
|
isNullable: true,
|
|
682
688
|
isPublished: true,
|
|
@@ -686,6 +692,7 @@ update_record({
|
|
|
686
692
|
create_column({
|
|
687
693
|
tableId: "<enfyra_user_table_id>",
|
|
688
694
|
name: "emailVerificationStatus",
|
|
695
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
689
696
|
type: "varchar",
|
|
690
697
|
isNullable: false,
|
|
691
698
|
defaultValue: "pending",
|
|
@@ -696,6 +703,7 @@ create_column({
|
|
|
696
703
|
create_column({
|
|
697
704
|
tableId: "<integration_secret_table_id>",
|
|
698
705
|
name: "value",
|
|
706
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
699
707
|
type: "text",
|
|
700
708
|
isNullable: false,
|
|
701
709
|
isPublished: false,
|
|
@@ -722,6 +730,7 @@ create_column({
|
|
|
722
730
|
create_column({
|
|
723
731
|
tableId: "<table_id>",
|
|
724
732
|
name: "api_secret",
|
|
733
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
725
734
|
type: "text",
|
|
726
735
|
isPublished: false,
|
|
727
736
|
isEncrypted: true
|
|
@@ -923,6 +932,7 @@ GET /enfyra/integrations?filter={"api_token":{"_eq":"plaintext-token"}}
|
|
|
923
932
|
create_column({
|
|
924
933
|
tableId: "<integrations_table_id>",
|
|
925
934
|
name: "api_token_lookup_sha256",
|
|
935
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
926
936
|
type: "varchar",
|
|
927
937
|
isNullable: false,
|
|
928
938
|
isPublished: false
|
|
@@ -958,6 +968,7 @@ const found = await #integrations.find({
|
|
|
958
968
|
routeId: "<route_id>",
|
|
959
969
|
method: "POST",
|
|
960
970
|
scriptLanguage: "javascript",
|
|
971
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
961
972
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
962
973
|
sourceCode: \`const email = @BODY.email
|
|
963
974
|
if (!email) @THROW400("Email is required")
|
|
@@ -966,7 +977,7 @@ return { ok: true, email }\`
|
|
|
966
977
|
})`,
|
|
967
978
|
notes: [
|
|
968
979
|
'Use sourceCode, not logic. The server generates compiledCode.',
|
|
969
|
-
'Call get_enfyra_required_knowledge before saving dynamic code and pass dynamicCodeAckKey as knowledgeAckKey.',
|
|
980
|
+
'Call get_enfyra_required_knowledge before saving dynamic code, pass globalRulesAckKey as globalRulesAckKey, and pass dynamicCodeAckKey as knowledgeAckKey.',
|
|
970
981
|
'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
|
|
971
982
|
'Do not pass name to enfyra_route_handler; one handler is identified by route + method.',
|
|
972
983
|
],
|
|
@@ -1020,6 +1031,7 @@ const scope = {
|
|
|
1020
1031
|
name: 'Encrypted field table definition',
|
|
1021
1032
|
code: `create_table({
|
|
1022
1033
|
name: "integrations",
|
|
1034
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1023
1035
|
columns: JSON.stringify([
|
|
1024
1036
|
{ name: "name", type: "varchar", isNullable: false },
|
|
1025
1037
|
{
|
|
@@ -1047,6 +1059,7 @@ const scope = {
|
|
|
1047
1059
|
name: "strip_email_verification_fields",
|
|
1048
1060
|
methods: ["PATCH"],
|
|
1049
1061
|
priority: -10,
|
|
1062
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1050
1063
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1051
1064
|
code: \`delete @BODY.emailVerifiedAt
|
|
1052
1065
|
delete @BODY.emailVerificationStatus
|
|
@@ -1065,6 +1078,7 @@ delete @BODY.emailVerificationSentAt\`
|
|
|
1065
1078
|
name: "shape_display_title",
|
|
1066
1079
|
methods: ["GET"],
|
|
1067
1080
|
priority: 0,
|
|
1081
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1068
1082
|
knowledgeAckKey: "<dynamicCodeAckKey from get_enfyra_required_knowledge>",
|
|
1069
1083
|
code: \`if (@ERROR) {
|
|
1070
1084
|
@LOGS("Request failed", @ERROR.message)
|
|
@@ -1558,6 +1572,7 @@ update_method({
|
|
|
1558
1572
|
icon: "lucide:bar-chart-3",
|
|
1559
1573
|
order: 20,
|
|
1560
1574
|
isEnabled: true,
|
|
1575
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1561
1576
|
permission: JSON.stringify({
|
|
1562
1577
|
or: [
|
|
1563
1578
|
{ route: "/reports", methods: ["GET"] },
|
|
@@ -1573,6 +1588,7 @@ ensure_page_extension({
|
|
|
1573
1588
|
menuId: "<created-menu-id>",
|
|
1574
1589
|
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\\"><article class=\\"eapp-surface-card p-4\\"><div class=\\"flex items-start justify-between gap-3\\"><div><p class=\\"text-sm font-medium eapp-text-tertiary\\">Total</p><p class=\\"mt-2 text-2xl font-semibold eapp-text-primary\\">0</p></div><span class=\\"eapp-primary-soft eapp-icon-tile\\"><span class=\\"eapp-primary-text\\">◆</span></span></div><div class=\\"mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted\\"><div class=\\"eapp-primary-solid h-full w-1/2\\"></div></div></article><article class=\\"eapp-primary-surface eapp-radius-panel border p-4\\"><p class=\\"text-sm font-semibold eapp-text-primary\\">Selected report</p><p class=\\"mt-1 text-sm eapp-text-tertiary\\">Only selected/current identity blocks use identity surface.</p></article></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); const { register: registerHeaderActions } = useHeaderActionRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); registerHeaderActions([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', color: 'neutral', variant: 'outline', onClick: () => {}, order: 80 }])</script>",
|
|
1575
1590
|
isEnabled: true,
|
|
1591
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1576
1592
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1577
1593
|
})`,
|
|
1578
1594
|
notes: [
|
|
@@ -1581,26 +1597,14 @@ ensure_page_extension({
|
|
|
1581
1597
|
'Use enfyra_menu.label, not title.',
|
|
1582
1598
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1583
1599
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1584
|
-
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
|
|
1585
|
-
'Call get_enfyra_required_knowledge before saving extension code and pass extensionAckKey as extensionKnowledgeAckKey.',
|
|
1600
|
+
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI; that tool is the authority for theme, color, layout, modal, drawer, and shell registry details.',
|
|
1601
|
+
'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
|
|
1586
1602
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1587
|
-
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1588
|
-
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
1589
1603
|
'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
|
|
1590
|
-
'Page extensions should be full-bleed
|
|
1591
|
-
'
|
|
1592
|
-
'Use
|
|
1593
|
-
'
|
|
1594
|
-
'Decision cases: normal decorative accents, feature icons, non-state tiles, active tabs, progress fills, selected segments, and primary actions use runtime primary/identity classes; true semantic states use their matching status colors such as error, warning, success, or info; large ordinary surfaces stay neutral and carry only small badges/icons for status; selected/current entity blocks may use eapp-primary-surface.',
|
|
1595
|
-
'Pattern examples: KPI/metric cards should be eapp-surface-card with a small icon tile using eapp-primary-soft eapp-icon-tile; selected/current entity cards may use eapp-primary-surface; progress bars use eapp-surface-muted tracks plus eapp-primary-solid fills; list rows use eapp-surface-card/eapp-divide-y/eapp-surface-hover and only small chips inside; primary scope actions use UButton color="primary" variant="solid"; secondary actions use neutral variants.',
|
|
1596
|
-
'Status colors belong only in UBadge/UAlert semantic colors or eapp-status-*-soft/text/border classes for badges, small icons, or short status text. Do not read --badge-* variables directly in extension templates. Do not color large panels, alert-like success blocks, KPI cards, list containers, or attention/reconciliation blocks green/yellow/red because of state; keep the block neutral and place the status badge/icon inside.',
|
|
1597
|
-
'Use PageHeader gradient: "none" for generated operational pages unless the user explicitly asks for a decorative page accent; do not hardcode cyan/violet/purple/blue/green gradients.',
|
|
1598
|
-
'For general card grids inside the shell, use md:grid-cols-2 xl:grid-cols-3 instead of lg:grid-cols-3 because the desktop sidebar leaves tablet-width content at 1024px.',
|
|
1599
|
-
'Do not use Nuxt UI neutral semantic classes such as bg-default, text-muted, text-dimmed, border-default, or divide-default inside extension code; use eApp class tokens instead. Do not write text-[var(...)], bg-[var(...)], or border-[var(...)] in generated extension templates unless no class token exists for that exact primitive.',
|
|
1600
|
-
'Do not pass ui.content: "eapp-surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller content classes should only append z-index or width.',
|
|
1601
|
-
'CommonModal and CommonDrawer own action-only footers through cancelAction, primaryAction, dangerAction, leadingActions, and footerHint. Pass action intent through props instead of styling footer buttons manually; cancelAction defaults to neutral outline, dangerAction is for irreversible destructive work, and Keep editing should use tone: "primary" in discard dialogs.',
|
|
1602
|
-
'Use UTabs for page sections instead of custom tab bars so the app-level active/inactive indicators, spacing, focus rings, and theme contrast stay consistent.',
|
|
1603
|
-
'Do not inject global CSS, create theme guards, redefine the app palette, or solve one extension by overriding the whole app shell.',
|
|
1604
|
+
'Page extensions should be full-bleed and responsive from the first version; the extension root is already inside the Enfyra admin page main.',
|
|
1605
|
+
'Render ordinary metrics and lists in the body, not PageHeader.stats, unless the user explicitly wants a compact overview header.',
|
|
1606
|
+
'Use app theme tokens and Nuxt UI semantic colors by intent; do not hard-code concrete palettes or redefine the app palette inside extension code.',
|
|
1607
|
+
'Use app-owned primitives such as UTabs, CommonModal, CommonDrawer, Widget, useMenuNotificationRegistry, and useAccountPanelRegistry when the workflow matches them.',
|
|
1604
1608
|
'Keep list selection local and fetch detail rows only; do not refetch the whole list after a row click unless the list data changed.',
|
|
1605
1609
|
'Page extension paths are admin app UI routes. Do not verify them with test_rest_endpoint against ENFYRA_API_URL unless inspect_route shows an API route with the same path.',
|
|
1606
1610
|
'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
|
|
@@ -1648,6 +1652,7 @@ ensure_widget_extension({
|
|
|
1648
1652
|
description: "Report status summary cards",
|
|
1649
1653
|
code: reportStatusWidgetCode,
|
|
1650
1654
|
isEnabled: true,
|
|
1655
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1651
1656
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1652
1657
|
})
|
|
1653
1658
|
|
|
@@ -1657,6 +1662,7 @@ ensure_page_extension({
|
|
|
1657
1662
|
menuId: "<reports-menu-id>",
|
|
1658
1663
|
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
|
|
1659
1664
|
isEnabled: true,
|
|
1665
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1660
1666
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1661
1667
|
})`,
|
|
1662
1668
|
notes: [
|
|
@@ -1751,6 +1757,7 @@ ensure_global_extension({
|
|
|
1751
1757
|
description: "Registers the app-wide notification bell in the account panel",
|
|
1752
1758
|
code: notificationBellCode,
|
|
1753
1759
|
isEnabled: true,
|
|
1760
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1754
1761
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1755
1762
|
})`,
|
|
1756
1763
|
notes: [
|
|
@@ -1909,6 +1916,7 @@ ensure_global_extension({
|
|
|
1909
1916
|
description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
|
|
1910
1917
|
code: signalBridgeCode,
|
|
1911
1918
|
isEnabled: true,
|
|
1919
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
|
|
1912
1920
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1913
1921
|
})`,
|
|
1914
1922
|
notes: [
|
|
@@ -2166,7 +2174,8 @@ console.log(ok, requiredTerms.has('terms'), loaded, label, date)
|
|
|
2166
2174
|
name: 'Install and use an app package in an extension',
|
|
2167
2175
|
code: `install_package({
|
|
2168
2176
|
name: "dayjs",
|
|
2169
|
-
type: "App"
|
|
2177
|
+
type: "App",
|
|
2178
|
+
globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>"
|
|
2170
2179
|
})
|
|
2171
2180
|
|
|
2172
2181
|
// Then in extension code:
|
|
@@ -29,7 +29,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
29
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
30
|
'- Load examples only when needed. Use `get_enfyra_examples` by category. Before extension UI, call `get_extension_theme_contract`; call `get_theme_class_reference` for exact eapp/Nuxt UI theme classes.',
|
|
31
31
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
|
-
'- Before
|
|
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.',
|