Skip to content

Commit 496016a

Browse files
authored
[MCP] Add Export/Import functionality for MCP Configurations (#6374)
### Summary Adds the ability to export and import MCP server configurations as JSON files, enabling users to easily share configurations across different Business Central environments. ### Changes #### New Features - **Export Configuration**: Export an MCP configuration (including all tools and permissions) to a JSON file - **Import Configuration**: Import an MCP configuration from a JSON file, with automatic name conflict detection #### UI Changes - Added **Export** and **Import** actions to the "Advanced" action group on the MCP Config List page - Both actions are promoted for easy access - Changed the "Connection String" action icon from `Export` to `Link` to avoid visual confusion with the new Export action - Enhanced the Copy Config dialog to display an instruction message when a name conflict is detected during import #### Implementation Details - Export creates a JSON file named `MCPConfig_<name>_<date>.json` containing: - Configuration settings (name, description, enableDynamicToolMode, discoverReadOnlyObjects, allowProdChanges) - All associated tools with their permissions - Import always creates configurations as **inactive** (validation happens on activation) - When importing a config with a name that already exists, displays a dialog prompting for a new name - Added public API methods `ExportConfiguration` and `ImportConfiguration` to the `MCP Config` codeunit for extensibility #### Files Modified - MCPConfig.Codeunit.al - Added public API for export/import - MCPConfigImplementation.Codeunit.al - Added export/import implementation logic - MCPConfigList.Page.al - Added Export/Import actions - MCPConfigCard.Page.al - Changed Connection String icon - MCPCopyConfig.Page.al - Added instruction message field and setter methods - MCPConfigTest.Codeunit.al - Added unit tests for export/import #### Tests - `TestExportConfiguration` - Verifies JSON export contains correct configuration data and tools - `TestImportConfiguration` - Verifies import creates config with correct settings and tools Fixes [AB#619975](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/619975)
1 parent eb2a413 commit 496016a

8 files changed

Lines changed: 372 additions & 15 deletions

File tree

src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfig.Codeunit.al

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,26 @@ codeunit 8350 "MCP Config"
230230
begin
231231
MCPConfigImplementation.DeleteEntraApplication(Name);
232232
end;
233+
234+
/// <summary>
235+
/// Exports the specified MCP configuration and its tools to a JSON stream.
236+
/// </summary>
237+
/// <param name="ConfigId">The SystemId (GUID) of the configuration to export.</param>
238+
/// <param name="OutStream">The output stream to write the JSON to.</param>
239+
procedure ExportConfiguration(ConfigId: Guid; var OutStream: OutStream)
240+
begin
241+
MCPConfigImplementation.ExportConfiguration(ConfigId, OutStream);
242+
end;
243+
244+
/// <summary>
245+
/// Imports an MCP configuration and its tools from a JSON stream.
246+
/// </summary>
247+
/// <param name="InStream">The input stream containing the JSON configuration.</param>
248+
/// <param name="NewName">The name for the imported configuration.</param>
249+
/// <param name="NewDescription">The description for the imported configuration.</param>
250+
/// <returns>The SystemId (GUID) of the imported configuration.</returns>
251+
procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid
252+
begin
253+
exit(MCPConfigImplementation.ImportConfiguration(InStream, NewName, NewDescription));
254+
end;
233255
}

src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al

Lines changed: 202 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ codeunit 8351 "MCP Config Implementation"
4747
VSCodeAppNameLbl: Label 'VS Code', Locked = true;
4848
VSCodeAppDescriptionLbl: Label 'Visual Studio Code';
4949
VSCodeClientIdLbl: Label 'aebc6443-996d-45c2-90f0-388ff96faa56', Locked = true;
50+
ExportFileNameTxt: Label 'MCPConfig_%1_%2.json', Locked = true, Comment = '%1 = config name, %2 = date';
51+
ExportTitleTxt: Label 'Export Configuration';
52+
ImportTitleTxt: Label 'Import Configuration';
53+
JsonFilterTxt: Label 'JSON Files (*.json)|*.json';
54+
InvalidJsonErr: Label 'The selected file is not a valid configuration file.';
55+
ConfigNameExistsMsg: Label 'A configuration with the name ''%1'' already exists. Please provide a different name.', Comment = '%1 = configuration name';
5056

5157
#region Configurations
5258
internal procedure GetConfigurationIdByName(Name: Text[100]): Guid
@@ -251,19 +257,6 @@ codeunit 8351 "MCP Config Implementation"
251257
MCPConfiguration.Insert();
252258
end;
253259

