Codeunit 8351 MCP Config Implementation, source in 28

Source272829metadata in 28

src/System Application/App/MCP/src/Configuration/Codeunits/MCPConfigImplementation.Codeunit.al1400 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// 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.Azure.Identity;
using System.Environment;
using System.Feedback;
using System.Reflection;
using System.Text;
using System.Utilities;

codeunit 8351 "MCP Config Implementation"
{
    Access = Internal;
    InherentEntitlements = X;
    InherentPermissions = X;

    var
        DefaultConfigCannotBeDeactivatedErr: Label 'The default configuration cannot be deactivated.';
        DefaultConfigCannotBeDeletedErr: Label 'The default configuration cannot be deleted.';
        DynamicToolModeCannotBeDisabledErr: Label 'Dynamic tool mode cannot be disabled for the default configuration.';
        DiscoverReadOnlyObjectsCannotBeDisabledErr: Label 'Access to all read-only objects cannot be disabled for the default configuration.';
        CreateUpdateDeleteNotAllowedErr: Label 'Create, update and delete tools are not allowed for this MCP configuration.';
        ToolsCannotBeAddedToDefaultConfigErr: Label 'Tools cannot be added to the default configuration.';
        ConfigurationNotFoundErr: Label 'The MCP configuration was not found.';
        PageNotFoundErr: Label 'Page not found.';
        QueryNotFoundErr: Label 'Query not found.';
        InvalidPageTypeErr: Label 'Only API pages are supported.';
        InvalidQueryTypeErr: Label 'Only API queries are supported.';
        InvalidAPIVersionErr: Label 'Only API v2.0 objects are supported.';
        APIToolNotSupportedErr: Label 'This API page is not available for MCP configuration.';
        DefaultMCPConfigurationDescriptionLbl: Label 'Default MCP configuration';
        DesignatedDefaultCannotBeDeactivatedErr: Label 'The designated default configuration cannot be deactivated. Clear the default designation first.';
        ConfigurationMustBeActiveErr: Label 'Only active configurations can be set as the default.';
        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;
        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.';
        MCPDefaultConfigDesignatedLbl: Label 'MCP default configuration designated', Locked = true;
        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;
        MCPPrefixProdLbl: Label 'businesscentral', Locked = true;
        MCPPrefixTIELbl: Label 'businesscentral-tie', Locked = true;
        MCPPrefixOnPremLbl: Label 'businesscentral-onprem', Locked = true;
        MCPOnPremSuffixLbl: Label '/mcp', Locked = true;
        ApiSuffixLbl: Label '/api', Locked = true;
        VSCodeAppNameLbl: Label 'VS Code', Locked = true;
        VSCodeAppDescriptionLbl: Label 'Visual Studio Code', Locked = true;
        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';
        MCPServerFeedbackConfirmQst: Label 'We noticed you no longer have any active configurations. Could you share what made you decide to stop using the MCP server? Your feedback helps us improve the experience.';
        MCPServerFeedbackQst: Label 'What could we do to improve the MCP server experience?';
        NoActiveConfigsFeedbackTxt: Label 'No active configs feedback triggered', Locked = true;
        GeneralFeedbackTxt: Label 'General MCP feedback triggered', Locked = true;

    #region Configurations
    internal procedure GetConfigurationIdByName(Name: Text[100]): Guid
    var
        MCPConfiguration: Record "MCP Configuration";
        EmptyGuid: Guid;
    begin
        if MCPConfiguration.Get(Name) then
            exit(MCPConfiguration.SystemId);

        exit(EmptyGuid);
    end;

    internal procedure CreateConfiguration(Name: Text[100]; Description: Text[250]): Guid
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        MCPConfiguration.Name := Name;
        MCPConfiguration.Description := Description;
        MCPConfiguration.Insert();
        LogConfigurationCreated(MCPConfiguration);
        exit(MCPConfiguration.SystemId);
    end;

    internal procedure ActivateConfiguration(ConfigId: Guid; Active: Boolean)
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if not Active and IsDefaultConfiguration(MCPConfiguration) then
            Error(DefaultConfigCannotBeDeactivatedErr);

        if not Active and IsDesignatedDefaultConfiguration(MCPConfiguration) then
            Error(DesignatedDefaultCannotBeDeactivatedErr);

        MCPConfiguration.Active := Active;
        MCPConfiguration.Modify();

    end;

    internal procedure AllowCreateUpdateDeleteTools(ConfigId: Guid; Allow: Boolean)
    var
        MCPConfiguration: Record "MCP Configuration";
        xMCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        xMCPConfiguration := MCPConfiguration;

        if not Allow then
            DisableCreateUpdateDeleteToolsInConfig(ConfigId);

        MCPConfiguration.AllowProdChanges := Allow;
        MCPConfiguration.Modify();
        LogConfigurationModified(MCPConfiguration, xMCPConfiguration);
    end;

    internal procedure DisableCreateUpdateDeleteToolsInConfig(ConfigId: Guid)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        MCPConfigurationTool.SetRange("Object Type", MCPConfigurationTool."Object Type"::Page);
        MCPConfigurationTool.SetRange(ID, ConfigId);
        if MCPConfigurationTool.IsEmpty() then
            exit;

        MCPConfigurationTool.ModifyAll("Allow Create", false);
        MCPConfigurationTool.ModifyAll("Allow Modify", false);
        MCPConfigurationTool.ModifyAll("Allow Delete", false);
        MCPConfigurationTool.ModifyAll("Allow Bound Actions", false);
    end;

    internal procedure DeleteConfiguration(ConfigId: Guid)
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if IsDefaultConfiguration(MCPConfiguration) then
            Error(DefaultConfigCannotBeDeletedErr);

        if IsDesignatedDefaultConfiguration(MCPConfiguration) then
            MarkSystemDefaultAsDefault();

        LogConfigurationDeleted(MCPConfiguration);
        MCPConfiguration.Delete();
    end;

    internal procedure CopyConfiguration(SourceConfigId: Guid)
    var
        MCPCopyConfig: Page "MCP Copy Config";
        ConfigName: Text[100];
        ConfigDescription: Text[250];
    begin
        MCPCopyConfig.LookupMode := true;
        if MCPCopyConfig.RunModal() <> Action::LookupOK then
            exit;

        ConfigName := MCPCopyConfig.GetConfigName();
        ConfigDescription := MCPCopyConfig.GetConfigDescription();

        CopyConfiguration(SourceConfigId, ConfigName, ConfigDescription);
    end;

    internal procedure CopyConfiguration(SourceConfigId: Guid; NewName: Text[100]; NewDescription: Text[250]): Guid
    var
        SourceMCPConfiguration: Record "MCP Configuration";
        NewMCPConfiguration: Record "MCP Configuration";
    begin
        if not SourceMCPConfiguration.GetBySystemId(SourceConfigId) then
            Error(ConfigurationNotFoundErr);

        NewMCPConfiguration.Copy(SourceMCPConfiguration);
        NewMCPConfiguration.Name := NewName;
        NewMCPConfiguration.Description := NewDescription;
        NewMCPConfiguration.Default := false;
        NewMCPConfiguration.Insert();

        CopyTools(SourceMCPConfiguration, NewMCPConfiguration);

        LogConfigurationCreated(NewMCPConfiguration);
        exit(NewMCPConfiguration.SystemId);
    end;

    local procedure CopyTools(SourceConfig: Record "MCP Configuration"; NewConfig: Record "MCP Configuration")
    var
        SourceMCPConfigurationTool: Record "MCP Configuration Tool";
        NewMCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        SourceMCPConfigurationTool.SetRange(ID, SourceConfig.SystemId);
        if not SourceMCPConfigurationTool.FindSet() then
            exit;

        repeat
            NewMCPConfigurationTool.Copy(SourceMCPConfigurationTool);
            NewMCPConfigurationTool.ID := NewConfig.SystemId;
            NewMCPConfigurationTool.Insert();
        until SourceMCPConfigurationTool.Next() = 0;
    end;

    internal procedure EnableDynamicToolMode(ConfigId: Guid; Enable: Boolean)
    var
        MCPConfiguration: Record "MCP Configuration";
        xMCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        xMCPConfiguration := MCPConfiguration;

        if not Enable and IsDefaultConfiguration(MCPConfiguration) then
            Error(DynamicToolModeCannotBeDisabledErr);

        MCPConfiguration.EnableDynamicToolMode := Enable;
        if not Enable then
            MCPConfiguration.DiscoverReadOnlyObjects := false;
        MCPConfiguration.Modify();
        LogConfigurationModified(MCPConfiguration, xMCPConfiguration);
    end;

    internal procedure EnableDiscoverReadOnlyObjects(ConfigId: Guid; Enable: Boolean)
    var
        MCPConfiguration: Record "MCP Configuration";
        xMCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        xMCPConfiguration := MCPConfiguration;

        if not Enable and IsDefaultConfiguration(MCPConfiguration) then
            Error(DiscoverReadOnlyObjectsCannotBeDisabledErr);

        if Enable and not MCPConfiguration.EnableDynamicToolMode then
            Error(DynamicToolModeRequiredErr);

        MCPConfiguration.DiscoverReadOnlyObjects := Enable;
        MCPConfiguration.Modify();
        LogConfigurationModified(MCPConfiguration, xMCPConfiguration);
    end;

    local procedure CheckAllowCreateUpdateDeleteTools(ConfigId: Guid)
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if not MCPConfiguration.AllowProdChanges then
            Error(CreateUpdateDeleteNotAllowedErr);
    end;

    internal procedure CreateDefaultConfiguration()
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        if not IsNullGuid(GetConfigurationIdByName('')) then
            exit;

        MCPConfiguration.Name := '';
        MCPConfiguration.Description := DefaultMCPConfigurationDescriptionLbl;
        MCPConfiguration.Active := true;
        MCPConfiguration.EnableDynamicToolMode := true;
        MCPConfiguration.DiscoverReadOnlyObjects := true;
        MCPConfiguration.AllowProdChanges := true;
        MCPConfiguration.Default := true;
        MCPConfiguration.Insert();
    end;

    internal procedure IsDefaultConfiguration(MCPConfiguration: Record "MCP Configuration"): Boolean
    begin
        exit(MCPConfiguration.Name = '');
    end;

    internal procedure IsDesignatedDefaultConfiguration(MCPConfiguration: Record "MCP Configuration"): Boolean
    begin
        exit(MCPConfiguration.Default);
    end;

    internal procedure SetAsDefaultConfiguration(ConfigId: Guid)
    var
        MCPConfiguration: Record "MCP Configuration";
        PreviousDefault: Record "MCP Configuration";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if not MCPConfiguration.Active then
            Error(ConfigurationMustBeActiveErr);

        PreviousDefault.SetRange(Default, true);
        PreviousDefault.ModifyAll(Default, false);

        MCPConfiguration.Default := true;
        MCPConfiguration.Modify();

        Session.LogMessage('0000R0R', MCPDefaultConfigDesignatedLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, GetDimensions(MCPConfiguration));
    end;

    internal procedure ClearDefaultConfiguration()
    var
        MCPConfiguration: Record "MCP Configuration";
        SystemDefault: Record "MCP Configuration";
    begin
        MCPConfiguration.SetRange(Default, true);
        MCPConfiguration.SetFilter(Name, '<>%1', '');
        MCPConfiguration.ModifyAll(Default, false);

        if SystemDefault.Get('') then begin
            SystemDefault.Default := true;
            SystemDefault.Modify();
        end;
    end;

    local procedure MarkSystemDefaultAsDefault()
    var
        SystemDefault: Record "MCP Configuration";
    begin
        if not SystemDefault.Get('') then
            exit;

        SystemDefault.Default := true;
        SystemDefault.Modify();
    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
    internal procedure CreateAPIPageTool(ConfigId: Guid; APIPageId: Integer; ValidateAPIPublisher: Boolean): Guid
    var
        MCPConfiguration: Record "MCP Configuration";
        MCPConfigurationTool: Record "MCP Configuration Tool";
        PageMetadata: Record "Page Metadata";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if IsDefaultConfiguration(MCPConfiguration) then
            Error(ToolsCannotBeAddedToDefaultConfigErr);

        PageMetadata := ValidateAPIPageTool(APIPageId, ValidateAPIPublisher);

        MCPConfigurationTool.ID := ConfigId;
        MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page;
        MCPConfigurationTool."Object ID" := APIPageId;
        MCPConfigurationTool."Allow Read" := true;
        MCPConfigurationTool."API Version" := GetHighestAPIPageVersion(PageMetadata);
        MCPConfigurationTool.Insert();
        exit(MCPConfigurationTool.SystemId);
    end;

    internal procedure GetAPIToolId(ConfigId: Guid; ObjectId: Integer; ObjectType: Option): Guid
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
        EmptyGuid: Guid;
    begin
        if MCPConfigurationTool.Get(ConfigId, ObjectType, ObjectId) then
            exit(MCPConfigurationTool.SystemId);

        exit(EmptyGuid);
    end;

    internal procedure CreateAPIQueryTool(ConfigId: Guid; QueryAPIId: Integer): Guid
    var
        MCPConfiguration: Record "MCP Configuration";
        MCPConfigurationTool: Record "MCP Configuration Tool";
        QueryMetadata: Record "Query Metadata";
    begin
        if not MCPConfiguration.GetBySystemId(ConfigId) then
            Error(ConfigurationNotFoundErr);

        if IsDefaultConfiguration(MCPConfiguration) then
            Error(ToolsCannotBeAddedToDefaultConfigErr);

        QueryMetadata := ValidateAPIQueryTool(QueryAPIId);

        MCPConfigurationTool.ID := ConfigId;
        MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Query;
        MCPConfigurationTool."Object ID" := QueryAPIId;
        MCPConfigurationTool."Allow Read" := true;
        MCPConfigurationTool."API Version" := GetHighestAPIQueryVersion(QueryMetadata);
        MCPConfigurationTool.Insert();
        exit(MCPConfigurationTool.SystemId);
    end;

    internal procedure DeleteTool(ToolId: Guid)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        MCPConfigurationTool.Delete();
    end;

    internal procedure AllowRead(ToolId: Guid; Allow: Boolean)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        MCPConfigurationTool."Allow Read" := Allow;
        MCPConfigurationTool.Modify();
    end;

    internal procedure AllowCreate(ToolId: Guid; Allow: Boolean)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        if MCPConfigurationTool."Object Type" = MCPConfigurationTool."Object Type"::Query then
            exit; // Create is not applicable for query tools

        if Allow then
            CheckAllowCreateUpdateDeleteTools(MCPConfigurationTool.ID);

        MCPConfigurationTool."Allow Create" := Allow;
        MCPConfigurationTool.Modify();
    end;

    internal procedure AllowModify(ToolId: Guid; Allow: Boolean)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        if MCPConfigurationTool."Object Type" = MCPConfigurationTool."Object Type"::Query then
            exit; // Modify is not applicable for query tools

        if Allow then
            CheckAllowCreateUpdateDeleteTools(MCPConfigurationTool.ID);

        MCPConfigurationTool."Allow Modify" := Allow;
        MCPConfigurationTool.Modify();
    end;

    internal procedure AllowDelete(ToolId: Guid; Allow: Boolean)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        if MCPConfigurationTool."Object Type" = MCPConfigurationTool."Object Type"::Query then
            exit; // Delete is not applicable for query tools

        if Allow then
            CheckAllowCreateUpdateDeleteTools(MCPConfigurationTool.ID);

        MCPConfigurationTool."Allow Delete" := Allow;
        MCPConfigurationTool.Modify();
    end;

    internal procedure AllowBoundActions(ToolId: Guid; Allow: Boolean)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit;

        if MCPConfigurationTool."Object Type" = MCPConfigurationTool."Object Type"::Query then
            exit; // Bound actions is not applicable for query tools

        if Allow then
            CheckAllowCreateUpdateDeleteTools(MCPConfigurationTool.ID);

        MCPConfigurationTool."Allow Bound Actions" := Allow;
        MCPConfigurationTool.Modify();
    end;

    internal procedure LookupAPIPageTools(var PageMetadata: Record "Page Metadata"): Boolean
    var
        MCPAPIConfigToolLookup: Page "MCP API Config Tool Lookup";
    begin
        PageMetadata.SetRange(PageType, PageMetadata.PageType::API);
        PageMetadata.SetFilter("AL Namespace", '<>%1', 'Microsoft.API.V1');
        PageMetadata.SetFilter(APIVersion, '<>%1', 'beta');

        MCPAPIConfigToolLookup.LookupMode := true;
        MCPAPIConfigToolLookup.SetTableView(PageMetadata);
        if MCPAPIConfigToolLookup.RunModal() <> Action::LookupOK then
            exit(false);

        MCPAPIConfigToolLookup.SetSelectionFilter(PageMetadata);
        exit(true);
    end;

    internal procedure LookupAPIQueryTools(var QueryMetadata: Record "Query Metadata"): Boolean
    var
        MCPQueryConfigToolLookup: Page "MCP Query Config Tool Lookup";
    begin
        QueryMetadata.SetFilter(EntityName, '<>%1', '');
        QueryMetadata.SetFilter("AL Namespace", '<>%1', 'Microsoft.API.V1');
        QueryMetadata.SetFilter(ID, '<>%1&<>%2', 5480, 5481); // Exclude beta customer and vendor queries from Base Application, as they are already part of API v2.0

        MCPQueryConfigToolLookup.LookupMode := true;
        MCPQueryConfigToolLookup.SetTableView(QueryMetadata);
        if MCPQueryConfigToolLookup.RunModal() <> Action::LookupOK then
            exit(false);

        MCPQueryConfigToolLookup.SetSelectionFilter(QueryMetadata);
        exit(true);
    end;

    internal procedure GetAPIPublishers(var MCPAPIPublisherGroup: Record "MCP API Publisher Group")
    begin
        GetAPIPagePublishers(MCPAPIPublisherGroup);
        GetAPIQueryPublishers(MCPAPIPublisherGroup);
    end;

    local procedure GetAPIPagePublishers(var MCPAPIPublisherGroup: Record "MCP API Publisher Group")
    var
        PageMetadata: Record "Page Metadata";
    begin
        PageMetadata.SetLoadFields(PageType, APIPublisher, APIGroup);
        PageMetadata.SetRange(PageType, PageMetadata.PageType::API);
        PageMetadata.SetFilter(APIPublisher, '<>%1', '');
        PageMetadata.SetFilter("AL Namespace", '<>%1', 'Microsoft.API.V1');
        PageMetadata.SetFilter(APIVersion, '<>%1', 'beta');

        if not PageMetadata.FindSet() then
            exit;

        repeat
            if MCPAPIPublisherGroup.Get(PageMetadata.APIPublisher, PageMetadata.APIGroup) then
                continue;
            MCPAPIPublisherGroup."API Publisher" := PageMetadata.APIPublisher;
            MCPAPIPublisherGroup."API Group" := PageMetadata.APIGroup;
            MCPAPIPublisherGroup.Insert();
        until PageMetadata.Next() = 0;
    end;

    local procedure GetAPIQueryPublishers(var MCPAPIPublisherGroup: Record "MCP API Publisher Group")
    var
        QueryMetadata: Record "Query Metadata";
    begin
        QueryMetadata.SetLoadFields(EntityName, APIPublisher, APIGroup);
        QueryMetadata.SetFilter(EntityName, '<>%1', '');
        QueryMetadata.SetFilter(APIPublisher, '<>%1', '');
        QueryMetadata.SetFilter("AL Namespace", '<>%1', 'Microsoft.API.V1');

        if not QueryMetadata.FindSet() then
            exit;

        repeat
            if MCPAPIPublisherGroup.Get(QueryMetadata.APIPublisher, QueryMetadata.APIGroup) then
                continue;
            MCPAPIPublisherGroup."API Publisher" := QueryMetadata.APIPublisher;
            MCPAPIPublisherGroup."API Group" := QueryMetadata.APIGroup;
            MCPAPIPublisherGroup.Insert();
        until QueryMetadata.Next() = 0;
    end;

    internal procedure LookupAPIPublisher(var MCPAPIPublisherGroup: Record "MCP API Publisher Group"; var APIPublisher: Text; var APIGroup: Text)
    begin
        if Page.RunModal(Page::"MCP API Publisher Lookup", MCPAPIPublisherGroup) = Action::LookupOK then begin
            APIPublisher := MCPAPIPublisherGroup."API Publisher";
            APIGroup := MCPAPIPublisherGroup."API Group";
        end;
    end;

    internal procedure LookupAPIGroup(var MCPAPIPublisherGroup: Record "MCP API Publisher Group"; APIPublisher: Text; var APIGroup: Text)
    begin
        MCPAPIPublisherGroup.SetRange("API Publisher", APIPublisher);
        if MCPAPIPublisherGroup.IsEmpty() then
            exit;

        if Page.RunModal(Page::"MCP API Publisher Lookup", MCPAPIPublisherGroup) = Action::LookupOK then
            APIGroup := MCPAPIPublisherGroup."API Group";
    end;

    internal procedure ValidateAPIPageTool(PageId: Integer; ValidateAPIPublisher: Boolean): Record "Page Metadata"
    var
        PageMetadata: Record "Page Metadata";
    begin
        if not PageMetadata.Get(PageId) then
            Error(PageNotFoundErr);

        if PageMetadata.PageType <> PageMetadata.PageType::API then
            Error(InvalidPageTypeErr);

        if not ValidateAPIPublisher then
            exit(PageMetadata);

        if PageMetadata."AL Namespace" = 'Microsoft.API.V1' then
            Error(APIToolNotSupportedErr);

        if PageMetadata.APIVersion = 'beta' then
            Error(APIToolNotSupportedErr);

        exit(PageMetadata);
    end;

    internal procedure ValidateAPIQueryTool(QueryId: Integer): Record "Query Metadata"
    var
        QueryMetadata: Record "Query Metadata";
    begin
        if not QueryMetadata.Get(QueryId) then
            Error(QueryNotFoundErr);

        if QueryMetadata.EntityName = '' then
            Error(InvalidQueryTypeErr);

        if QueryMetadata."AL Namespace" = 'Microsoft.API.V1' then
            Error(InvalidAPIVersionErr);

        exit(QueryMetadata);
    end;

    internal procedure AddToolsByAPIGroup(ConfigId: Guid)
    var
        MCPToolsByAPIGroup: Page "MCP Tools By API Group";
        APIGroup: Text;
        APIPublisher: Text;
    begin
        MCPToolsByAPIGroup.LookupMode := true;
        if MCPToolsByAPIGroup.RunModal() <> Action::LookupOK then
            exit;

        APIGroup := MCPToolsByAPIGroup.GetAPIGroup();
        APIPublisher := MCPToolsByAPIGroup.GetAPIPublisher();

        if (APIGroup = '') or (APIPublisher = '') then
            exit;

        AddAPIPageTools(ConfigId, APIPublisher, APIGroup);
        AddAPIQueryTools(ConfigId, APIPublisher, APIGroup);
    end;

    local procedure AddAPIPageTools(ConfigId: Guid; APIPublisher: Text; APIGroup: Text)
    var
        PageMetadata: Record "Page Metadata";
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        PageMetadata.SetRange(PageType, PageMetadata.PageType::API);
        PageMetadata.SetFilter(APIPublisher, APIPublisher);
        PageMetadata.SetFilter(APIGroup, APIGroup);
        PageMetadata.SetFilter("AL Namespace", '<>%1', 'Microsoft.API.V1');
        PageMetadata.SetFilter(APIVersion, '<>%1', 'beta');

        if not PageMetadata.FindSet() then
            exit;

        repeat
            if CheckAPIToolExists(ConfigId, PageMetadata.ID, MCPConfigurationTool."Object Type"::Page) then
                continue;
            CreateAPIPageTool(ConfigId, PageMetadata.ID, false);
        until PageMetadata.Next() = 0;
    end;

    local procedure AddAPIQueryTools(ConfigId: Guid; APIPublisher: Text; APIGroup: Text)
    var
        QueryMetadata: Record "Query Metadata";
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        QueryMetadata.SetFilter(EntityName, '<>%1', '');
        QueryMetadata.SetFilter(APIPublisher, APIPublisher);
        QueryMetadata.SetFilter(APIGroup, APIGroup);
        if not QueryMetadata.FindSet() then
            exit;

        repeat
            if CheckAPIToolExists(ConfigId, QueryMetadata.ID, MCPConfigurationTool."Object Type"::Query) then
                continue;
            CreateAPIQueryTool(ConfigId, QueryMetadata.ID);
        until QueryMetadata.Next() = 0;
    end;

    internal procedure AddStandardAPITools(ConfigId: Guid)
    var
        PageMetadata: Record "Page Metadata";
        QueryMetadata: Record "Query Metadata";
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        PageMetadata.SetRange(PageType, PageMetadata.PageType::API);
        PageMetadata.SetFilter(APIPublisher, '=%1', '');
        PageMetadata.SetFilter(APIGroup, '=%1', '');
        PageMetadata.SetRange(APIVersion, 'v2.0');
        if PageMetadata.FindSet() then
            repeat
                if CheckAPIToolExists(ConfigId, PageMetadata.ID, MCPConfigurationTool."Object Type"::Page) then
                    continue;
                CreateAPIPageTool(ConfigId, PageMetadata.ID, false);
            until PageMetadata.Next() = 0;

        QueryMetadata.SetFilter(EntityName, '<>%1', '');
        QueryMetadata.SetFilter(APIPublisher, '=%1', '');
        QueryMetadata.SetFilter(APIGroup, '=%1', '');
        QueryMetadata.SetRange(APIVersion, 'v2.0');
        if QueryMetadata.FindSet() then
            repeat
                if CheckAPIToolExists(ConfigId, QueryMetadata.ID, MCPConfigurationTool."Object Type"::Query) then
                    continue;
                CreateAPIQueryTool(ConfigId, QueryMetadata.ID);
            until QueryMetadata.Next() = 0;
    end;

    internal procedure CheckAPIToolExists(ConfigId: Guid; ObjectId: Integer; ObjectType: Option): Boolean
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
    begin
        MCPConfigurationTool.SetRange(ID, ConfigId);
        MCPConfigurationTool.SetRange("Object Type", ObjectType);
        MCPConfigurationTool.SetRange("Object ID", ObjectId);
        exit(not MCPConfigurationTool.IsEmpty());
    end;

    internal procedure GetObjectCaption(ToolId: Guid): Text[100]
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
        AllObjWithCaption: Record AllObjWithCaption;
        ObjectType: Option "TableData","Table",,"Report",,"Codeunit","XMLport","MenuSuite","Page","Query","System","FieldNumber",,,"PageExtension","TableExtension","Enum","EnumExtension","Profile","ProfileExtension","PermissionSet","PermissionSetExtension","ReportExtension";
    begin
        if not MCPConfigurationTool.GetBySystemId(ToolId) then
            exit('');

        case MCPConfigurationTool."Object Type" of
            MCPConfigurationTool."Object Type"::Page:
                ObjectType := ObjectType::Page;
            MCPConfigurationTool."Object Type"::Query:
                ObjectType := ObjectType::Query;
        end;

        if AllObjWithCaption.Get(ObjectType, MCPConfigurationTool."Object ID") then
            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;

    internal procedure ValidateAPIPageVersion(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 ValidateAPIQueryVersion(ObjectId: Integer; APIVersion: Text)
    var
        QueryMetadata: Record "Query Metadata";
        Versions: List of [Text];
    begin
        if not QueryMetadata.Get(ObjectId) then
            exit;

        Versions := QueryMetadata.APIVersion.Split(',');
        if not Versions.Contains(APIVersion) then
            Error(VersionNotValidErr);
    end;

    internal procedure LookupAPIPageVersions(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 LookupAPIQueryVersions(QueryId: Integer; var APIVersion: Text[30])
    var
        QueryMetadata: Record "Query Metadata";
        MCPAPIVersion: Record "MCP API Version";
        Versions: List of [Text];
        Version: Text[30];
    begin
        if not QueryMetadata.Get(QueryId) then
            exit;

        Versions := QueryMetadata.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 GetHighestAPIPageVersion(PageMetadata: Record "Page Metadata"): Text[30]
    begin
        if PageMetadata.APIVersion = '' then
            exit('');

        exit(GetHighestVersion(PageMetadata.APIVersion.Split(',')));
    end;

    internal procedure GetHighestAPIQueryVersion(QueryMetadata: Record "Query Metadata"): Text[30]
    begin
        if QueryMetadata.APIVersion = '' then
            exit('');

        exit(GetHighestVersion(QueryMetadata.APIVersion.Split(',')));
    end;

    local procedure GetHighestVersion(Versions: List of [Text]): Text[30]
    var
        Version: Text;
        HighestVersion: Text;
        HighestMajor: Integer;
        HighestMinor: Integer;
        CurrentMajor: Integer;
        CurrentMinor: Integer;
    begin
        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
    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";
        ConnectionString: Text;
    begin
        ConnectionString := GenerateConnectionString(ConfigurationName);
        MCPConnectionString.SetConnectionString(ConnectionString, ConfigurationName);
        MCPConnectionString.Caption(StrSubstNo(ConnectionStringLbl, ConfigurationName));
        MCPConnectionString.RunModal();
    end;

    internal procedure GenerateConnectionString(ConfigurationName: Text[100]): Text
    var
        AzureADTenant: Codeunit "Azure AD Tenant";
        EnvironmentInformation: Codeunit "Environment Information";
        MCPUrl: Text;
        MCPPrefix: Text;
        TenantId: Text;
        EnvironmentName: Text;
        Company: Text;
        IsSaaS: Boolean;
    begin
        IsSaaS := EnvironmentInformation.IsSaaS();
        Company := CompanyName();

        GetMCPUrlAndPrefix(MCPUrl, MCPPrefix, IsSaaS);

        if IsSaaS then begin
            TenantId := AzureADTenant.GetAadTenantId();
            EnvironmentName := EnvironmentInformation.GetEnvironmentName();
        end;

        exit(BuildConnectionStringJson(MCPPrefix, MCPUrl, TenantId, EnvironmentName, Company, ConfigurationName, IsSaaS));
    end;

    local procedure GetMCPUrlAndPrefix(var MCPUrl: Text; var MCPPrefix: Text; IsSaaS: Boolean)
    var
        BaseUrl: Text;
    begin
        if not IsSaaS then begin
            BaseUrl := GetUrl(ClientType::Api).TrimEnd('/');
            if BaseUrl.EndsWith(ApiSuffixLbl) then
                BaseUrl := BaseUrl.Substring(1, StrLen(BaseUrl) - StrLen(ApiSuffixLbl));
            MCPUrl := BaseUrl + MCPOnPremSuffixLbl;
            MCPPrefix := MCPPrefixOnPremLbl;
            exit;
        end;

        if IsTIEEnvironment() then begin
            MCPUrl := MCPUrlTIELbl;
            MCPPrefix := MCPPrefixTIELbl;
        end else begin
            MCPUrl := MCPUrlProdLbl;
            MCPPrefix := MCPPrefixProdLbl;
        end;
    end;

    local procedure IsTIEEnvironment(): Boolean
    var
        Uri: Codeunit Uri;
    begin
        exit(Uri.AreURIsHaveSameHost(GetUrl(ClientType::Web), 'https://businesscentral.dynamics-tie.com'));
    end;

    local procedure BuildConnectionStringJson(MCPPrefix: Text; MCPUrl: Text; TenantId: Text; EnvironmentName: Text; Company: Text; ConfigurationName: Text[100]; IsSaaS: Boolean): Text
    var
        JsonBuilder: TextBuilder;
    begin
        JsonBuilder.AppendLine('"' + MCPPrefix + '": {');
        JsonBuilder.AppendLine('  "url": "' + MCPUrl + '",');
        JsonBuilder.AppendLine('  "type": "http",');
        JsonBuilder.AppendLine('  "headers": {');
        if IsSaaS then begin
            JsonBuilder.AppendLine('    "TenantId": "' + TenantId + '",');
            JsonBuilder.AppendLine('    "EnvironmentName": "' + EnvironmentName + '",');
        end;
        // Company and ConfigurationName are the only header values the customer can populate
        // with non-ASCII characters. The MCP server detects the "=?base64?<value>?=" wrapper
        // and decodes the UTF-8 bytes; ASCII values are emitted unchanged to remain
        // backward-compatible with existing client configurations.
        JsonBuilder.AppendLine('    "Company": "' + EncodeForMCPHeaderIfNonAscii(Company) + '",');
        JsonBuilder.AppendLine('    "ConfigurationName": "' + EncodeForMCPHeaderIfNonAscii(ConfigurationName) + '"');
        JsonBuilder.AppendLine('  }');
        JsonBuilder.AppendLine('}');
        exit(JsonBuilder.ToText());
    end;

    internal procedure EncodeForMCPHeaderIfNonAscii(Value: Text): Text
    var
        Base64Convert: Codeunit "Base64 Convert";
        Regex: Codeunit Regex;
    begin
        if not Regex.IsMatch(Value, '[^\x00-\x7F]') then
            exit(Value);

        exit('=?base64?' + Base64Convert.ToBase64(Value, TextEncoding::UTF8) + '?=');
    end;

    internal procedure CreateEntraApplication(Name: Text[100]; Description: Text[250]; ClientId: Guid)
    var
        MCPEntraApplication: Record "MCP Entra Application";
    begin
        MCPEntraApplication.Name := Name;
        MCPEntraApplication.Description := Description;
        MCPEntraApplication."Client ID" := ClientId;
        MCPEntraApplication.Insert();
    end;

    internal procedure DeleteEntraApplication(Name: Text[100])
    var
        MCPEntraApplication: Record "MCP Entra Application";
    begin
        if not MCPEntraApplication.Get(Name) then
            exit;

        MCPEntraApplication.Delete();
    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, '<Year4>-<Month,2>-<Day,2>'));
        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
            Error(ConfigurationNotFoundErr);

        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('apiVersion', MCPConfigurationTool."API Version");
                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 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));

        exit(true);
    end;

    internal procedure ImportConfiguration(var InStream: InStream; NewName: Text[100]; NewDescription: Text[250]): Guid
    var
        MCPConfiguration: Record "MCP Configuration";
        ConfigJson: JsonObject;
        ToolsArray: JsonArray;
        ToolToken: JsonToken;
        InputText: Text;
    begin
        InStream.ReadText(InputText);
        if not ConfigJson.ReadFrom(InputText) then
            exit;

        MCPConfiguration.Name := NewName;
        MCPConfiguration.Description := NewDescription;
        MCPConfiguration.Active := false;

        if ConfigJson.Contains('enableDynamicToolMode') then
            MCPConfiguration.EnableDynamicToolMode := ConfigJson.GetBoolean('enableDynamicToolMode');

        if ConfigJson.Contains('discoverReadOnlyObjects') then
            MCPConfiguration.DiscoverReadOnlyObjects := ConfigJson.GetBoolean('discoverReadOnlyObjects');

        if ConfigJson.Contains('allowProdChanges') then
            MCPConfiguration.AllowProdChanges := ConfigJson.GetBoolean('allowProdChanges');

        MCPConfiguration.Insert();
        LogConfigurationCreated(MCPConfiguration);

        if ConfigJson.Contains('tools') then begin
            ToolsArray := ConfigJson.GetArray('tools');
            foreach ToolToken in ToolsArray do
                ImportTool(MCPConfiguration.SystemId, ToolToken.AsObject());
        end;

        exit(MCPConfiguration.SystemId);
    end;

    local procedure ImportTool(ConfigId: Guid; ToolJson: JsonObject)
    var
        MCPConfigurationTool: Record "MCP Configuration Tool";
        ObjectTypeText: Text;
    begin
        MCPConfigurationTool.Init();
        MCPConfigurationTool.ID := ConfigId;

        if ToolJson.Contains('objectType') then begin
            ObjectTypeText := ToolJson.GetText('objectType');
            if ObjectTypeText = 'Page' then
                MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Page;
            if ObjectTypeText = 'Query' then
                MCPConfigurationTool."Object Type" := MCPConfigurationTool."Object Type"::Query;
        end;

        if ToolJson.Contains('objectId') then
            MCPConfigurationTool."Object ID" := ToolJson.GetInteger('objectId');

        if ToolJson.Contains('apiVersion') then
            MCPConfigurationTool."API Version" := CopyStr(ToolJson.GetText('apiVersion'), 1, MaxStrLen(MCPConfigurationTool."API Version"));

        if ToolJson.Contains('allowRead') then
            MCPConfigurationTool."Allow Read" := ToolJson.GetBoolean('allowRead');

        if ToolJson.Contains('allowCreate') then
            MCPConfigurationTool."Allow Create" := ToolJson.GetBoolean('allowCreate');

        if ToolJson.Contains('allowModify') then
            MCPConfigurationTool."Allow Modify" := ToolJson.GetBoolean('allowModify');

        if ToolJson.Contains('allowDelete') then
            MCPConfigurationTool."Allow Delete" := ToolJson.GetBoolean('allowDelete');

        if ToolJson.Contains('allowBoundActions') then
            MCPConfigurationTool."Allow Bound Actions" := ToolJson.GetBoolean('allowBoundActions');

        MCPConfigurationTool.Insert();
    end;
    #endregion

    #region Feedback
    internal procedure TriggerNoActiveConfigsFeedback()
    var
        Feedback: Codeunit "Microsoft User Feedback";
    begin
        if not Confirm(MCPServerFeedbackConfirmQst, true) then
            exit;

        Feedback.WithCustomQuestion(MCPServerFeedbackQst, MCPServerFeedbackQst).WithCustomQuestionType(Enum::FeedbackQuestionType::Text);
        Feedback.RequestDislikeFeedback('MCP Server', 'MCPServer', 'Model Context Protocol (MCP) Server');

        Session.LogMessage('0000RTR', NoActiveConfigsFeedbackTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', GetTelemetryCategory());
    end;

    internal procedure TriggerGeneralFeedback()
    var
        Feedback: Codeunit "Microsoft User Feedback";
    begin
        Feedback.RequestFeedback('MCP Server', 'MCPServer', 'Model Context Protocol (MCP) Server');

        Session.LogMessage('0000RTS', GeneralFeedbackTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', GetTelemetryCategory());
    end;

    internal procedure HasNoActiveConfigurations(): Boolean
    var
        MCPConfiguration: Record "MCP Configuration";
    begin
        MCPConfiguration.SetRange(Active, true);
        MCPConfiguration.SetFilter(Name, '<>%1', '');
        exit(MCPConfiguration.IsEmpty());
    end;
    #endregion Feedback

    #region Telemetry
    local procedure GetDimensions(MCPConfiguration: Record "MCP Configuration") Dimensions: Dictionary of [Text, Text]
    begin
        Dimensions.Add('Category', GetTelemetryCategory());
        Dimensions.Add('MCPConfigurationName', MCPConfiguration.Name);
        Dimensions.Add('Active', Format(MCPConfiguration.Active));
        Dimensions.Add('IsDesignatedDefault', Format(MCPConfiguration.Default));
        Dimensions.Add('UnblockEditTools', Format(MCPConfiguration.AllowProdChanges));
        Dimensions.Add('DynamicToolMode', Format(MCPConfiguration.EnableDynamicToolMode));
        Dimensions.Add('DiscoverReadOnlyObjects', Format(MCPConfiguration.DiscoverReadOnlyObjects));
    end;

    internal procedure GetTelemetryCategory(): Text[50]
    begin
        exit('MCP');
    end;

    internal procedure LogConfigurationCreated(MCPConfiguration: Record "MCP Configuration")
    begin
        Session.LogMessage('0000R0Q', MCPConfigurationCreatedLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, GetDimensions(MCPConfiguration));
        Session.LogAuditMessage(StrSubstNo(MCPConfigurationAuditCreatedLbl, MCPConfiguration.Name, UserSecurityId(), CompanyName()), SecurityOperationResult::Success, AuditCategory::ApplicationManagement, 3, 0);
    end;

    internal procedure LogConfigurationModified(MCPConfiguration: Record "MCP Configuration"; xMCPConfiguration: Record "MCP Configuration")
    var
        Dimensions: Dictionary of [Text, Text];
    begin
        Dimensions.Add('Category', GetTelemetryCategory());
        Dimensions.Add('MCPConfigurationName', MCPConfiguration.Name);
        if MCPConfiguration.Active <> xMCPConfiguration.Active then begin
            Dimensions.Add('OldActive', Format(xMCPConfiguration.Active));
            Dimensions.Add('NewActive', Format(MCPConfiguration.Active));
        end;
        if MCPConfiguration.AllowProdChanges <> xMCPConfiguration.AllowProdChanges then begin
            Dimensions.Add('OldUnblockEditTools', Format(xMCPConfiguration.AllowProdChanges));
            Dimensions.Add('NewUnblockEditTools', Format(MCPConfiguration.AllowProdChanges));
        end;
        if MCPConfiguration.EnableDynamicToolMode <> xMCPConfiguration.EnableDynamicToolMode then begin
            Dimensions.Add('OldDynamicToolMode', Format(xMCPConfiguration.EnableDynamicToolMode));
            Dimensions.Add('NewDynamicToolMode', Format(MCPConfiguration.EnableDynamicToolMode));
        end;
        if MCPConfiguration.DiscoverReadOnlyObjects <> xMCPConfiguration.DiscoverReadOnlyObjects then begin
            Dimensions.Add('OldDiscoverReadOnlyObjects', Format(xMCPConfiguration.DiscoverReadOnlyObjects));
            Dimensions.Add('NewDiscoverReadOnlyObjects', Format(MCPConfiguration.DiscoverReadOnlyObjects));
        end;
        Session.LogMessage('0000QE9', MCPConfigurationModifiedLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, Dimensions);
        Session.LogAuditMessage(StrSubstNo(MCPConfigurationAuditModifiedLbl, MCPConfiguration.Name, UserSecurityId(), CompanyName()), SecurityOperationResult::Success, AuditCategory::ApplicationManagement, 3, 0);
    end;

    internal procedure LogConfigurationDeleted(MCPConfiguration: Record "MCP Configuration")
    begin
        Session.LogMessage('0000QEB', MCPConfigurationDeletedLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, GetDimensions(MCPConfiguration));
        Session.LogAuditMessage(StrSubstNo(MCPConfigurationAuditDeletedLbl, MCPConfiguration.Name, UserSecurityId(), CompanyName()), SecurityOperationResult::Success, AuditCategory::ApplicationManagement, 3, 0);
    end;
    #endregion
}