From b4a79fcd2f0b663e342c861c4f1ed9da0956bd5f Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:09:44 +0100 Subject: [PATCH 1/2] Add API Version Support to MCP Configuration Tools --- .../Permissions/MCPObjects.PermissionSet.al | 1 + .../MCPConfigImplementation.Codeunit.al | 109 +++++++++++++++++- .../MCPConfigMissingParent.Codeunit.al | 2 +- .../Codeunits/MCPUpgrade.Codeunit.al | 58 ++++++++++ .../Pages/MCPAPIVersionLookup.Page.al | 36 ++++++ .../Pages/MCPConfigToolList.Page.al | 29 ++++- .../Tables/MCPAPIPublisherGroup.Table.al | 2 - .../Tables/MCPAPIVersion.Table.al | 33 ++++++ .../MCP/src/MCPConfigTestLibrary.Codeunit.al | 5 + .../MCP/src/MockAPIMultiVersion.Page.al | 33 ++++++ .../Test/MCP/src/MCPConfigTest.Codeunit.al | 50 ++++++++ 11 files changed, 350 insertions(+), 8 deletions(-) create mode 100644 src/System Application/App/MCP/src/Configuration/Codeunits/MCPUpgrade.Codeunit.al create mode 100644 src/System Application/App/MCP/src/Configuration/Pages/MCPAPIVersionLookup.Page.al create mode 100644 src/System Application/App/MCP/src/Configuration/Tables/MCPAPIVersion.Table.al create mode 100644 src/System Application/Test Library/MCP/src/MockAPIMultiVersion.Page.al diff --git a/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al b/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al index d3f26d1af5..fbbf1b728c 100644 --- a/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al +++ b/src/System Application/App/MCP/Permissions/MCPObjects.PermissionSet.al @@ -12,6 +12,7 @@ permissionset 8350 "MCP - Objects" Caption = 'MCP - Objects'; Permissions = table "MCP API Publisher Group" = X, + table "MCP API Version" = X, table "MCP Configuration" = X, table "MCP Configuration Tool" = X, table "MCP Config Warning" = X, 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 dfdfb8fcff..7aa3a4b029 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 @@ -31,6 +31,7 @@ codeunit 8351 "MCP Config Implementation" InvalidAPIVersionErr: Label 'Only API v2.0 pages are supported.'; DefaultMCPConfigurationDescriptionLbl: Label 'Default MCP configuration'; DynamicToolModeRequiredErr: Label 'Dynamic tool mode needs to be enabled to discover read-only objects.'; + VersionNotValidErr: Label 'The API version is not valid for the selected tool.'; MCPConfigurationCreatedLbl: Label 'MCP Configuration created', Locked = true; MCPConfigurationModifiedLbl: Label 'MCP Configuration modified', Locked = true; MCPConfigurationDeletedLbl: Label 'MCP Configuration deleted', Locked = true; @@ -45,7 +46,7 @@ codeunit 8351 "MCP Config Implementation" MCPPrefixProdLbl: Label 'businesscentral', Locked = true; MCPPrefixTIELbl: Label 'businesscentral-tie', Locked = true; VSCodeAppNameLbl: Label 'VS Code', Locked = true; - VSCodeAppDescriptionLbl: Label 'Visual Studio Code'; + VSCodeAppDescriptionLbl: Label 'Visual Studio Code', Locked = true; VSCodeClientIdLbl: Label 'aebc6443-996d-45c2-90f0-388ff96faa56', Locked = true; #region Configurations @@ -359,6 +360,7 @@ codeunit 8351 "MCP Config Implementation" var MCPConfiguration: Record "MCP Configuration"; MCPConfigurationTool: Record "MCP Configuration Tool"; + PageMetadata: Record "Page Metadata"; begin if not MCPConfiguration.GetBySystemId(ConfigId) then exit; @@ -366,12 +368,13 @@ codeunit 8351 "MCP Config Implementation" if IsDefaultConfiguration(MCPConfiguration) then Error(ToolsCannotBeAddedToDefaultConfigErr); - ValidateAPITool(APIPageId, ValidateAPIPublisher); + PageMetadata := ValidateAPITool(APIPageId, ValidateAPIPublisher); MCPConfigurationTool.ID := ConfigId; MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page; MCPConfigurationTool."Object ID" := APIPageId; MCPConfigurationTool."Allow Read" := true; + MCPConfigurationTool."API Version" := GetHighestAPIVersion(PageMetadata); MCPConfigurationTool.Insert(); exit(MCPConfigurationTool.SystemId); end; @@ -518,7 +521,7 @@ codeunit 8351 "MCP Config Implementation" APIGroup := MCPAPIPublisherGroup."API Group"; end; - internal procedure ValidateAPITool(PageId: Integer; ValidateAPIPublisher: Boolean) + internal procedure ValidateAPITool(PageId: Integer; ValidateAPIPublisher: Boolean): Record "Page Metadata" var PageMetadata: Record "Page Metadata"; begin @@ -529,13 +532,15 @@ codeunit 8351 "MCP Config Implementation" Error(InvalidPageTypeErr); if not ValidateAPIPublisher then - exit; + exit(PageMetadata); if PageMetadata.APIPublisher = 'microsoft' then Error(InvalidAPIVersionErr); if PageMetadata."AL Namespace" = 'Microsoft.API.V1' then Error(InvalidAPIVersionErr); + + exit(PageMetadata); end; internal procedure AddToolsByAPIGroup(ConfigId: Guid) @@ -636,6 +641,102 @@ codeunit 8351 "MCP Config Implementation" MCPSystemTool."Tool Description" := ToolDescription; MCPSystemTool.Insert(); end; + + internal procedure ValidateAPIVersion(ObjectId: Integer; APIVersion: Text) + var + PageMetadata: Record "Page Metadata"; + Versions: List of [Text]; + begin + if not PageMetadata.Get(ObjectId) then + exit; + + Versions := PageMetadata.APIVersion.Split(','); + if not Versions.Contains(APIVersion) then + Error(VersionNotValidErr); + end; + + internal procedure LookupAPIVersions(PageId: Integer; var APIVersion: Text[30]) + var + PageMetadata: Record "Page Metadata"; + MCPAPIVersion: Record "MCP API Version"; + Versions: List of [Text]; + Version: Text[30]; + begin + if not PageMetadata.Get(PageId) then + exit; + + Versions := PageMetadata.APIVersion.Split(','); + foreach Version in Versions do begin + MCPAPIVersion."API Version" := Version; + MCPAPIVersion.Insert(); + end; + + if Page.RunModal(Page::"MCP API Version Lookup", MCPAPIVersion) = Action::LookupOK then + APIVersion := MCPAPIVersion."API Version"; + end; + + internal procedure GetHighestAPIVersion(PageMetadata: Record "Page Metadata"): Text[30] + var + Versions: List of [Text]; + Version: Text; + HighestVersion: Text; + HighestMajor: Integer; + HighestMinor: Integer; + CurrentMajor: Integer; + CurrentMinor: Integer; + begin + if PageMetadata.APIVersion = '' then + exit(''); + + Versions := PageMetadata.APIVersion.Split(','); + + if Versions.Count() = 1 then + exit(CopyStr(Versions.Get(1), 1, 30)); + + HighestMajor := -1; + HighestMinor := -1; + + foreach Version in Versions do + if TryParseVersion(Version, CurrentMajor, CurrentMinor) then + if (CurrentMajor > HighestMajor) or ((CurrentMajor = HighestMajor) and (CurrentMinor > HighestMinor)) then begin + HighestMajor := CurrentMajor; + HighestMinor := CurrentMinor; + HighestVersion := Version; + end; + + exit(CopyStr(HighestVersion, 1, 30)); + end; + + local procedure TryParseVersion(Version: Text; var Major: Integer; var Minor: Integer): Boolean + var + VersionParts: List of [Text]; + VersionNumber: Text; + begin + // 'beta' is treated as lowest priority + if Version.ToLower() = 'beta' then begin + Major := -1; + Minor := -1; + exit(true); + end; + + // Expected format: vMajor.Minor (e.g., v1.0, v2.0) + if not Version.StartsWith('v') then + exit(false); + + VersionNumber := Version.Substring(2); // Remove 'v' + VersionParts := VersionNumber.Split('.'); + + if VersionParts.Count() <> 2 then + exit(false); + + if not Evaluate(Major, VersionParts.Get(1)) then + exit(false); + + if not Evaluate(Minor, VersionParts.Get(2)) then + exit(false); + + exit(true); + end; #endregion #region Connection String 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 index e0856d1cb4..f85bd92244 100644 --- a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingParent.Codeunit.al +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigMissingParent.Codeunit.al @@ -39,7 +39,7 @@ codeunit 8354 "MCP Config Missing Parent" implements "MCP Config Warning" repeat if PageMetadata.Get(MCPConfigurationTool."Object ID") then if PageMetadata.PageType = PageMetadata.PageType::API then - PageIdVersions.Add(MCPConfigurationTool."Object ID", PageMetadata.APIVersion); + PageIdVersions.Add(MCPConfigurationTool."Object ID", MCPConfigurationTool."API Version"); until MCPConfigurationTool.Next() = 0; // Get parent mappings from platform diff --git a/src/System Application/App/MCP/src/Configuration/Codeunits/MCPUpgrade.Codeunit.al b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPUpgrade.Codeunit.al new file mode 100644 index 0000000000..262339d0b4 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Codeunits/MCPUpgrade.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; +using System.Upgrade; + +codeunit 8356 "MCP Upgrade" +{ + Subtype = Upgrade; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnUpgradePerDatabase() + begin + UpgradeMCPAPIToolVersion(); + end; + + internal procedure UpgradeMCPAPIToolVersion() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + PageMetadata: Record "Page Metadata"; + MCPConfigImplementation: Codeunit "MCP Config Implementation"; + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if UpgradeTag.HasDatabaseUpgradeTag(GetMCPAPIToolVersionUpgradeTag()) then + exit; + + MCPConfigurationTool.SetRange("API Version", ''); + if MCPConfigurationTool.FindSet() then + repeat + if not PageMetadata.Get(MCPConfigurationTool."Object ID") then + continue; + + if PageMetadata.PageType <> PageMetadata.PageType::API then + continue; + + MCPConfigurationTool."API Version" := MCPConfigImplementation.GetHighestAPIVersion(PageMetadata); + MCPConfigurationTool.Modify(); + until MCPConfigurationTool.Next() = 0; + + UpgradeTag.SetUpgradeTag(GetMCPAPIToolVersionUpgradeTag()); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", OnGetPerDatabaseUpgradeTags, '', false, false)] + local procedure RegisterUpgradeTags(var PerDatabaseUpgradeTags: List of [Code[250]]) + begin + PerDatabaseUpgradeTags.Add(GetMCPAPIToolVersionUpgradeTag()); + end; + + local procedure GetMCPAPIToolVersionUpgradeTag(): Text[250] + begin + exit('MS-619475-MCPAPIToolVersion-20260126'); + end; +} \ No newline at end of file diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPAPIVersionLookup.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPAPIVersionLookup.Page.al new file mode 100644 index 0000000000..0f469dbeb4 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPAPIVersionLookup.Page.al @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------------------------------ +// 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 8366 "MCP API Version Lookup" +{ + PageType = List; + ApplicationArea = All; + SourceTable = "MCP API Version"; + Caption = 'API Versions'; + Extensible = false; + Editable = false; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(Control1) + { + field("API Version"; Rec."API Version") + { + Caption = 'API Version'; + ToolTip = 'Specifies the API version.'; + } + } + } + } +} \ 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 2ced63473d..c4f2d3d50c 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 @@ -50,8 +50,11 @@ page 8352 "MCP Config Tool List" end; trigger OnValidate() + var + PageMetadata: Record "Page Metadata"; begin - MCPConfigImplementation.ValidateAPITool(Rec."Object Id", true); + PageMetadata := MCPConfigImplementation.ValidateAPITool(Rec."Object Id", true); + Rec."API Version" := MCPConfigImplementation.GetHighestAPIVersion(PageMetadata); SetPermissions(); end; } @@ -61,6 +64,30 @@ page 8352 "MCP Config Tool List" Editable = false; ToolTip = 'Specifies the name of the object.'; } + field("API Version"; Rec."API Version") + { + Caption = 'API Version'; + ToolTip = 'Specifies the API version of the tool.'; + + trigger OnLookup(var Text: Text): Boolean + var + APIVersion: Text[30]; + begin + if Rec."Object ID" = 0 then + exit; + + MCPConfigImplementation.LookupAPIVersions(Rec."Object Id", APIVersion); + if APIVersion <> '' then + Rec."API Version" := APIVersion; + end; + + trigger OnValidate() + begin + MCPConfigImplementation.ValidateAPIVersion(Rec."Object Id", Rec."API Version"); + end; + + // TODONAT: Upgrade + } field("Allow Read"; Rec."Allow Read") { } field("Allow Create"; Rec."Allow Create") { diff --git a/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIPublisherGroup.Table.al b/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIPublisherGroup.Table.al index 1f3ed99104..ae3c238f11 100644 --- a/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIPublisherGroup.Table.al +++ b/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIPublisherGroup.Table.al @@ -15,12 +15,10 @@ table 8350 "MCP API Publisher Group" { field(1; "API Publisher"; Text[40]) { - DataClassification = ToBeClassified; Caption = 'API Publisher'; } field(2; "API Group"; Text[40]) { - DataClassification = ToBeClassified; Caption = 'API Group'; } } diff --git a/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIVersion.Table.al b/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIVersion.Table.al new file mode 100644 index 0000000000..0314e81371 --- /dev/null +++ b/src/System Application/App/MCP/src/Configuration/Tables/MCPAPIVersion.Table.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// 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 8354 "MCP API Version" +{ + Access = Internal; + DataClassification = SystemMetadata; + TableType = Temporary; + + fields + { + field(1; "API Version"; Text[30]) + { + Caption = 'API Version'; + } + } + + keys + { + key(Key1; "API Version") + { + Clustered = true; + } + } + + fieldgroups + { + } +} \ No newline at end of file diff --git a/src/System Application/Test Library/MCP/src/MCPConfigTestLibrary.Codeunit.al b/src/System Application/Test Library/MCP/src/MCPConfigTestLibrary.Codeunit.al index 4a84d53f4e..156696dbc4 100644 --- a/src/System Application/Test Library/MCP/src/MCPConfigTestLibrary.Codeunit.al +++ b/src/System Application/Test Library/MCP/src/MCPConfigTestLibrary.Codeunit.al @@ -43,4 +43,9 @@ codeunit 130131 "MCP Config Test Library" MCPConfigImplementation.GetAPIPublishers(MCPAPIPublisherGroup); MCPConfigImplementation.LookupAPIGroup(MCPAPIPublisherGroup, APIPublisher, APIGroup); end; + + procedure GetHighestAPIVersion(PageMetadata: Record "Page Metadata"): Text[30] + begin + exit(MCPConfigImplementation.GetHighestAPIVersion(PageMetadata)); + end; } \ No newline at end of file diff --git a/src/System Application/Test Library/MCP/src/MockAPIMultiVersion.Page.al b/src/System Application/Test Library/MCP/src/MockAPIMultiVersion.Page.al new file mode 100644 index 0000000000..e140b532dc --- /dev/null +++ b/src/System Application/Test Library/MCP/src/MockAPIMultiVersion.Page.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.TestLibraries.MCP; + +page 130134 "Mock API Multi Version" +{ + PageType = API; + Caption = 'Mock API Multi Version'; + APIPublisher = 'mock'; + APIGroup = 'mcp'; + APIVersion = 'v1.0', 'v2.0', 'beta'; + EntityName = 'mockMultiVersion'; + EntitySetName = 'mockMultiVersions'; + SourceTable = "Mock API"; + DelayedInsert = true; + + layout + { + area(Content) + { + repeater(Group) + { + field(id; Rec.SystemId) + { + Caption = 'ID'; + } + } + } + } +} diff --git a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al index c7b10b1191..a87dc5c41f 100644 --- a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al +++ b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al @@ -246,6 +246,56 @@ codeunit 130130 "MCP Config Test" Assert.ExpectedError('Only API pages are supported.'); end; + [Test] + procedure TestCreateAPIToolSetsAPIVersion() + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + ConfigId: Guid; + ToolId: Guid; + begin + // [GIVEN] Configuration is created + ConfigId := CreateMCPConfig(false, true, true, false); + + // [WHEN] Create API tool is called with a multi-version API page + ToolId := MCPConfig.CreateAPITool(ConfigId, Page::"Mock API Multi Version"); + + // [THEN] API tool is created with the highest API version + MCPConfigurationTool.GetBySystemId(ToolId); + Assert.AreEqual('v2.0', MCPConfigurationTool."API Version", 'API Version should be the highest version'); + end; + + [Test] + procedure TestGetHighestAPIVersionSingleVersion() + var + PageMetadata: Record "Page Metadata"; + HighestVersion: Text[30]; + begin + // [GIVEN] A page metadata with single API version + PageMetadata.Get(Page::"Mock API"); + + // [WHEN] GetHighestAPIVersion is called + HighestVersion := MCPConfigTestLibrary.GetHighestAPIVersion(PageMetadata); + + // [THEN] The single version is returned + Assert.AreEqual('v0.1', HighestVersion, 'Should return the single version'); + end; + + [Test] + procedure TestGetHighestAPIVersionMultipleVersions() + var + PageMetadata: Record "Page Metadata"; + HighestVersion: Text[30]; + begin + // [GIVEN] A page metadata with multiple API versions (v1.0,v2.0,beta) + PageMetadata.Get(Page::"Mock API Multi Version"); + + // [WHEN] GetHighestAPIVersion is called + HighestVersion := MCPConfigTestLibrary.GetHighestAPIVersion(PageMetadata); + + // [THEN] The highest version is returned + Assert.AreEqual('v2.0', HighestVersion, 'Should return v2.0 as highest version'); + end; + [Test] procedure TestAllowRead() var From b68e20ff70578ca93b59a4865a8ad43348ac369d Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:00:36 +0100 Subject: [PATCH 2/2] Apply suggestion from @onbuyuka --- .../App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al | 2 -- 1 file changed, 2 deletions(-) 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 c4f2d3d50c..9c1b542ead 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 @@ -85,8 +85,6 @@ page 8352 "MCP Config Tool List" begin MCPConfigImplementation.ValidateAPIVersion(Rec."Object Id", Rec."API Version"); end; - - // TODONAT: Upgrade } field("Allow Read"; Rec."Allow Read") { } field("Allow Create"; Rec."Allow Create")