254-
internal procedure CreateVSCodeEntraApplication()
255-
var
256-
MCPEntraApplication: Record "MCP Entra Application";
257-
begin
258-
if MCPEntraApplication.Get(VSCodeAppNameLbl) then
259-
exit;
260-
261-
MCPEntraApplication.Name := VSCodeAppNameLbl;
262-
MCPEntraApplication.Description := VSCodeAppDescriptionLbl;
263-
Evaluate(MCPEntraApplication."Client ID", VSCodeClientIdLbl);
264-
MCPEntraApplication.Insert();
265-
end;
266-
267260
internal procedure IsDefaultConfiguration(MCPConfiguration: Record "MCP Configuration"): Boolean
268261
begin
269262
exit(MCPConfiguration.Name = '');
@@ -639,6 +632,19 @@ codeunit 8351 "MCP Config Implementation"
639632
#endregion
640633

641634
#region Connection String
635+
internal procedure CreateVSCodeEntraApplication()
636+
var
637+
MCPEntraApplication: Record "MCP Entra Application";
638+
begin
639+
if MCPEntraApplication.Get(VSCodeAppNameLbl) then
640+
exit;
641+
642+
MCPEntraApplication.Name := VSCodeAppNameLbl;
643+
MCPEntraApplication.Description := VSCodeAppDescriptionLbl;
644+
Evaluate(MCPEntraApplication."Client ID", VSCodeClientIdLbl);
645+
MCPEntraApplication.Insert();
646+
end;
647+
642648
internal procedure ShowConnectionString(ConfigurationName: Text[100])
643649
var
644650
MCPConnectionString: Page "MCP Connection String";
@@ -724,6 +730,189 @@ codeunit 8351 "MCP Config Implementation"
724730
end;
725731
#endregion
726732

