From a71a1b036433dfd733fdde43adb9fcd60ea5a098 Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:02:10 +0100 Subject: [PATCH 1/4] Add Export/Import functionality for MCP Configurations --- .../Codeunits/MCPConfig.Codeunit.al | 22 ++ .../MCPConfigImplementation.Codeunit.al | 214 ++++++++++++++++-- .../Configuration/Pages/MCPConfigCard.Page.al | 2 +- .../Configuration/Pages/MCPConfigList.Page.al | 35 ++- .../Configuration/Pages/MCPCopyConfig.Page.al | 25 ++ .../Test/MCP/src/MCPConfigTest.Codeunit.al | 76 +++++++ 6 files changed, 359 insertions(+), 15 deletions(-) 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 d114f9161c..d6ae1dc9eb 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 @@ -230,4 +230,26 @@ codeunit 8350 "MCP Config" begin MCPConfigImplementation.DeleteEntraApplication(Name); end; + + /// + /// Exports the specified MCP configuration and its tools to a JSON stream. + /// + /// The SystemId (GUID) of the configuration to export. + /// The output stream to write the JSON to. + procedure ExportConfiguration(ConfigId: Guid; var OutStream: OutStream) + begin + MCPConfigImplementation.ExportConfiguration(ConfigId, OutStream); + end; + + /// + /// Imports an MCP configuration and its tools from a JSON stream. + /// + /// The input stream containing the JSON configuration. + /// The name for the imported configuration. + /// The description for the imported configuration. + /// The SystemId (GUID) of the imported configuration. + procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid + begin + exit(MCPConfigImplementation.ImportConfiguration(InStream, NewName, NewDescription)); + end; } \ No newline at end of file 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 99380e840e..78b1054bd5 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 @@ -47,6 +47,12 @@ codeunit 8351 "MCP Config Implementation" VSCodeAppNameLbl: Label 'VS Code', Locked = true; VSCodeAppDescriptionLbl: Label 'Visual Studio Code'; VSCodeClientIdLbl: Label 'aebc6443-996d-45c2-90f0-388ff96faa56', Locked = true; + ExportFileNameTxt: Label 'MCPConfig_%1_%2.json', Locked = true, Comment = '%1 = config name, %2 = date'; + ExportTitleTxt: Label 'Export Configuration'; + ImportTitleTxt: Label 'Import Configuration'; + JsonFilterTxt: Label 'JSON Files (*.json)|*.json'; + InvalidJsonErr: Label 'The selected file is not a valid configuration file.'; + ConfigNameExistsMsg: Label 'A configuration with the name ''%1'' already exists. Please provide a different name.', Comment = '%1 = configuration name'; #region Configurations internal procedure GetConfigurationIdByName(Name: Text[100]): Guid @@ -251,19 +257,6 @@ codeunit 8351 "MCP Config Implementation" MCPConfiguration.Insert(); end; - internal procedure CreateVSCodeEntraApplication() - var - MCPEntraApplication: Record "MCP Entra Application"; - begin - if MCPEntraApplication.Get(VSCodeAppNameLbl) then - exit; - - MCPEntraApplication.Name := VSCodeAppNameLbl; - MCPEntraApplication.Description := VSCodeAppDescriptionLbl; - Evaluate(MCPEntraApplication."Client ID", VSCodeClientIdLbl); - MCPEntraApplication.Insert(); - end; - internal procedure IsDefaultConfiguration(MCPConfiguration: Record "MCP Configuration"): Boolean begin exit(MCPConfiguration.Name = ''); @@ -639,6 +632,19 @@ codeunit 8351 "MCP Config Implementation" #endregion #region Connection String + internal procedure CreateVSCodeEntraApplication() + var + MCPEntraApplication: Record "MCP Entra Application"; + begin + if MCPEntraApplication.Get(VSCodeAppNameLbl) then + exit; + + MCPEntraApplication.Name := VSCodeAppNameLbl; + MCPEntraApplication.Description := VSCodeAppDescriptionLbl; + Evaluate(MCPEntraApplication."Client ID", VSCodeClientIdLbl); + MCPEntraApplication.Insert(); + end; + internal procedure ShowConnectionString(ConfigurationName: Text[100]) var MCPConnectionString: Page "MCP Connection String"; @@ -724,6 +730,188 @@ codeunit 8351 "MCP Config Implementation" end; #endregion + #region Export/Import + internal procedure ExportConfigurationToFile(ConfigId: Guid; ConfigName: Text[100]) + var + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + InStream: InStream; + FileName: Text; + begin + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + ExportConfiguration(ConfigId, OutStream); + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + FileName := StrSubstNo(ExportFileNameTxt, ConfigName, Format(Today(), 0, '--')); + DownloadFromStream(InStream, ExportTitleTxt, '', JsonFilterTxt, FileName); + end; + + internal procedure ImportConfigurationFromFile() + var + MCPConfiguration: Record "MCP Configuration"; + TempBlob: Codeunit "Temp Blob"; + MCPCopyConfig: Page "MCP Copy Config"; + InStream: InStream; + OutStream: OutStream; + FileName: Text; + ConfigName: Text[100]; + ConfigDescription: Text[250]; + begin + if not UploadIntoStream(ImportTitleTxt, '', JsonFilterTxt, FileName, InStream) then + exit; + + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + CopyStream(OutStream, InStream); + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + + if not GetConfigFromJson(InStream, ConfigName, ConfigDescription) then + Error(InvalidJsonErr); + + MCPConfiguration.SetRange(Name, ConfigName); + if not MCPConfiguration.IsEmpty() then begin + MCPCopyConfig.SetConfigName(ConfigName); + MCPCopyConfig.SetConfigDescription(ConfigDescription); + MCPCopyConfig.SetInstructionMessage(StrSubstNo(ConfigNameExistsMsg, ConfigName)); + MCPCopyConfig.LookupMode := true; + if MCPCopyConfig.RunModal() <> Action::LookupOK then + exit; + ConfigName := MCPCopyConfig.GetConfigName(); + ConfigDescription := MCPCopyConfig.GetConfigDescription(); + end; + + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + ImportConfiguration(InStream, ConfigName, ConfigDescription); + end; + + internal procedure ExportConfiguration(ConfigId: Guid; var OutStream: OutStream) + var + MCPConfiguration: Record "MCP Configuration"; + MCPConfigurationTool: Record "MCP Configuration Tool"; + ConfigJson: JsonObject; + ToolsArray: JsonArray; + ToolJson: JsonObject; + OutputText: Text; + begin + if not MCPConfiguration.GetBySystemId(ConfigId) then + exit; + + ConfigJson.Add('name', MCPConfiguration.Name); + ConfigJson.Add('description', MCPConfiguration.Description); + ConfigJson.Add('enableDynamicToolMode', MCPConfiguration.EnableDynamicToolMode); + ConfigJson.Add('discoverReadOnlyObjects', MCPConfiguration.DiscoverReadOnlyObjects); + ConfigJson.Add('allowProdChanges', MCPConfiguration.AllowProdChanges); + + MCPConfigurationTool.SetRange(ID, ConfigId); + if MCPConfigurationTool.FindSet() then + repeat + Clear(ToolJson); + ToolJson.Add('objectType', Format(MCPConfigurationTool."Object Type")); + ToolJson.Add('objectId', MCPConfigurationTool."Object ID"); + ToolJson.Add('allowRead', MCPConfigurationTool."Allow Read"); + ToolJson.Add('allowCreate', MCPConfigurationTool."Allow Create"); + ToolJson.Add('allowModify', MCPConfigurationTool."Allow Modify"); + ToolJson.Add('allowDelete', MCPConfigurationTool."Allow Delete"); + ToolJson.Add('allowBoundActions', MCPConfigurationTool."Allow Bound Actions"); + ToolsArray.Add(ToolJson); + until MCPConfigurationTool.Next() = 0; + + ConfigJson.Add('tools', ToolsArray); + ConfigJson.WriteTo(OutputText); + OutStream.WriteText(OutputText); + end; + + local procedure GetConfigFromJson(var InStream: InStream; var ConfigName: Text[100]; var ConfigDescription: Text[250]): Boolean + var + ConfigJson: JsonObject; + JsonToken: JsonToken; + InputText: Text; + begin + InStream.ReadText(InputText); + if not ConfigJson.ReadFrom(InputText) then + exit(false); + + if ConfigJson.Get('name', JsonToken) then + ConfigName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigName)); + + if ConfigJson.Get('description', JsonToken) then + ConfigDescription := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigDescription)); + + exit(true); + end; + + internal procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid + var + MCPConfiguration: Record "MCP Configuration"; + MCPConfigurationTool: Record "MCP Configuration Tool"; + ConfigJson: JsonObject; + ToolsArray: JsonArray; + ToolToken: JsonToken; + ToolJson: JsonObject; + JsonToken: JsonToken; + InputText: Text; + ObjectTypeText: Text; + i: Integer; + begin + InStream.ReadText(InputText); + if not ConfigJson.ReadFrom(InputText) then + exit; + + MCPConfiguration.Name := NewName; + MCPConfiguration.Description := NewDescription; + MCPConfiguration.Active := false; + + if ConfigJson.Get('enableDynamicToolMode', JsonToken) then + MCPConfiguration.EnableDynamicToolMode := JsonToken.AsValue().AsBoolean(); + + if ConfigJson.Get('discoverReadOnlyObjects', JsonToken) then + MCPConfiguration.DiscoverReadOnlyObjects := JsonToken.AsValue().AsBoolean(); + + if ConfigJson.Get('allowProdChanges', JsonToken) then + MCPConfiguration.AllowProdChanges := JsonToken.AsValue().AsBoolean(); + + MCPConfiguration.Insert(); + LogConfigurationCreated(MCPConfiguration); + + if ConfigJson.Get('tools', JsonToken) then begin + ToolsArray := JsonToken.AsArray(); + for i := 0 to ToolsArray.Count() - 1 do begin + ToolsArray.Get(i, ToolToken); + ToolJson := ToolToken.AsObject(); + + MCPConfigurationTool.Init(); + MCPConfigurationTool.ID := MCPConfiguration.SystemId; + + if ToolJson.Get('objectType', JsonToken) then begin + ObjectTypeText := JsonToken.AsValue().AsText(); + if ObjectTypeText = 'Page' then + MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page; + end; + + if ToolJson.Get('objectId', JsonToken) then + MCPConfigurationTool."Object ID" := JsonToken.AsValue().AsInteger(); + + if ToolJson.Get('allowRead', JsonToken) then + MCPConfigurationTool."Allow Read" := JsonToken.AsValue().AsBoolean(); + + if ToolJson.Get('allowCreate', JsonToken) then + MCPConfigurationTool."Allow Create" := JsonToken.AsValue().AsBoolean(); + + if ToolJson.Get('allowModify', JsonToken) then + MCPConfigurationTool."Allow Modify" := JsonToken.AsValue().AsBoolean(); + + if ToolJson.Get('allowDelete', JsonToken) then + MCPConfigurationTool."Allow Delete" := JsonToken.AsValue().AsBoolean(); + + if ToolJson.Get('allowBoundActions', JsonToken) then + MCPConfigurationTool."Allow Bound Actions" := JsonToken.AsValue().AsBoolean(); + + MCPConfigurationTool.Insert(); + end; + end; + + exit(MCPConfiguration.SystemId); + end; + #endregion + #if not CLEAN28 internal procedure IsFeatureEnabled(): Boolean var 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 1ee87dbac9..50c373a0c9 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 @@ -142,7 +142,7 @@ page 8351 "MCP Config Card" { Caption = 'Connection String'; ToolTip = 'Generate a connection string for this MCP configuration to use in your MCP client.'; - Image = Export; + Image = Link; trigger OnAction() begin diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigList.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigList.Page.al index 5c172350c9..33f6ec5ece 100644 --- a/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigList.Page.al +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPConfigList.Page.al @@ -51,6 +51,7 @@ page 8350 "MCP Config List" ToolTip = 'Creates a copy of the current MCP configuration, including its tools and permissions.'; Image = Copy; AccessByPermission = tabledata "MCP Configuration" = IM; + Scope = Repeater; trigger OnAction() var @@ -71,7 +72,7 @@ page 8350 "MCP Config List" { Caption = 'Connection String'; ToolTip = 'Generate a connection string for this MCP configuration to use in your MCP client.'; - Image = Export; + Image = Link; Scope = Repeater; trigger OnAction() @@ -88,6 +89,35 @@ page 8350 "MCP Config List" Image = Setup; RunObject = page "MCP Entra Application List"; } + action(ExportConfiguration) + { + Caption = 'Export'; + ToolTip = 'Export the selected MCP configuration and its tools to a JSON file.'; + Image = Export; + Scope = Repeater; + + trigger OnAction() + var + MCPConfigImplementation: Codeunit "MCP Config Implementation"; + begin + MCPConfigImplementation.ExportConfigurationToFile(Rec.SystemId, Rec.Name); + end; + } + action(ImportConfiguration) + { + Caption = 'Import'; + ToolTip = 'Import an MCP configuration and its tools from a JSON file.'; + Image = Import; + AccessByPermission = tabledata "MCP Configuration" = IM; + + trigger OnAction() + var + MCPConfigImplementation: Codeunit "MCP Config Implementation"; + begin + MCPConfigImplementation.ImportConfigurationFromFile(); + CurrPage.Update(false); + end; + } } } area(Promoted) @@ -99,6 +129,8 @@ page 8350 "MCP Config List" actionref(Promoted_GenerateConnectionString; GenerateConnectionString) { } actionref(Promoted_MCPEntraApplications; MCPEntraApplications) { } + actionref(Promoted_ExportConfiguration; ExportConfiguration) { } + actionref(Promoted_ImportConfiguration; ImportConfiguration) { } } } } @@ -131,4 +163,5 @@ page 8350 "MCP Config List" FeatureNotEnabledErr: Label 'MCP server feature is not enabled. Please contact your system administrator to enable the feature.'; GoToFeatureManagementLbl: Label 'Go to Feature Management'; #endif + } \ No newline at end of file diff --git a/src/System Application/App/MCP/src/Configuration/Pages/MCPCopyConfig.Page.al b/src/System Application/App/MCP/src/Configuration/Pages/MCPCopyConfig.Page.al index 18e2110f29..66d5bfcc62 100644 --- a/src/System Application/App/MCP/src/Configuration/Pages/MCPCopyConfig.Page.al +++ b/src/System Application/App/MCP/src/Configuration/Pages/MCPCopyConfig.Page.al @@ -19,6 +19,15 @@ page 8355 "MCP Copy Config" group(Control1) { ShowCaption = false; + field(InstructionMessage; InstructionMessage) + { + ApplicationArea = All; + Editable = false; + ShowCaption = false; + MultiLine = true; + Style = Attention; + Visible = InstructionMessage <> ''; + } field(ConfigName; ConfigName) { ApplicationArea = All; @@ -38,6 +47,7 @@ page 8355 "MCP Copy Config" var ConfigName: Text[100]; ConfigDescription: Text[250]; + InstructionMessage: Text; internal procedure GetConfigName(): Text[100] begin @@ -48,4 +58,19 @@ page 8355 "MCP Copy Config" begin exit(ConfigDescription); end; + + internal procedure SetConfigName(NewConfigName: Text[100]) + begin + ConfigName := NewConfigName; + end; + + internal procedure SetConfigDescription(NewConfigDescription: Text[250]) + begin + ConfigDescription := NewConfigDescription; + end; + + internal procedure SetInstructionMessage(NewInstructionMessage: Text) + begin + InstructionMessage := NewInstructionMessage; + end; } \ 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 c7b10b1191..9aa0ed74d5 100644 --- a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al +++ b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al @@ -9,6 +9,7 @@ using System.MCP; using System.Reflection; using System.TestLibraries.MCP; using System.TestLibraries.Utilities; +using System.Utilities; codeunit 130130 "MCP Config Test" { @@ -760,6 +761,81 @@ codeunit 130130 "MCP Config Test" Assert.RecordIsEmpty(MCPConfigWarning); end; + [Test] + procedure TestExportConfiguration() + var + MCPConfiguration: Record "MCP Configuration"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + InStream: InStream; + ConfigId: Guid; + JsonText: Text; + ConfigJson: JsonObject; + JsonToken: JsonToken; + begin + // [GIVEN] Configuration with tools is created + ConfigId := CreateMCPConfig(false, true, true, true); + CreateMCPConfigTool(ConfigId); + MCPConfiguration.GetBySystemId(ConfigId); + + // [WHEN] Export configuration is called + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + MCPConfig.ExportConfiguration(ConfigId, OutStream); + + // [THEN] JSON contains configuration data + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + InStream.ReadText(JsonText); + Assert.IsTrue(ConfigJson.ReadFrom(JsonText), 'Invalid JSON exported'); + + ConfigJson.Get('name', JsonToken); + Assert.AreEqual(MCPConfiguration.Name, JsonToken.AsValue().AsText(), 'Name mismatch'); + + ConfigJson.Get('enableDynamicToolMode', JsonToken); + Assert.AreEqual(true, JsonToken.AsValue().AsBoolean(), 'EnableDynamicToolMode mismatch'); + + ConfigJson.Get('tools', JsonToken); + Assert.AreEqual(1, JsonToken.AsArray().Count(), 'Tools count mismatch'); + end; + + [Test] + procedure TestImportConfiguration() + var + MCPConfiguration: Record "MCP Configuration"; + MCPConfigurationTool: Record "MCP Configuration Tool"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + InStream: InStream; + SourceConfigId: Guid; + ImportedConfigId: Guid; + NewName: Text[100]; + NewDescription: Text[250]; + begin + // [GIVEN] Configuration with tools is created and exported + SourceConfigId := CreateMCPConfig(false, true, true, true); + CreateMCPConfigTool(SourceConfigId); + + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + MCPConfig.ExportConfiguration(SourceConfigId, OutStream); + + // [WHEN] Import configuration is called with new name + NewName := CopyStr(Format(CreateGuid()), 1, 100); + NewDescription := 'Imported configuration'; + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + ImportedConfigId := MCPConfig.ImportConfiguration(InStream, NewName, NewDescription); + + // [THEN] New configuration is created with imported settings + MCPConfiguration.GetBySystemId(ImportedConfigId); + Assert.AreEqual(NewName, MCPConfiguration.Name, 'Name mismatch'); + Assert.AreEqual(NewDescription, MCPConfiguration.Description, 'Description mismatch'); + Assert.IsFalse(MCPConfiguration.Active, 'Imported config should be inactive'); + Assert.IsTrue(MCPConfiguration.EnableDynamicToolMode, 'EnableDynamicToolMode mismatch'); + Assert.IsTrue(MCPConfiguration.DiscoverReadOnlyObjects, 'DiscoverReadOnlyObjects mismatch'); + + // [THEN] Tools are imported + MCPConfigurationTool.SetRange(ID, ImportedConfigId); + Assert.RecordCount(MCPConfigurationTool, 1); + end; + local procedure CreateMCPConfig(Active: Boolean; DynamicToolMode: Boolean; AllowCreateUpdateDeleteTools: Boolean; DiscoverReadOnlyObjects: Boolean): Guid var MCPConfiguration: Record "MCP Configuration"; From 1e53331e91fdb553a2997d0424c8cc1a31e148eb Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:36:37 +0100 Subject: [PATCH 2/4] Update --- .../Codeunits/MCPConfigImplementation.Codeunit.al | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 78b1054bd5..68f46936ce 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 @@ -829,8 +829,10 @@ codeunit 8351 "MCP Config Implementation" if not ConfigJson.ReadFrom(InputText) then exit(false); - if ConfigJson.Get('name', JsonToken) then - ConfigName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigName)); + if not ConfigJson.Get('name', JsonToken) then + exit(false); + + ConfigName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigName)); if ConfigJson.Get('description', JsonToken) then ConfigDescription := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigDescription)); From bb7008629759f4dcecb129ebe4259adfc1bd0515 Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:03:48 +0100 Subject: [PATCH 3/4] Update --- .../MCPConfigImplementation.Codeunit.al | 77 +++++++++---------- .../Test/MCP/src/MCPConfigTest.Codeunit.al | 10 ++- 2 files changed, 44 insertions(+), 43 deletions(-) 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 68f46936ce..ba878f94f8 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 @@ -843,15 +843,10 @@ codeunit 8351 "MCP Config Implementation" internal procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid var MCPConfiguration: Record "MCP Configuration"; - MCPConfigurationTool: Record "MCP Configuration Tool"; ConfigJson: JsonObject; ToolsArray: JsonArray; ToolToken: JsonToken; - ToolJson: JsonObject; - JsonToken: JsonToken; InputText: Text; - ObjectTypeText: Text; - i: Integer; begin InStream.ReadText(InputText); if not ConfigJson.ReadFrom(InputText) then @@ -861,56 +856,60 @@ codeunit 8351 "MCP Config Implementation" MCPConfiguration.Description := NewDescription; MCPConfiguration.Active := false; - if ConfigJson.Get('enableDynamicToolMode', JsonToken) then - MCPConfiguration.EnableDynamicToolMode := JsonToken.AsValue().AsBoolean(); + if ConfigJson.Contains('enableDynamicToolMode') then + MCPConfiguration.EnableDynamicToolMode := ConfigJson.GetBoolean('enableDynamicToolMode'); - if ConfigJson.Get('discoverReadOnlyObjects', JsonToken) then - MCPConfiguration.DiscoverReadOnlyObjects := JsonToken.AsValue().AsBoolean(); + if ConfigJson.Contains('discoverReadOnlyObjects') then + MCPConfiguration.DiscoverReadOnlyObjects := ConfigJson.GetBoolean('discoverReadOnlyObjects'); - if ConfigJson.Get('allowProdChanges', JsonToken) then - MCPConfiguration.AllowProdChanges := JsonToken.AsValue().AsBoolean(); + if ConfigJson.Contains('allowProdChanges') then + MCPConfiguration.AllowProdChanges := ConfigJson.GetBoolean('allowProdChanges'); MCPConfiguration.Insert(); LogConfigurationCreated(MCPConfiguration); - if ConfigJson.Get('tools', JsonToken) then begin - ToolsArray := JsonToken.AsArray(); - for i := 0 to ToolsArray.Count() - 1 do begin - ToolsArray.Get(i, ToolToken); - ToolJson := ToolToken.AsObject(); + if ConfigJson.Contains('tools') then begin + ToolsArray := ConfigJson.GetArray('tools'); + foreach ToolToken in ToolsArray do + ImportTool(MCPConfiguration.SystemId, ToolToken.AsObject()); + end; - MCPConfigurationTool.Init(); - MCPConfigurationTool.ID := MCPConfiguration.SystemId; + exit(MCPConfiguration.SystemId); + end; - if ToolJson.Get('objectType', JsonToken) then begin - ObjectTypeText := JsonToken.AsValue().AsText(); - if ObjectTypeText = 'Page' then - MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page; - end; + local procedure ImportTool(ConfigId: Guid; ToolJson: JsonObject) + var + MCPConfigurationTool: Record "MCP Configuration Tool"; + ObjectTypeText: Text; + begin + MCPConfigurationTool.Init(); + MCPConfigurationTool.ID := ConfigId; - if ToolJson.Get('objectId', JsonToken) then - MCPConfigurationTool."Object ID" := JsonToken.AsValue().AsInteger(); + if ToolJson.Contains('objectType') then begin + ObjectTypeText := ToolJson.GetText('objectType'); + if ObjectTypeText = 'Page' then + MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page; + end; - if ToolJson.Get('allowRead', JsonToken) then - MCPConfigurationTool."Allow Read" := JsonToken.AsValue().AsBoolean(); + if ToolJson.Contains('objectId') then + MCPConfigurationTool."Object ID" := ToolJson.GetInteger('objectId'); - if ToolJson.Get('allowCreate', JsonToken) then - MCPConfigurationTool."Allow Create" := JsonToken.AsValue().AsBoolean(); + if ToolJson.Contains('allowRead') then + MCPConfigurationTool."Allow Read" := ToolJson.GetBoolean('allowRead'); - if ToolJson.Get('allowModify', JsonToken) then - MCPConfigurationTool."Allow Modify" := JsonToken.AsValue().AsBoolean(); + if ToolJson.Contains('allowCreate') then + MCPConfigurationTool."Allow Create" := ToolJson.GetBoolean('allowCreate'); - if ToolJson.Get('allowDelete', JsonToken) then - MCPConfigurationTool."Allow Delete" := JsonToken.AsValue().AsBoolean(); + if ToolJson.Contains('allowModify') then + MCPConfigurationTool."Allow Modify" := ToolJson.GetBoolean('allowModify'); - if ToolJson.Get('allowBoundActions', JsonToken) then - MCPConfigurationTool."Allow Bound Actions" := JsonToken.AsValue().AsBoolean(); + if ToolJson.Contains('allowDelete') then + MCPConfigurationTool."Allow Delete" := ToolJson.GetBoolean('allowDelete'); - MCPConfigurationTool.Insert(); - end; - end; + if ToolJson.Contains('allowBoundActions') then + MCPConfigurationTool."Allow Bound Actions" := ToolJson.GetBoolean('allowBoundActions'); - exit(MCPConfiguration.SystemId); + MCPConfigurationTool.Insert(); end; #endregion diff --git a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al index 9aa0ed74d5..68b8f33a0a 100644 --- a/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al +++ b/src/System Application/Test/MCP/src/MCPConfigTest.Codeunit.al @@ -773,9 +773,10 @@ codeunit 130130 "MCP Config Test" ConfigJson: JsonObject; JsonToken: JsonToken; begin - // [GIVEN] Configuration with tools is created + // [GIVEN] Configuration with two tools is created ConfigId := CreateMCPConfig(false, true, true, true); CreateMCPConfigTool(ConfigId); + CreateMCPConfigTool(ConfigId); MCPConfiguration.GetBySystemId(ConfigId); // [WHEN] Export configuration is called @@ -794,7 +795,7 @@ codeunit 130130 "MCP Config Test" Assert.AreEqual(true, JsonToken.AsValue().AsBoolean(), 'EnableDynamicToolMode mismatch'); ConfigJson.Get('tools', JsonToken); - Assert.AreEqual(1, JsonToken.AsArray().Count(), 'Tools count mismatch'); + Assert.AreEqual(2, JsonToken.AsArray().Count(), 'Tools count mismatch'); end; [Test] @@ -810,9 +811,10 @@ codeunit 130130 "MCP Config Test" NewName: Text[100]; NewDescription: Text[250]; begin - // [GIVEN] Configuration with tools is created and exported + // [GIVEN] Configuration with two tools is created and exported SourceConfigId := CreateMCPConfig(false, true, true, true); CreateMCPConfigTool(SourceConfigId); + CreateMCPConfigTool(SourceConfigId); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); MCPConfig.ExportConfiguration(SourceConfigId, OutStream); @@ -833,7 +835,7 @@ codeunit 130130 "MCP Config Test" // [THEN] Tools are imported MCPConfigurationTool.SetRange(ID, ImportedConfigId); - Assert.RecordCount(MCPConfigurationTool, 1); + Assert.RecordCount(MCPConfigurationTool, 2); end; local procedure CreateMCPConfig(Active: Boolean; DynamicToolMode: Boolean; AllowCreateUpdateDeleteTools: Boolean; DiscoverReadOnlyObjects: Boolean): Guid From 0a0f341d3e3bae3ad79e5b19cff5da88ce805e53 Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:02:54 +0100 Subject: [PATCH 4/4] Update --- .../MCP/src/Configuration/Pages/MCPConfigToolList.Page.al | 4 ++++ src/System Application/Test/MCP/app.json | 6 ++++++ 2 files changed, 10 insertions(+) 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 eb8958bd5f..03f61c3736 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 @@ -41,6 +41,8 @@ page 8352 "MCP Config Tool List" exit; repeat + if MCPConfigImplementation.CheckAPIToolExists(Rec.ID, PageMetadata.ID) then + continue; MCPConfig.CreateAPITool(Rec.ID, PageMetadata.ID); until PageMetadata.Next() = 0; @@ -104,6 +106,8 @@ page 8352 "MCP Config Tool List" exit; repeat + if MCPConfigImplementation.CheckAPIToolExists(Rec.ID, PageMetadata.ID) then + continue; MCPConfig.CreateAPITool(Rec.ID, PageMetadata.ID); until PageMetadata.Next() = 0; diff --git a/src/System Application/Test/MCP/app.json b/src/System Application/Test/MCP/app.json index 36e090514a..66ff28ebb5 100644 --- a/src/System Application/Test/MCP/app.json +++ b/src/System Application/Test/MCP/app.json @@ -40,6 +40,12 @@ "name": "Environment Information Test Library", "publisher": "Microsoft", "version": "28.0.0.0" + }, + { + "id": "e31ad830-3d46-472e-afeb-1d3d35247943", + "name": "BLOB Storage", + "publisher": "Microsoft", + "version": "28.0.0.0" } ], "screenshots": [],