diff --git a/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al b/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al index a2d44f50c3..d3f26d1af5 100644 --- a/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al +++ b/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al @@ -14,5 +14,7 @@ permissionset 8350 "MCP - Objects" Permissions = table "MCP API Publisher Group" = X, table "MCP Configuration" = X, table "MCP Configuration Tool" = X, - table "MCP Entra Application" = X; -} \ No newline at end of file + table "MCP Config Warning" = X, + table "MCP Entra Application" = X, + table "MCP System Tool" = X; +} diff --git a/src/System Application/App/MCP/app.json b/src/System Application/App/MCP/app.json index 60607910b1..1594c07602 100644 --- a/src/System Application/App/MCP/app.json +++ b/src/System Application/App/MCP/app.json @@ -53,6 +53,10 @@ { "from": 8350, "to": 8360 + }, + { + "from": 8365, + "to": 8370 } ], "target": "OnPrem", diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfig.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfig.Codeunit.al index f615c9bbcd..b2e613ca66 100644 --- a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfig.Codeunit.al +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfig.Codeunit.al @@ -98,6 +98,26 @@ codeunit 8350 "MCP Config" MCPConfigImplementation.EnableDiscoverReadOnlyObjects(ConfigId, Enable); end; + /// + /// Finds warnings for the specified MCP configuration, such as missing objects or missing parent objects. + /// + /// The SystemId (GUID) of the configuration to find warnings for. + /// A temporary record variable to hold the found warnings. + /// True if any warnings were found; otherwise, false. + procedure FindWarningsForConfiguration(ConfigId: Guid; var MCPConfigWarning: Record "MCP Config Warning"): Boolean + begin + exit(MCPConfigImplementation.FindWarningsForConfiguration(ConfigId, MCPConfigWarning)); + end; + + /// + /// Applies the recommended action for the specified warning. + /// + /// The warning record to apply the recommended action for. + procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning") + begin + MCPConfigImplementation.ApplyRecommendedAction(MCPConfigWarning); + end; + /// /// Creates a new API tool for the specified configuration and API page. /// diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al index c132d50bae..dfdfb8fcff 100644 --- a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al @@ -37,6 +37,8 @@ codeunit 8351 "MCP Config Implementation" MCPConfigurationAuditCreatedLbl: Label 'MCP Configuration %1 created by user %2 in company %3', Comment = '%1 - configuration name, %2 - user security ID, %3 - company name', Locked = true; MCPConfigurationAuditModifiedLbl: Label 'MCP Configuration %1 modified by user %2 in company %3', Comment = '%1 - configuration name, %2 - user security ID, %3 - company name', Locked = true; MCPConfigurationAuditDeletedLbl: Label 'MCP Configuration %1 deleted by user %2 in company %3', Comment = '%1 - configuration name, %2 - user security ID, %3 - company name', Locked = true; + InvalidConfigurationWarningLbl: Label 'The configuration is invalid and may not work as expected. Do you want to review warnings before activating?'; + ConfigValidLbl: Label 'No warnings found. The configuration is valid.'; ConnectionStringLbl: Label '%1 Connection String', Comment = '%1 - configuration name'; MCPUrlProdLbl: Label 'https://mcp.businesscentral.dynamics.com', Locked = true; MCPUrlTIELbl: Label 'https://mcp.businesscentral.dynamics-tie.com', Locked = true; @@ -266,6 +268,90 @@ codeunit 8351 "MCP Config Implementation" begin exit(MCPConfiguration.Name = ''); end; + + internal procedure IsConfigurationActive(ConfigId: Guid): Boolean + var + MCPConfiguration: Record "MCP Configuration"; + begin + if MCPConfiguration.GetBySystemId(ConfigId) then + exit(MCPConfiguration.Active); + exit(false); + end; + + internal procedure ValidateConfiguration(var MCPConfiguration: Record "MCP Configuration"; OnActivate: Boolean) + var + MCPConfigurationWarning: Record "MCP Config Warning"; + begin + // Raise warning if any issues found + if not FindWarningsForConfiguration(MCPConfiguration.SystemId, MCPConfigurationWarning) then begin + if not OnActivate then + Message(ConfigValidLbl); + exit; + end; + + if OnActivate then + if not Confirm(InvalidConfigurationWarningLbl) then + exit; + + MCPConfiguration.Active := false; + Page.Run(Page::"MCP Config Warning List", MCPConfigurationWarning); + end; + + internal procedure FindWarningsForConfiguration(ConfigId: Guid; var MCPConfigurationWarning: Record "MCP Config Warning"): Boolean + var + IMCPConfigWarning: Interface "MCP Config Warning"; + MCPConfigWarningType: Enum "MCP Config Warning Type"; + WarningImplementations: List of [Integer]; + WarningImplementation: Integer; + EntryNo: Integer; + begin + if MCPConfigurationWarning.FindLast() then + EntryNo := MCPConfigurationWarning."Entry No." + 1 + else + EntryNo := 1; + + WarningImplementations := MCPConfigWarningType.Ordinals(); + foreach WarningImplementation in WarningImplementations do begin + IMCPConfigWarning := "MCP Config Warning Type".FromInteger(WarningImplementation); + IMCPConfigWarning.CheckForWarnings(ConfigId, MCPConfigurationWarning, EntryNo); + end; + + exit(not MCPConfigurationWarning.IsEmpty()); + end; + + internal procedure GetWarningMessage(MCPConfigWarning: Record "MCP Config Warning"): Text + var + IMCPConfigWarning: Interface "MCP Config Warning"; + begin + IMCPConfigWarning := MCPConfigWarning."Warning Type"; + exit(IMCPConfigWarning.WarningMessage(MCPConfigWarning)); + end; + + internal procedure GetRecommendedAction(MCPConfigWarning: Record "MCP Config Warning"): Text + var + IMCPConfigWarning: Interface "MCP Config Warning"; + begin + IMCPConfigWarning := MCPConfigWarning."Warning Type"; + exit(IMCPConfigWarning.RecommendedAction(MCPConfigWarning)); + end; + + internal procedure ApplyRecommendedActions(var MCPConfigWarning: Record "MCP Config Warning") + begin + if not MCPConfigWarning.FindSet() then + exit; + + repeat + ApplyRecommendedAction(MCPConfigWarning); + until MCPConfigWarning.Next() = 0; + end; + + internal procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning") + var + IMCPConfigWarning: Interface "MCP Config Warning"; + begin + IMCPConfigWarning := MCPConfigWarning."Warning Type"; + IMCPConfigWarning.ApplyRecommendedAction(MCPConfigWarning); + end; #endregion #region Tools @@ -503,7 +589,7 @@ codeunit 8351 "MCP Config Implementation" until PageMetadata.Next() = 0; end; - local procedure CheckAPIToolExists(ConfigId: Guid; PageId: Integer): Boolean + internal procedure CheckAPIToolExists(ConfigId: Guid; PageId: Integer): Boolean var MCPConfigurationTool: Record "MCP Configuration Tool"; begin @@ -529,6 +615,27 @@ codeunit 8351 "MCP Config Implementation" exit(CopyStr(AllObjWithCaption."Object Name", 1, 100)); exit(''); end; + + internal procedure LoadSystemTools(var MCPSystemTool: Record "MCP System Tool") + var + MCPUtilities: Codeunit "MCP Utilities"; + SystemTools: Dictionary of [Text, Text]; + ToolName: Text; + begin + MCPSystemTool.Reset(); + MCPSystemTool.DeleteAll(); + + SystemTools := MCPUtilities.GetSystemToolsInDynamicMode(); + foreach ToolName in SystemTools.Keys() do + InsertSystemTool(MCPSystemTool, CopyStr(ToolName, 1, MaxStrLen(MCPSystemTool."Tool Name")), CopyStr(SystemTools.Get(ToolName), 1, MaxStrLen(MCPSystemTool."Tool Description"))); + end; + + local procedure InsertSystemTool(var MCPSystemTool: Record "MCP System Tool"; ToolName: Text[100]; ToolDescription: Text[250]) + begin + MCPSystemTool."Tool Name" := ToolName; + MCPSystemTool."Tool Description" := ToolDescription; + MCPSystemTool.Insert(); + end; #endregion #region Connection String diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingObject.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingObject.Codeunit.al new file mode 100644 index 0000000000..a31c6acc3f --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingObject.Codeunit.al @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +using System.Reflection; + +codeunit 8353 "MCP Config Missing Object" implements "MCP Config Warning" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MissingObjectWarningLbl: Label '%1 (%2) referenced by this configuration no longer exists in the system.', Comment = '%1=Object type, %2=Object Id'; + MissingObjectFixLbl: Label 'Remove this tool from the configuration.'; + + procedure CheckForWarnings(ConfigId: Guid; var MCPConfigWarning: Record "MCP Config Warning"; var EntryNo: Integer) + var + AllObj: Record AllObj; + begin + MCPConfigurationTool.SetRange(ID, ConfigId); + if MCPConfigurationTool.FindSet() then + repeat + AllObj.SetRange("Object Type", AllObj."Object Type"::Page); + AllObj.SetRange("Object ID", MCPConfigurationTool."Object ID"); + if AllObj.IsEmpty() then begin + MCPConfigWarning."Entry No." := EntryNo; + MCPConfigWarning."Config Id" := ConfigId; + MCPConfigWarning."Tool Id" := MCPConfigurationTool.SystemId; + MCPConfigWarning."Warning Type" := MCPConfigWarning."Warning Type"::"Missing Object"; + MCPConfigWarning.Insert(); + EntryNo += 1; + end; + until MCPConfigurationTool.Next() = 0; + end; + + procedure WarningMessage(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + if MCPConfigurationTool.GetBySystemId(MCPConfigWarning."Tool Id") then + exit(StrSubstNo(MissingObjectWarningLbl, MCPConfigurationTool."Object Type", MCPConfigurationTool."Object Id")); + end; + + procedure RecommendedAction(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + exit(MissingObjectFixLbl); + end; + + procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning") + begin + if MCPConfigurationTool.GetBySystemId(MCPConfigWarning."Tool Id") then + MCPConfigurationTool.Delete(); + MCPConfigWarning.Delete(); + end; +} diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingParent.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingParent.Codeunit.al new file mode 100644 index 0000000000..e0856d1cb4 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingParent.Codeunit.al @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +using System.Reflection; + +codeunit 8354 "MCP Config Missing Parent" implements "MCP Config Warning" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + MissingParentWarningLbl: Label 'This API page is missing parent page(s): %1', Comment = '%1 = comma-separated list of missing parent page IDs'; + MissingParentFixLbl: Label 'Add the parent API pages to the configuration.'; + + procedure CheckForWarnings(ConfigId: Guid; var MCPConfigWarning: Record "MCP Config Warning"; var EntryNo: Integer) + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + PageMetadata: Record "Page Metadata"; + MCPUtilities: Codeunit "MCP Utilities"; + PageIdVersions: Dictionary of [Integer, Text]; + ParentMCPTools: Dictionary of [Integer, List of [Integer]]; + ParentPageIds: List of [Integer]; + MissingParentIds: List of [Integer]; + PageId: Integer; + ParentPageId: Integer; + MissingParentsText: Text; + begin + // Build dictionary of page IDs and API versions from configuration tools + MCPConfigurationTool.SetRange(ID, ConfigId); + MCPConfigurationTool.SetRange("Object Type", MCPConfigurationTool."Object Type"::Page); + if not MCPConfigurationTool.FindSet() then + exit; + + repeat + if PageMetadata.Get(MCPConfigurationTool."Object ID") then + if PageMetadata.PageType = PageMetadata.PageType::API then + PageIdVersions.Add(MCPConfigurationTool."Object ID", PageMetadata.APIVersion); + until MCPConfigurationTool.Next() = 0; + + // Get parent mappings from platform + ParentMCPTools := MCPUtilities.GetParentMCPTools(PageIdVersions); + + // Check each page with parents for missing parent tools + foreach PageId in ParentMCPTools.Keys() do begin + ParentPageIds := ParentMCPTools.Get(PageId); + Clear(MissingParentIds); + + // Check if each parent exists in the configuration + foreach ParentPageId in ParentPageIds do + if not PageIdVersions.ContainsKey(ParentPageId) then + MissingParentIds.Add(ParentPageId); + + // Create warning if there are missing parents + if MissingParentIds.Count() > 0 then begin + // Get the tool record to retrieve its SystemId + MCPConfigurationTool.Get(ConfigId, MCPConfigurationTool."Object Type"::Page, PageId); + + MissingParentsText := FormatPageIdList(MissingParentIds); + MCPConfigWarning."Entry No." := EntryNo; + MCPConfigWarning."Config Id" := ConfigId; + MCPConfigWarning."Tool Id" := MCPConfigurationTool.SystemId; + MCPConfigWarning."Warning Type" := MCPConfigWarning."Warning Type"::"Missing Parent Object"; + MCPConfigWarning."Additional Info" := CopyStr(MissingParentsText, 1, MaxStrLen(MCPConfigWarning."Additional Info")); + MCPConfigWarning.Insert(); + EntryNo += 1; + end; + end; + end; + + procedure WarningMessage(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + exit(StrSubstNo(MissingParentWarningLbl, MCPConfigWarning."Additional Info")); + end; + + procedure RecommendedAction(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + exit(MissingParentFixLbl); + end; + + procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning") + var + MCPConfigImplementation: Codeunit "MCP Config Implementation"; + PageIdList: List of [Text]; + PageIdText: Text; + PageId: Integer; + begin + if MCPConfigWarning."Additional Info" = '' then + exit; + + // Parse comma-separated page IDs and add each as a tool + PageIdList := MCPConfigWarning."Additional Info".Split(','); + foreach PageIdText in PageIdList do + if Evaluate(PageId, PageIdText.Trim()) then + if not MCPConfigImplementation.CheckAPIToolExists(MCPConfigWarning."Config Id", PageId) then + MCPConfigImplementation.CreateAPITool(MCPConfigWarning."Config Id", PageId, false); + + MCPConfigWarning.Delete(); + end; + + local procedure FormatPageIdList(PageIds: List of [Integer]): Text + var + PageId: Integer; + Result: TextBuilder; + begin + foreach PageId in PageIds do begin + Result.Append(Format(PageId)); + Result.Append(', '); + end; + exit(Result.ToText().TrimEnd(', ')); + end; +} diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingReadTool.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingReadTool.Codeunit.al new file mode 100644 index 0000000000..d3aa5961de --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingReadTool.Codeunit.al @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +codeunit 8355 "MCP Config Missing Read Tool" implements "MCP Config Warning" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MissingReadToolWarningLbl: Label '%1 (%2) has Allow Modify enabled but Allow Read is disabled.', Comment = '%1=Object type, %2=Object Id'; + MissingReadToolFixLbl: Label 'Enable Allow Read for this tool.'; + + procedure CheckForWarnings(ConfigId: Guid; var MCPConfigWarning: Record "MCP Config Warning"; var EntryNo: Integer) + begin + MCPConfigurationTool.SetRange(ID, ConfigId); + MCPConfigurationTool.SetRange("Allow Modify", true); + MCPConfigurationTool.SetRange("Allow Read", false); + if MCPConfigurationTool.FindSet() then + repeat + MCPConfigWarning."Entry No." := EntryNo; + MCPConfigWarning."Config Id" := ConfigId; + MCPConfigWarning."Tool Id" := MCPConfigurationTool.SystemId; + MCPConfigWarning."Warning Type" := MCPConfigWarning."Warning Type"::"Missing Read Tool"; + MCPConfigWarning.Insert(); + EntryNo += 1; + until MCPConfigurationTool.Next() = 0; + end; + + procedure WarningMessage(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + if MCPConfigurationTool.GetBySystemId(MCPConfigWarning."Tool Id") then + exit(StrSubstNo(MissingReadToolWarningLbl, MCPConfigurationTool."Object Type", MCPConfigurationTool."Object ID")); + end; + + procedure RecommendedAction(MCPConfigWarning: Record "MCP Config Warning"): Text + begin + exit(MissingReadToolFixLbl); + end; + + procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning") + begin + if MCPConfigurationTool.GetBySystemId(MCPConfigWarning."Tool Id") then begin + MCPConfigurationTool."Allow Read" := true; + MCPConfigurationTool.Modify(); + end; + MCPConfigWarning.Delete(); + end; +} diff --git a/src/System Application/App/MCP/src/Configuration/Enums/MCPConfigWarningType.Enum.al b/src/System Application/App/MCP/src/Configuration/Enums/MCPConfigWarningType.Enum.al new file mode 100644 index 0000000000..7fa0432bac --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Enums/MCPConfigWarningType.Enum.al @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +enum 8350 "MCP Config Warning Type" implements "MCP Config Warning" +{ + Access = Public; + Extensible = false; + + value(0; "Missing Object") + { + Caption = 'Missing Object'; + Implementation = "MCP Config Warning" = "MCP Config Missing Object"; + } + value(1; "Missing Parent Object") + { + Caption = 'Missing Parent Object'; + Implementation = "MCP Config Warning" = "MCP Config Missing Parent"; + } + value(2; "Missing Read Tool") + { + Caption = 'Missing Read Tool'; + Implementation = "MCP Config Warning" = "MCP Config Missing Read Tool"; + } +} diff --git a/src/System Application/App/MCP/src/Configuration/Interfaces/MCPConfigWarning.Interface.al b/src/System Application/App/MCP/src/Configuration/Interfaces/MCPConfigWarning.Interface.al new file mode 100644 index 0000000000..72e8c78dc7 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Interfaces/MCPConfigWarning.Interface.al @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +interface "MCP Config Warning" +{ + Access = Internal; + + procedure CheckForWarnings(ConfigId: Guid; var MCPConfigWarning: Record "MCP Config Warning"; var EntryNo: Integer); + procedure WarningMessage(MCPConfigWarning: Record "MCP Config Warning"): Text; + procedure RecommendedAction(MCPConfigWarning: Record "MCP Config Warning"): Text; + procedure ApplyRecommendedAction(var MCPConfigWarning: Record "MCP Config Warning"); +} diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigCard.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigCard.Page.al index f3d4823194..a304705e76 100644 --- a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigCard.Page.al +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigCard.Page.al @@ -15,7 +15,7 @@ page 8351 "MCP Config Card" InherentEntitlements = X; InherentPermissions = X; AboutTitle = 'About model context protocol (MCP) server configuration'; - AboutText = 'Manage how MCP configurations are set up. Specify which APIs are available as tools, control data access permissions, and enable dynamic discovery of tools. You can also duplicate existing configurations to quickly create new setups.'; + AboutText = 'Manage how MCP configurations are set up. Specify which APIs are available as tools, control data access permissions, and enable dynamic discovery of tools. You can also duplicate existing configurations to quickly create new setups. Configurations are read-only when activated to ensure stability.'; layout { @@ -26,33 +26,44 @@ page 8351 "MCP Config Card" Caption = 'General'; field(Name; Rec.Name) { - Editable = not IsDefault; + Editable = not IsDefault and not Rec.Active; } field(Description; Rec.Description) { - Editable = not IsDefault; + Editable = not IsDefault and not Rec.Active; MultiLine = true; } field(Active; Rec.Active) { Editable = not IsDefault; + + trigger OnValidate() + begin + if Rec.Active then + MCPConfigImplementation.ValidateConfiguration(Rec, true); + end; } field(EnableDynamicToolMode; Rec.EnableDynamicToolMode) { - Editable = not IsDefault; + Editable = not IsDefault and not Rec.Active; trigger OnValidate() begin if not Rec.EnableDynamicToolMode then Rec.DiscoverReadOnlyObjects := false; + + GetToolModeDescription(); + CurrPage.Update(); end; } field(DiscoverReadOnlyObjects; Rec.DiscoverReadOnlyObjects) { - Editable = not IsDefault and Rec.EnableDynamicToolMode; + Editable = not IsDefault and Rec.EnableDynamicToolMode and not Rec.Active; } field(AllowProdChanges; Rec.AllowProdChanges) { + Editable = not IsDefault and not Rec.Active; + trigger OnValidate() begin if not Rec.AllowProdChanges then @@ -61,12 +72,33 @@ page 8351 "MCP Config Card" end; } } + group(Control2) + { + Caption = 'Tool Modes'; + ShowCaption = false; + + field(ToolMode; ToolModeLbl) + { + ApplicationArea = All; + Editable = false; + Caption = 'Tool Mode'; + ShowCaption = false; + MultiLine = true; + } + } + part(SystemToolList; "MCP System Tool List") + { + ApplicationArea = All; + Visible = not IsDefault and Rec.EnableDynamicToolMode; + Editable = false; + } part(ToolList; "MCP Config Tool List") { ApplicationArea = All; SubPageLink = ID = field(SystemId); UpdatePropagation = Both; Visible = not IsDefault; + Editable = not Rec.Active; } } } @@ -89,6 +121,17 @@ page 8351 "MCP Config Card" } area(Processing) { + action(Validate) + { + Caption = 'Validate'; + ToolTip = 'Validates the MCP configuration to ensure all settings and tools are correctly configured.'; + Image = ValidateEmailLoggingSetup; + + trigger OnAction() + begin + MCPConfigImplementation.ValidateConfiguration(Rec, false); + end; + } action(MCPEntraApplications) { Caption = 'Entra Applications'; @@ -111,6 +154,7 @@ page 8351 "MCP Config Card" area(Promoted) { actionref(Promoted_Copy; Copy) { } + actionref(Promoted_Validate; Validate) { } actionref(Promoted_MCPEntraApplications; MCPEntraApplications) { } actionref(Promoted_GenerateConnectionString; GenerateConnectionString) { } } @@ -119,6 +163,12 @@ page 8351 "MCP Config Card" trigger OnAfterGetRecord() begin IsDefault := MCPConfigImplementation.IsDefaultConfiguration(Rec); + GetToolModeDescription(); + end; + + trigger OnNewRecord(BelowxRec: Boolean) + begin + ToolModeLbl := StaticToolModeLbl; end; trigger OnDeleteRecord(): Boolean @@ -139,4 +189,12 @@ page 8351 "MCP Config Card" var MCPConfigImplementation: Codeunit "MCP Config Implementation"; IsDefault: Boolean; + ToolModeLbl: Text; + StaticToolModeLbl: Label 'In Static Tool Mode, objects in the available tools will be directly exposed to clients. You can manage these tools by adding, modifying, or removing them from the configuration.'; + DynamicToolModeLbl: Label 'In Dynamic Tool Mode, only system tools will be exposed to clients. Objects within the available tools can be discovered, described and invoked dynamically using system tools. You can enable dynamic discovery of any read-only object outside of the available tools using Discover Additional Objects setting.'; + + local procedure GetToolModeDescription(): Text + begin + ToolModeLbl := Rec.EnableDynamicToolMode ? DynamicToolModeLbl : StaticToolModeLbl; + end; } \ No newline at end of file diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al index d385bce2df..2ced63473d 100644 --- a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al @@ -5,7 +5,6 @@ namespace System.MCP; -using System.Environment; using System.Reflection; page 8352 "MCP Config Tool List" @@ -65,19 +64,19 @@ page 8352 "MCP Config Tool List" field("Allow Read"; Rec."Allow Read") { } field("Allow Create"; Rec."Allow Create") { - Editable = AllowCreateEditable and (IsSandbox or AllowCreateUpdateDeleteTools); + Editable = AllowCreateEditable and AllowCreateUpdateDeleteTools; } field("Allow Modify"; Rec."Allow Modify") { - Editable = AllowModifyEditable and (IsSandbox or AllowCreateUpdateDeleteTools); + Editable = AllowModifyEditable and AllowCreateUpdateDeleteTools; } field("Allow Delete"; Rec."Allow Delete") { - Editable = AllowDeleteEditable and (IsSandbox or AllowCreateUpdateDeleteTools); + Editable = AllowDeleteEditable and AllowCreateUpdateDeleteTools; } field("Allow Bound Actions"; Rec."Allow Bound Actions") { - Editable = IsSandbox or AllowCreateUpdateDeleteTools; + Editable = AllowCreateUpdateDeleteTools; } } } @@ -92,6 +91,7 @@ page 8352 "MCP Config Tool List" Caption = 'Add Tools by API Group'; Image = NewResourceGroup; ToolTip = 'Adds tools to the configuration by API publisher and group.'; + Enabled = not IsConfigActive; trigger OnAction() begin @@ -104,6 +104,7 @@ page 8352 "MCP Config Tool List" Caption = 'Add All Standard APIs as Tools'; Image = ResourceGroup; ToolTip = 'Adds tools for all standard API v2.0 to the configuration.'; + Enabled = not IsConfigActive; trigger OnAction() begin @@ -127,11 +128,9 @@ page 8352 "MCP Config Tool List" end; trigger OnOpenPage() - var - EnvironmentInformation: Codeunit "Environment Information"; begin - IsSandbox := EnvironmentInformation.IsSandbox(); GetAllowCreateUpdateDeleteTools(); + IsConfigActive := MCPConfigImplementation.IsConfigurationActive(Rec.ID); end; trigger OnNewRecord(BelowxRec: Boolean) @@ -142,11 +141,11 @@ page 8352 "MCP Config Tool List" var MCPConfig: Codeunit "MCP Config"; MCPConfigImplementation: Codeunit "MCP Config Implementation"; - IsSandbox: Boolean; AllowCreateEditable: Boolean; AllowModifyEditable: Boolean; AllowDeleteEditable: Boolean; AllowCreateUpdateDeleteTools: Boolean; + IsConfigActive: Boolean; local procedure SetPermissions() var diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigWarningList.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigWarningList.Page.al new file mode 100644 index 0000000000..08683b25a9 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigWarningList.Page.al @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +page 8359 "MCP Config Warning List" +{ + ApplicationArea = All; + PageType = List; + SourceTable = "MCP Config Warning"; + SourceTableTemporary = true; + Caption = 'MCP Configuration Warnings'; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + Editable = false; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + + layout + { + area(Content) + { + repeater(Control1) + { + field("Warning Message"; MCPConfigImplementation.GetWarningMessage(Rec)) + { + Caption = 'Warning Message'; + ToolTip = 'Specifies the warning message.'; + } + field("Recommended Action"; MCPConfigImplementation.GetRecommendedAction(Rec)) + { + Caption = 'Recommended Action'; + ToolTip = 'Specifies the recommended action for the warning.'; + } + } + } + } + + actions + { + area(Processing) + { + action(Fix) + { + ApplicationArea = All; + Caption = 'Apply Recommended Action'; + ToolTip = 'Applies the recommended action for the selected warnings.'; + Image = ApprovalSetup; + + trigger OnAction() + begin + SetSelectionFilter(Rec); + MCPConfigImplementation.ApplyRecommendedActions(Rec); + Rec.Reset(); + if not Rec.IsEmpty() then + Rec.FindSet(); + end; + } + } + area(Promoted) + { + actionref(Promoted_Fix; Fix) { } + } + } + + var + MCPConfigImplementation: Codeunit "MCP Config Implementation"; +} diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPSystemToolList.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPSystemToolList.Page.al new file mode 100644 index 0000000000..55344181c5 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPSystemToolList.Page.al @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +page 8365 "MCP System Tool List" +{ + Caption = 'System Tools'; + ApplicationArea = All; + PageType = ListPart; + SourceTable = "MCP System Tool"; + SourceTableTemporary = true; + Editable = false; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(Control1) + { + ShowCaption = false; + field("Tool Name"; Rec."Tool Name") { } + field("Tool Description"; Rec."Tool Description") { } + } + } + } + + trigger OnOpenPage() + begin + LoadSystemTools(); + end; + + var + MCPConfigImplementation: Codeunit "MCP Config Implementation"; + IsLoaded: Boolean; + + local procedure LoadSystemTools() + begin + if IsLoaded then + exit; + + IsLoaded := true; + MCPConfigImplementation.LoadSystemTools(Rec); + end; +} \ No newline at end of file diff --git a/src/System Application/App/MCP/src/Configuration/Tables/MCPConfigWarning.Table.al b/src/System Application/App/MCP/src/Configuration/Tables/MCPConfigWarning.Table.al new file mode 100644 index 0000000000..218f256873 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Tables/MCPConfigWarning.Table.al @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +table 8352 "MCP Config Warning" +{ + Access = Public; + Extensible = false; + DataClassification = SystemMetadata; + TableType = Temporary; + Caption = 'MCP Configuration Warning'; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + } + field(2; "Config Id"; Guid) + { + Caption = 'Config Id'; + ToolTip = 'Specifies the ID of the MCP configuration.'; + } + field(3; "Tool Id"; Guid) + { + Caption = 'Tool Id'; + ToolTip = 'Specifies the ID of the tool that has a warning.'; + } + field(4; "Warning Type"; Enum "MCP Config Warning Type") + { + Caption = 'Warning Type'; + ToolTip = 'Specifies the type of warning.'; + } + field(5; "Additional Info"; Text[2048]) + { + Caption = 'Additional Info'; + ToolTip = 'Specifies additional information about the warning, such as missing parent page IDs.'; + } + } + + keys + { + key(Key1; "Entry No.") + { + Clustered = true; + } + key(Key2; "Config Id", "Tool Id") + { + } + } +} diff --git a/src/System Application/App/MCP/src/Configuration/Tables/MCPSystemTool.Table.al b/src/System Application/App/MCP/src/Configuration/Tables/MCPSystemTool.Table.al new file mode 100644 index 0000000000..e056179a88 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Tables/MCPSystemTool.Table.al @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.MCP; + +table 8353 "MCP System Tool" +{ + Access = Internal; + DataClassification = SystemMetadata; + TableType = Temporary; + + fields + { + field(1; "Tool Name"; Text[100]) + { + Caption = 'Tool Name'; + ToolTip = 'Specifies the name of the system tool.'; + } + field(2; "Tool Description"; Text[250]) + { + Caption = 'Tool Description'; + ToolTip = 'Specifies the description of the system tool.'; + } + } + + keys + { + key(Key1; "Tool Name") + { + Clustered = true; + } + } + + fieldgroups + { + } +} \ No newline at end of file diff --git a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al index dd7f0b16d0..c7b10b1191 100644 --- a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al +++ b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al @@ -578,6 +578,188 @@ codeunit 130130 "MCP Config Test" Assert.IsFalse(MCPConfigurationTool."Allow Bound Actions", 'Allow Bound Actions is not false'); end; + [Test] + procedure TestFindMissingObjectWarningsForConfiguration() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with non-existing object is created + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool.Rename(MCPConfigurationTool.ID, MCPConfigurationTool."Object Type", -1); // non-existing object + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [THEN] Warning is created for the tool with non-existing object +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Object"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + Assert.RecordCount(MCPConfigWarning, 1); + end; + + [Test] + procedure TestApplyMissingObjectRecommendedAction() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with non-existing object is created + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool.Rename(MCPConfigurationTool.ID, MCPConfigurationTool."Object Type", -1); // non-existing object + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [WHEN] Apply recommended action is called +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Object"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + MCPConfigWarning.FindFirst(); + MCPConfig.ApplyRecommendedAction(MCPConfigWarning); + + // [THEN] Warning is resolved after applying the recommended action +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Object"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + Assert.RecordIsEmpty(MCPConfigWarning); + + // [THEN] Configuration tool is deleted + MCPConfigurationTool.SetRange(SystemId, ToolId); + Assert.RecordIsEmpty(MCPConfigurationTool); + end; + + [Test] + procedure TestFindMissingReadToolWarningsForConfiguration() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with Allow Modify enabled but Allow Read disabled + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool."Allow Read" := false; + MCPConfigurationTool."Allow Modify" := true; + MCPConfigurationTool.Modify(); + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [THEN] Warning is created for the tool with missing read permission +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Read Tool"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + Assert.RecordCount(MCPConfigWarning, 1); + end; + + [Test] + procedure TestApplyMissingReadToolRecommendedAction() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with Allow Modify enabled but Allow Read disabled + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool."Allow Read" := false; + MCPConfigurationTool."Allow Modify" := true; + MCPConfigurationTool.Modify(); + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [WHEN] Apply recommended action is called +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Read Tool"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + MCPConfigWarning.FindFirst(); + MCPConfig.ApplyRecommendedAction(MCPConfigWarning); + + // [THEN] Warning is resolved after applying the recommended action +#pragma warning disable AA0210 + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Read Tool"); +#pragma warning restore AA0210 + MCPConfigWarning.SetRange("Tool Id", ToolId); + Assert.RecordIsEmpty(MCPConfigWarning); + + // [THEN] Configuration tool has Allow Read enabled + MCPConfigurationTool.GetBySystemId(ToolId); + Assert.IsTrue(MCPConfigurationTool."Allow Read", 'Allow Read should be true after applying recommended action'); + end; + + [Test] + procedure TestNoMissingReadToolWarningWhenReadEnabled() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with both Allow Modify and Allow Read enabled + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool."Allow Read" := true; + MCPConfigurationTool."Allow Modify" := true; + MCPConfigurationTool.Modify(); + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [THEN] No Missing Read Tool warning is created + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Read Tool"); + Assert.RecordIsEmpty(MCPConfigWarning); + end; + + [Test] + procedure TestNoMissingReadToolWarningWhenModifyDisabled() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + MCPConfigWarning: Record "MCP Config Warning"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration and tool with Allow Modify disabled + ConfigId := CreateMCPConfig(false, false, true, false); + ToolId := CreateMCPConfigTool(ConfigId); + MCPConfigurationTool.GetBySystemId(ToolId); + MCPConfigurationTool."Allow Read" := false; + MCPConfigurationTool."Allow Modify" := false; + MCPConfigurationTool.Modify(); + Commit(); + + // [WHEN] Find warnings for configuration is called + MCPConfig.FindWarningsForConfiguration(ConfigId, MCPConfigWarning); + + // [THEN] No Missing Read Tool warning is created + MCPConfigWarning.SetRange("Warning Type", MCPConfigWarning."Warning Type"::"Missing Read Tool"); + Assert.RecordIsEmpty(MCPConfigWarning); + end; + local procedure CreateMCPConfig(Active: Boolean; DynamicToolMode: Boolean; AllowCreateUpdateDeleteTools: Boolean; DiscoverReadOnlyObjects: Boolean): Guid var MCPConfiguration: Record "MCP Configuration";