733+
#region Export/Import
734+
internal procedure ExportConfigurationToFile(ConfigId: Guid; ConfigName: Text[100])
735+
var
736+
TempBlob: Codeunit "Temp Blob";
737+
OutStream: OutStream;
738+
InStream: InStream;
739+
FileName: Text;
740+
begin
741+
TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
742+
ExportConfiguration(ConfigId, OutStream);
743+
TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
744+
FileName := StrSubstNo(ExportFileNameTxt, ConfigName, Format(Today(), 0, '<Year4>-<Month,2>-<Day,2>'));
745+
DownloadFromStream(InStream, ExportTitleTxt, '', JsonFilterTxt, FileName);
746+
end;
747+
748+
internal procedure ImportConfigurationFromFile()
749+
var
750+
MCPConfiguration: Record "MCP Configuration";
751+
TempBlob: Codeunit "Temp Blob";
752+
MCPCopyConfig: Page "MCP Copy Config";
753+
InStream: InStream;
754+
OutStream: OutStream;
755+
FileName: Text;
756+
ConfigName: Text[100];
757+
ConfigDescription: Text[250];
758+
begin
759+
if not UploadIntoStream(ImportTitleTxt, '', JsonFilterTxt, FileName, InStream) then
760+
exit;
761+
762+
TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
763+
CopyStream(OutStream, InStream);
764+
TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
765+
766+
if not GetConfigFromJson(InStream, ConfigName, ConfigDescription) then
767+
Error(InvalidJsonErr);
768+
769+
MCPConfiguration.SetRange(Name, ConfigName);
770+
if not MCPConfiguration.IsEmpty() then begin
771+
MCPCopyConfig.SetConfigName(ConfigName);
772+
MCPCopyConfig.SetConfigDescription(ConfigDescription);
773+
MCPCopyConfig.SetInstructionMessage(StrSubstNo(ConfigNameExistsMsg, ConfigName));
774+
MCPCopyConfig.LookupMode := true;
775+
if MCPCopyConfig.RunModal() <> Action::LookupOK then
776+
exit;
777+
ConfigName := MCPCopyConfig.GetConfigName();
778+
ConfigDescription := MCPCopyConfig.GetConfigDescription();
779+
end;
780+
781+
TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
782+
ImportConfiguration(InStream, ConfigName, ConfigDescription);
783+
end;
784+
785+
internal procedure ExportConfiguration(ConfigId: Guid; var OutStream: OutStream)
786+
var
787+
MCPConfiguration: Record "MCP Configuration";
788+
MCPConfigurationTool: Record "MCP Configuration Tool";
789+
ConfigJson: JsonObject;
790+
ToolsArray: JsonArray;
791+
ToolJson: JsonObject;
792+
OutputText: Text;
793+
begin
794+
if not MCPConfiguration.GetBySystemId(ConfigId) then
795+
exit;
796+
797+
ConfigJson.Add('name', MCPConfiguration.Name);
798+
ConfigJson.Add('description', MCPConfiguration.Description);
799+
ConfigJson.Add('enableDynamicToolMode', MCPConfiguration.EnableDynamicToolMode);
800+
ConfigJson.Add('discoverReadOnlyObjects', MCPConfiguration.DiscoverReadOnlyObjects);
801+
ConfigJson.Add('allowProdChanges', MCPConfiguration.AllowProdChanges);
802+
803+
MCPConfigurationTool.SetRange(ID, ConfigId);
804+
if MCPConfigurationTool.FindSet() then
805+
repeat
806+
Clear(ToolJson);
807+
ToolJson.Add('objectType', Format(MCPConfigurationTool."Object Type"));
808+
ToolJson.Add('objectId', MCPConfigurationTool."Object ID");
809+
ToolJson.Add('allowRead', MCPConfigurationTool."Allow Read");
810+
ToolJson.Add('allowCreate', MCPConfigurationTool."Allow Create");
811+
ToolJson.Add('allowModify', MCPConfigurationTool."Allow Modify");
812+
ToolJson.Add('allowDelete', MCPConfigurationTool."Allow Delete");
813+
ToolJson.Add('allowBoundActions', MCPConfigurationTool."Allow Bound Actions");
814+
ToolsArray.Add(ToolJson);
815+
until MCPConfigurationTool.Next() = 0;
816+
817+
ConfigJson.Add('tools', ToolsArray);
818+
ConfigJson.WriteTo(OutputText);
819+
OutStream.WriteText(OutputText);
820+
end;
821+
822+
local procedure GetConfigFromJson(var InStream: InStream; var ConfigName: Text[100]; var ConfigDescription: Text[250]): Boolean
823+
var
824+
ConfigJson: JsonObject;
825+
JsonToken: JsonToken;
826+
InputText: Text;
827+
begin
828+
InStream.ReadText(InputText);
829+
if not ConfigJson.ReadFrom(InputText) then
830+
exit(false);
831+
832+
if not ConfigJson.Get('name', JsonToken) then
833+
exit(false);
834+
835+
ConfigName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigName));
836+
837+
if ConfigJson.Get('description', JsonToken) then
838+
ConfigDescription := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(ConfigDescription));
839+
840+
exit(true);
841+
end;
842+
843+
internal procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid
844+
var
845+
MCPConfiguration: Record "MCP Configuration";
846+
ConfigJson: JsonObject;
847+
ToolsArray: JsonArray;
848+
ToolToken: JsonToken;
849+
InputText: Text;
850+
begin
851+
InStream.ReadText(InputText);
852+
if not ConfigJson.ReadFrom(InputText) then
853+
exit;
854+
855+
MCPConfiguration.Name := NewName;
856+
MCPConfiguration.Description := NewDescription;
857+
MCPConfiguration.Active := false;
858+
859+
if ConfigJson.Contains('enableDynamicToolMode') then
860+
MCPConfiguration.EnableDynamicToolMode := ConfigJson.GetBoolean('enableDynamicToolMode');
861+
862+
if ConfigJson.Contains('discoverReadOnlyObjects') then
863+
MCPConfiguration.DiscoverReadOnlyObjects := ConfigJson.GetBoolean('discoverReadOnlyObjects');
864+
865+
if ConfigJson.Contains('allowProdChanges') then
866+
MCPConfiguration.AllowProdChanges := ConfigJson.GetBoolean('allowProdChanges');
867+
868+
MCPConfiguration.Insert();
869+
LogConfigurationCreated(MCPConfiguration);
870+
871+
if ConfigJson.Contains('tools') then begin
872+
ToolsArray := ConfigJson.GetArray('tools');
873+
foreach ToolToken in ToolsArray do
874+
ImportTool(MCPConfiguration.SystemId, ToolToken.AsObject());
875+
end;
876+
877+
exit(MCPConfiguration.SystemId);
878+
end;
879+
880+
local procedure ImportTool(ConfigId: Guid; ToolJson: JsonObject)
881+
var
882+
MCPConfigurationTool: Record "MCP Configuration Tool";
883+
ObjectTypeText: Text;
884+
begin
885+
MCPConfigurationTool.Init();
886+
MCPConfigurationTool.ID := ConfigId;
887+
888+
if ToolJson.Contains('objectType') then begin
889+
ObjectTypeText := ToolJson.GetText('objectType');
890+
if ObjectTypeText = 'Page' then
891+
MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page;
892+
end;
893+
894+
if ToolJson.Contains('objectId') then
895+
MCPConfigurationTool."Object ID" := ToolJson.GetInteger('objectId');
896+
897+
if ToolJson.Contains('allowRead') then
898+
MCPConfigurationTool."Allow Read" := ToolJson.GetBoolean('allowRead');
899+
900+
if ToolJson.Contains('allowCreate') then
901+
MCPConfigurationTool."Allow Create" := ToolJson.GetBoolean('allowCreate');
902+
903+
if ToolJson.Contains('allowModify') then
904+
MCPConfigurationTool."Allow Modify" := ToolJson.GetBoolean('allowModify');
905+
906+
if ToolJson.Contains('allowDelete') then
907+
MCPConfigurationTool."Allow Delete" := ToolJson.GetBoolean('allowDelete');
908+
909+
if ToolJson.Contains('allowBoundActions') then
910+
MCPConfigurationTool."Allow Bound Actions" := ToolJson.GetBoolean('allowBoundActions');
911+
912+
MCPConfigurationTool.Insert();
913+
end;
914+
#endregion
915+
727916
#if not CLEAN28
728917
internal procedure IsFeatureEnabled(): Boolean
729918
var

src/System Application/App/MCP/src/Configuration/Pages/MCPConfigCard.Page.al

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ page 8351 "MCP Config Card"
142142
{
143143
Caption = 'Connection String';
144144
ToolTip = 'Generate a connection string for this MCP configuration to use in your MCP client.';
145-
Image = Export;
145+
Image = Link;
146146

147147
trigger OnAction()
148148
begin

src/System Application/App/MCP/src/Configuration/Pages/MCPConfigList.Page.al

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ page 8350 "MCP Config List"
5151
ToolTip = 'Creates a copy of the current MCP configuration, including its tools and permissions.';
5252
Image = Copy;
5353
AccessByPermission = tabledata "MCP Configuration" = IM;
54+
Scope = Repeater;
5455

5556
trigger OnAction()
5657
var
@@ -71,7 +72,7 @@ page 8350 "MCP Config List"
7172
{
7273
Caption = 'Connection String';
7374
ToolTip = 'Generate a connection string for this MCP configuration to use in your MCP client.';
74-
Image = Export;
75+
Image = Link;
7576
Scope = Repeater;
7677

7778
trigger OnAction()
@@ -88,6 +89,35 @@ page 8350 "MCP Config List"
8889
Image = Setup;
8990
RunObject = page "MCP Entra Application List";
9091
}
92+
action(ExportConfiguration)
93+
{
94+
Caption = 'Export';
95+
ToolTip = 'Export the selected MCP configuration and its tools to a JSON file.';
96+
Image = Export;
97+
Scope = Repeater;
98+
99+
trigger OnAction()
100+
var
101+
MCPConfigImplementation: Codeunit "MCP Config Implementation";
102+
begin
103+
MCPConfigImplementation.ExportConfigurationToFile(Rec.SystemId, Rec.Name);
104+
end;
105+
}
106+
action(ImportConfiguration)
107+
{
108+
Caption = 'Import';
109+
ToolTip = 'Import an MCP configuration and its tools from a JSON file.';
110+
Image = Import;
111+
AccessByPermission = tabledata "MCP Configuration" = IM;
112+
113+
trigger OnAction()
114+
var
115+
MCPConfigImplementation: Codeunit "MCP Config Implementation";
116+
begin
117+
MCPConfigImplementation.ImportConfigurationFromFile();
118+
CurrPage.Update(false);
119+
end;
120+
}
91121
}
92122
}
93123
area(Promoted)
@@ -99,6 +129,8 @@ page 8350 "MCP Config List"
99129

100130
actionref(Promoted_GenerateConnectionString; GenerateConnectionString) { }
101131
actionref(Promoted_MCPEntraApplications; MCPEntraApplications) { }
132+
actionref(Promoted_ExportConfiguration; ExportConfiguration) { }
133+
actionref(Promoted_ImportConfiguration; ImportConfiguration) { }
102134
}
103135
}
104136
}
@@ -131,4 +163,5 @@ page 8350 "MCP Config List"
131163
FeatureNotEnabledErr: Label 'MCP server feature is not enabled. Please contact your system administrator to enable the feature.';
132164
GoToFeatureManagementLbl: Label 'Go to Feature Management';
133165
#endif
166+
134167
}

src/System Application/App/MCP/src/Configuration/Pages/MCPConfigToolList.Page.al

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ page 8352 "MCP Config Tool List"
4141
exit;
4242

4343
repeat
44+
if MCPConfigImplementation.CheckAPIToolExists(Rec.ID, PageMetadata.ID) then
45+
continue;
4446
MCPConfig.CreateAPITool(Rec.ID, PageMetadata.ID);
4547
until PageMetadata.Next() = 0;
4648

@@ -104,6 +106,8 @@ page 8352 "MCP Config Tool List"
104106
exit;
105107

106108
repeat
109+
if MCPConfigImplementation.CheckAPIToolExists(Rec.ID, PageMetadata.ID) then
110+
continue;
107111
MCPConfig.CreateAPITool(Rec.ID, PageMetadata.ID);
108112
until PageMetadata.Next() = 0;
109113

0 commit comments

Comments
 (0)