diff --git a/.gitignore b/.gitignore index 5c211d8d82..a11d810d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ node_modules/ generated/ /docs/commandline .DS_Store +.vscode/mcp.json diff --git a/AzureMcp.sln b/AzureMcp.sln index 62c4a14fe1..3a33bfc631 100644 --- a/AzureMcp.sln +++ b/AzureMcp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 d18.0 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{FBF56CC3-7AE6-AD2D-3F14-7F97FD322CD6}" EndProject @@ -267,6 +267,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0131AD4F-393 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.Storage", "tools\Azure.Mcp.Tools.Storage\src\Azure.Mcp.Tools.Storage.csproj", "{DE1B4312-1A4F-4774-B7EB-B1EC77F80D5E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure.Mcp.Tools.StorageSync", "Azure.Mcp.Tools.StorageSync", "{85F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{95F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.StorageSync", "tools\Azure.Mcp.Tools.StorageSync\src\Azure.Mcp.Tools.StorageSync.csproj", "{A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure.Mcp.Tools.VirtualDesktop", "Azure.Mcp.Tools.VirtualDesktop", "{B28A9B67-1C09-C756-C02A-7AC1895F9584}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E38B6DEF-57A1-6CCA-498B-5697FF0B466C}" @@ -539,6 +545,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.Storage.Liv EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.Storage.UnitTests", "tools\Azure.Mcp.Tools.Storage\tests\Azure.Mcp.Tools.Storage.UnitTests\Azure.Mcp.Tools.Storage.UnitTests.csproj", "{F3F49C7E-9106-4FF7-A71D-442022D63F7B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.StorageSync.UnitTests", "tools\Azure.Mcp.Tools.StorageSync\tests\Azure.Mcp.Tools.StorageSync.UnitTests\Azure.Mcp.Tools.StorageSync.UnitTests.csproj", "{C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D38B6103-E564-8894-9748-4CF0C62984DB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.VirtualDesktop.LiveTests", "tools\Azure.Mcp.Tools.VirtualDesktop\tests\Azure.Mcp.Tools.VirtualDesktop.LiveTests\Azure.Mcp.Tools.VirtualDesktop.LiveTests.csproj", "{0A09784C-BB49-44E8-B07A-DA4EEEC1184E}" @@ -563,6 +573,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{319B94CD EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Server.UnitTests", "servers\Azure.Mcp.Server\tests\Azure.Mcp.Server.UnitTests\Azure.Mcp.Server.UnitTests.csproj", "{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.StorageSync.LiveTests", "tools\Azure.Mcp.Tools.StorageSync\tests\Azure.Mcp.Tools.StorageSync.LiveTests\Azure.Mcp.Tools.StorageSync.LiveTests.csproj", "{38FE6BAB-DAEF-2CF7-2752-379F9094C190}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1101,6 +1113,18 @@ Global {DE1B4312-1A4F-4774-B7EB-B1EC77F80D5E}.Release|x64.Build.0 = Release|Any CPU {DE1B4312-1A4F-4774-B7EB-B1EC77F80D5E}.Release|x86.ActiveCfg = Release|Any CPU {DE1B4312-1A4F-4774-B7EB-B1EC77F80D5E}.Release|x86.Build.0 = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x64.Build.0 = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x86.Build.0 = Debug|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|Any CPU.Build.0 = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x64.ActiveCfg = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x64.Build.0 = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x86.ActiveCfg = Release|Any CPU + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x86.Build.0 = Release|Any CPU {3156A400-78C7-410A-9B79-9CDFFD5B94E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3156A400-78C7-410A-9B79-9CDFFD5B94E3}.Debug|Any CPU.Build.0 = Debug|Any CPU {3156A400-78C7-410A-9B79-9CDFFD5B94E3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -2037,6 +2061,18 @@ Global {F3F49C7E-9106-4FF7-A71D-442022D63F7B}.Release|x64.Build.0 = Release|Any CPU {F3F49C7E-9106-4FF7-A71D-442022D63F7B}.Release|x86.ActiveCfg = Release|Any CPU {F3F49C7E-9106-4FF7-A71D-442022D63F7B}.Release|x86.Build.0 = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x64.Build.0 = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Debug|x86.Build.0 = Debug|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|Any CPU.Build.0 = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x64.ActiveCfg = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x64.Build.0 = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x86.ActiveCfg = Release|Any CPU + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B}.Release|x86.Build.0 = Release|Any CPU {0A09784C-BB49-44E8-B07A-DA4EEEC1184E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A09784C-BB49-44E8-B07A-DA4EEEC1184E}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A09784C-BB49-44E8-B07A-DA4EEEC1184E}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -2121,6 +2157,18 @@ Global {ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x64.Build.0 = Release|Any CPU {ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x86.ActiveCfg = Release|Any CPU {ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x86.Build.0 = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|x64.ActiveCfg = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|x64.Build.0 = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|x86.ActiveCfg = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Debug|x86.Build.0 = Debug|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|Any CPU.Build.0 = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|x64.ActiveCfg = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|x64.Build.0 = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|x86.ActiveCfg = Release|Any CPU + {38FE6BAB-DAEF-2CF7-2752-379F9094C190}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2255,6 +2303,9 @@ Global {ED9D3D4A-502F-41A4-BBCC-970E65472F33} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84} {0131AD4F-3934-F56E-5081-42129AD09143} = {ED9D3D4A-502F-41A4-BBCC-970E65472F33} {DE1B4312-1A4F-4774-B7EB-B1EC77F80D5E} = {0131AD4F-3934-F56E-5081-42129AD09143} + {85F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84} + {95F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} = {85F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} + {A5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} = {95F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} {B28A9B67-1C09-C756-C02A-7AC1895F9584} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84} {E38B6DEF-57A1-6CCA-498B-5697FF0B466C} = {B28A9B67-1C09-C756-C02A-7AC1895F9584} {3156A400-78C7-410A-9B79-9CDFFD5B94E3} = {E38B6DEF-57A1-6CCA-498B-5697FF0B466C} @@ -2392,6 +2443,8 @@ Global {E03D2171-C4AB-45A3-681D-A2A2EBBB122A} = {ED9D3D4A-502F-41A4-BBCC-970E65472F33} {9A72A0E3-091A-4C64-AE66-ADAA5B46B1E8} = {E03D2171-C4AB-45A3-681D-A2A2EBBB122A} {F3F49C7E-9106-4FF7-A71D-442022D63F7B} = {E03D2171-C4AB-45A3-681D-A2A2EBBB122A} + {B5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} = {85F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} + {C5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} = {B5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} {D38B6103-E564-8894-9748-4CF0C62984DB} = {B28A9B67-1C09-C756-C02A-7AC1895F9584} {0A09784C-BB49-44E8-B07A-DA4EEEC1184E} = {D38B6103-E564-8894-9748-4CF0C62984DB} {F5980D17-1A14-4DD9-82DF-6496E0C4B70D} = {D38B6103-E564-8894-9748-4CF0C62984DB} @@ -2403,6 +2456,7 @@ Global {BF0354AE-3748-A8DC-F79D-B21FDDEDDFAE} = {37B0CE47-14C8-F5BF-BDDD-13EEBE580A88} {319B94CD-694C-16E8-9E3A-9577B99158DD} = {F7E192D1-DE6C-42A2-B52F-02849D482450} {ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C} = {319B94CD-694C-16E8-9E3A-9577B99158DD} + {38FE6BAB-DAEF-2CF7-2752-379F9094C190} = {B5F9C8A1-2B3D-4E5F-6A7B-8C9D0E1F2A3B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {926577F9-9246-44E4-BCE9-25DB003F1C51} diff --git a/Directory.Build.props b/Directory.Build.props index c9bceecf58..6b0e5d9932 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -46,10 +46,6 @@ true - - $(RepoRoot)eng/dnx/.mcp/server.json - - true diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index 206917e460..0000000000 --- a/Directory.Build.targets +++ /dev/null @@ -1,12 +0,0 @@ - - - - <_ProjectReferences>@(ProjectReference->'"%(FullPath)"', ',%0D ') - <_ProjectReferencesJson>[%0D $([System.String]::Copy('$(_ProjectReferences)').Replace('\','\\'))%0D] - $(MSBuildThisFileDirectory).work/project-references.json - - - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b1a3b4f39..751a1b4c0c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -56,6 +56,7 @@ + diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs index 47fc2db6e8..39cd160cf4 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; using Azure.Mcp.Core.Areas.Server.Commands.Discovery; @@ -265,6 +264,13 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser .BindConfiguration(string.Empty) .Configure>((options, rootConfiguration, serviceStartOptions) => { + // This environment variable can be used to disable telemetry collection entirely. This takes precedence + // over any other settings. + var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true); + var transport = serviceStartOptions.Value.Transport; + var isStdioTransport = string.IsNullOrEmpty(transport) + || string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase); + // Assembly.GetEntryAssembly is used to retrieve the version of the server application as that is // the assembly that will run the tool calls. var entryAssembly = Assembly.GetEntryAssembly(); @@ -275,22 +281,6 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser options.Version = AssemblyHelper.GetAssemblyVersion(entryAssembly); - // Disable telemetry when support logging is enabled to prevent sensitive data from being sent - // to telemetry endpoints. Support logging captures debug-level information that may contain - // sensitive data, so we disable all telemetry as a safety measure. - if (!string.IsNullOrWhiteSpace(serviceStartOptions.Value.SupportLoggingFolder)) - { - options.IsTelemetryEnabled = false; - return; - } - - // This environment variable can be used to disable telemetry collection entirely. This takes precedence - // over any other settings. - var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true); - var transport = serviceStartOptions.Value.Transport; - var isStdioTransport = string.IsNullOrEmpty(transport) - || string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase); - // if transport is not set (default to stdio) or is set to stdio, enable telemetry // telemetry is disabled for HTTP transport options.IsTelemetryEnabled = collectTelemetry && isStdioTransport; diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs index 1b93932300..7ecb2bf4fc 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs @@ -7,7 +7,6 @@ using Azure.Mcp.Core.Areas.Server.Models; using Azure.Mcp.Core.Areas.Server.Options; using Azure.Mcp.Core.Helpers; -using Azure.Mcp.Core.Logging; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Authentication; using Azure.Mcp.Core.Services.Caching; @@ -83,7 +82,6 @@ protected override void RegisterOptions(Command command) command.Options.Add(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth); command.Options.Add(ServiceOptionDefinitions.InsecureDisableElicitation); command.Options.Add(ServiceOptionDefinitions.OutgoingAuthStrategy); - command.Options.Add(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir); command.Validators.Add(commandResult => { string transport = ResolveTransport(commandResult); @@ -95,42 +93,9 @@ protected override void RegisterOptions(Command command) commandResult.GetValueOrDefault(ServiceOptionDefinitions.Tool.Name), commandResult); ValidateOutgoingAuthStrategy(commandResult); - ValidateSupportLoggingFolder(commandResult); }); } - /// - /// Validates that the support logging folder path is valid when specified. - /// - /// Command result to update on failure. - private static void ValidateSupportLoggingFolder(CommandResult commandResult) - { - string? folderPath = commandResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name); - - if (folderPath is null) - { - return; // Option not specified, nothing to validate - } - - // Validate the folder path is not empty or whitespace - if (string.IsNullOrWhiteSpace(folderPath)) - { - commandResult.AddError("The --dangerously-write-support-logs-to-dir option requires a valid folder path."); - return; - } - - // Validate the folder path is actually a valid path format - try - { - // GetFullPath will throw for invalid path characters and other path format issues - _ = Path.GetFullPath(folderPath); - } - catch (Exception ex) when (ex is ArgumentException or PathTooLongException or NotSupportedException) - { - commandResult.AddError($"The --dangerously-write-support-logs-to-dir option contains an invalid folder path '{folderPath}': {ex.Message}"); - } - } - /// /// Binds the parsed command line arguments to the ServiceStartOptions object. /// @@ -159,8 +124,7 @@ protected override ServiceStartOptions BindOptions(ParseResult parseResult) Debug = parseResult.GetValueOrDefault(ServiceOptionDefinitions.Debug.Name), DangerouslyDisableHttpIncomingAuth = parseResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth.Name), InsecureDisableElicitation = parseResult.GetValueOrDefault(ServiceOptionDefinitions.InsecureDisableElicitation.Name), - OutgoingAuthStrategy = outgoingAuthStrategy, - SupportLoggingFolder = parseResult.GetValueOrDefault(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name) + OutgoingAuthStrategy = outgoingAuthStrategy }; return options; } @@ -232,26 +196,6 @@ internal static void LogStartTelemetry(ITelemetryService telemetryService, Servi } } - /// - /// Configures support logging when a support logging folder is specified. - /// This enables debug-level logging for troubleshooting and support purposes. - /// - /// The logging builder to configure. - /// The server configuration options. - private static void ConfigureSupportLogging(ILoggingBuilder logging, ServiceStartOptions options) - { - if (options.SupportLoggingFolder is null) - { - return; - } - - // Set minimum log level to Debug when support logging is enabled - logging.SetMinimumLevel(LogLevel.Debug); - - // Add file logging to the specified folder - logging.AddSupportFileLogging(options.SupportLoggingFolder); - } - /// /// Validates if the provided mode is a valid mode type. /// @@ -424,8 +368,6 @@ private IHost CreateStdioHost(ServiceStartOptions serverOptions) logging.AddFilter("Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider", LogLevel.Debug); logging.SetMinimumLevel(LogLevel.Debug); } - - ConfigureSupportLogging(logging, serverOptions); }) .ConfigureServices(services => { @@ -452,7 +394,6 @@ private IHost CreateHttpHost(ServiceStartOptions serverOptions) builder.Logging.ConfigureOpenTelemetryLogger(); builder.Logging.AddEventSourceLogger(); builder.Logging.AddConsole(); - ConfigureSupportLogging(builder.Logging, serverOptions); IServiceCollection services = builder.Services; @@ -630,7 +571,6 @@ private IHost CreateIncomingAuthDisabledHttpHost(ServiceStartOptions serverOptio builder.Logging.ConfigureOpenTelemetryLogger(); builder.Logging.AddEventSourceLogger(); builder.Logging.AddConsole(); - ConfigureSupportLogging(builder.Logging, serverOptions); IServiceCollection services = builder.Services; @@ -831,14 +771,6 @@ private static WebApplication UseHttpsRedirectionIfEnabled(WebApplication app) return null; } - // Disable telemetry when support logging is enabled to prevent sensitive data from being sent - // to telemetry endpoints. Support logging captures debug-level information that may contain - // sensitive data, so we disable all telemetry as a safety measure. - if (!string.IsNullOrWhiteSpace(options.SupportLoggingFolder)) - { - return null; - } - string? collectTelemetry = Environment.GetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY"); bool isTelemetryEnabled = string.IsNullOrWhiteSpace(collectTelemetry) || (bool.TryParse(collectTelemetry, out bool shouldCollectTelemetry) && shouldCollectTelemetry); diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs index 0cb4a951e1..4b554e79e9 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs @@ -14,7 +14,6 @@ public static class ServiceOptionDefinitions public const string DangerouslyDisableHttpIncomingAuthName = "dangerously-disable-http-incoming-auth"; public const string InsecureDisableElicitationName = "insecure-disable-elicitation"; public const string OutgoingAuthStrategyName = "outgoing-auth-strategy"; - public const string DangerouslyWriteSupportLogsToDirName = "dangerously-write-support-logs-to-dir"; public static readonly Option Transport = new($"--{TransportName}") { @@ -92,12 +91,4 @@ public static class ServiceOptionDefinitions Description = "Outgoing authentication strategy for Azure service requests. Valid values: NotSet, UseHostingEnvironmentIdentity, UseOnBehalfOf.", DefaultValueFactory = _ => Options.OutgoingAuthStrategy.NotSet }; - - public static readonly Option DangerouslyWriteSupportLogsToDir = new( - $"--{DangerouslyWriteSupportLogsToDirName}") - { - Required = false, - Description = "Dangerously enables detailed debug-level logging for support and troubleshooting purposes. Specify a folder path where log files will be automatically created with timestamp-based filenames (e.g., azmcp_20251202_143052.log). This may include sensitive information in logs. Use with extreme caution and only when requested by support.", - DefaultValueFactory = _ => null - }; } diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs index dd3c6256f3..8023c60605 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs @@ -72,13 +72,4 @@ public class ServiceStartOptions /// [JsonPropertyName("outgoingAuthStrategy")] public OutgoingAuthStrategy OutgoingAuthStrategy { get; set; } = OutgoingAuthStrategy.NotSet; - - /// - /// Gets or sets the folder path for support logging. - /// When specified, detailed debug-level logging is enabled and logs are written to - /// automatically generated files in this folder with timestamp-based filenames. - /// Warning: This may include sensitive information in logs. - /// - [JsonPropertyName("supportLoggingFolder")] - public string? SupportLoggingFolder { get; set; } = null; } diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json b/core/Azure.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json index db35c9f20a..46b10d8b0d 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json +++ b/core/Azure.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json @@ -2196,6 +2196,600 @@ "mappedToolList": [ "speech_tts_synthesize" ] + }, + { + "name": "get_azure_storage_sync_services", + "description": "Get and list Azure File Sync Storage Sync Services. A Storage Sync Service is the top-level resource for Azure File Sync deployments.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_service_get" + ] + }, + { + "name": "get_azure_storage_sync_groups", + "description": "Get and list Azure File Sync Sync Groups. A Sync Group defines the synchronization topology for a set of cloud and server endpoints.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_syncgroup_get" + ] + }, + { + "name": "get_azure_storage_sync_cloud_endpoints", + "description": "Get and list Azure File Sync Cloud Endpoints. A Cloud Endpoint represents an Azure file share being synchronized.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_cloudendpoint_get" + ] + }, + { + "name": "get_azure_storage_sync_registered_servers", + "description": "Get and list Azure File Sync Registered Servers. A Registered Server represents a server that has been registered with a Storage Sync Service.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_registeredserver_get" + ] + }, + { + "name": "get_azure_storage_sync_server_endpoints", + "description": "Get and list Azure File Sync Server Endpoints. A Server Endpoint represents a specific folder on a registered server that is being synchronized.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_serverendpoint_get" + ] + }, + { + "name": "create_azure_storage_sync_services", + "description": "Create new Azure File Sync Storage Sync Services. A Storage Sync Service is the top-level resource for Azure File Sync deployments.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": false, + "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_service_create" + ] + }, + { + "name": "create_azure_storage_sync_groups", + "description": "Create new Azure File Sync Sync Groups. A Sync Group defines the synchronization topology for a set of cloud and server endpoints.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": false, + "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_syncgroup_create" + ] + }, + { + "name": "create_azure_storage_sync_cloud_endpoints", + "description": "Create new Azure File Sync Cloud Endpoints. A Cloud Endpoint represents an Azure file share being synchronized.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": false, + "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_cloudendpoint_create" + ] + }, + { + "name": "create_azure_storage_sync_server_endpoints", + "description": "Create new Azure File Sync Server Endpoints. A Server Endpoint represents a specific folder on a registered server that is being synchronized.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": false, + "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_serverendpoint_create" + ] + }, + { + "name": "update_azure_storage_sync_services", + "description": "Update Azure File Sync Storage Sync Services configurations and settings.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_service_update" + ] + }, + { + "name": "update_azure_storage_sync_registered_servers", + "description": "Update Azure File Sync Registered Server configurations and settings.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_registeredserver_update" + ] + }, + { + "name": "update_azure_storage_sync_server_endpoints", + "description": "Update Azure File Sync Server Endpoint configurations and settings.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_serverendpoint_update" + ] + }, + { + "name": "delete_azure_storage_sync_services", + "description": "Delete Azure File Sync Storage Sync Services. Warning: This permanently removes the Storage Sync Service and all its associated sync groups and endpoints.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_service_delete" + ] + }, + { + "name": "delete_azure_storage_sync_groups", + "description": "Delete Azure File Sync Sync Groups. Warning: This permanently removes the Sync Group and all its associated endpoints.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_syncgroup_delete" + ] + }, + { + "name": "delete_azure_storage_sync_cloud_endpoints", + "description": "Delete Azure File Sync Cloud Endpoints. Warning: This stops synchronization for the associated Azure file share.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_cloudendpoint_delete" + ] + }, + { + "name": "delete_azure_storage_sync_registered_servers", + "description": "Unregister Azure File Sync Registered Servers from a Storage Sync Service. Warning: This removes the server registration and stops synchronization for all associated server endpoints.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_registeredserver_unregister" + ] + }, + { + "name": "delete_azure_storage_sync_server_endpoints", + "description": "Delete Azure File Sync Server Endpoints. Warning: This stops synchronization for the specified server folder.", + "toolMetadata": { + "destructive": { + "value": true, + "description": "This tool may delete or modify existing resources in its environment." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_serverendpoint_delete" + ] + }, + { + "name": "trigger_azure_storage_sync_cloud_endpoint_change_detection", + "description": "Trigger change detection on Azure File Sync Cloud Endpoints to force detection of changes in the Azure file share.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": false, + "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "storagesync_cloudendpoint_changedetection" + ] } ] -} \ No newline at end of file +} diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs index c7f7527eb4..32e4e00688 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ServiceCollectionExtensionsSerializedTests.cs @@ -107,78 +107,4 @@ public void InitializeConfigurationAndOptions_Stdio() Assert.False(actual.IsTelemetryEnabled); } - - /// - /// When SupportLoggingFolder is set, telemetry should be automatically disabled - /// to prevent sensitive debug information from being sent to telemetry endpoints. - /// - [Fact] - public void InitializeConfigurationAndOptions_WithSupportLoggingFolder_DisablesTelemetry() - { - // Arrange - var serviceStartOptions = new ServiceStartOptions - { - SupportLoggingFolder = "/tmp/logs" - }; - var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); - - // Act - Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", null); - ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); - var provider = services.BuildServiceProvider(); - - // Assert - var options = provider.GetRequiredService>(); - Assert.False(options.Value.IsTelemetryEnabled, "Telemetry should be disabled when support logging folder is set"); - } - - /// - /// SupportLoggingFolder takes precedence over AZURE_MCP_COLLECT_TELEMETRY=true. - /// When support logging is enabled, telemetry must be disabled regardless of env var. - /// - [Fact] - public void InitializeConfigurationAndOptions_WithSupportLoggingFolderAndEnvVarTrue_StillDisablesTelemetry() - { - // Arrange - var serviceStartOptions = new ServiceStartOptions - { - SupportLoggingFolder = "/tmp/logs" - }; - var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); - - // Act - Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", "true"); - ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); - var provider = services.BuildServiceProvider(); - - // Assert - var options = provider.GetRequiredService>(); - Assert.False(options.Value.IsTelemetryEnabled, "Telemetry should be disabled when support logging folder is set, regardless of environment variable"); - } - - /// - /// Empty or whitespace SupportLoggingFolder should not disable telemetry. - /// - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void InitializeConfigurationAndOptions_WithEmptyOrWhitespaceSupportLoggingFolder_EnablesTelemetry(string? folderPath) - { - // Arrange - var serviceStartOptions = new ServiceStartOptions - { - SupportLoggingFolder = folderPath - }; - var services = SetupBaseServices().AddSingleton(Options.Create(serviceStartOptions)); - - // Act - Environment.SetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY", null); - ServiceCollectionExtensions.InitializeConfigurationAndOptions(services); - var provider = services.BuildServiceProvider(); - - // Assert - var options = provider.GetRequiredService>(); - Assert.True(options.Value.IsTelemetryEnabled, $"Telemetry should be enabled when support logging folder is '{folderPath}'"); - } } diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs index 4465af5a86..a90169df9f 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/ServiceStartCommandTests.cs @@ -279,62 +279,6 @@ public void BindOptions_WithDefaults_ReturnsDefaultValues() Assert.False(options.Debug); Assert.False(options.DangerouslyDisableHttpIncomingAuth); Assert.False(options.InsecureDisableElicitation); - Assert.Null(options.SupportLoggingFolder); - } - - [Theory] - [InlineData("/tmp/logs")] - [InlineData("C:\\logs")] - [InlineData(null)] - public void DangerouslyWriteSupportLogsToDirOption_ParsesCorrectly(string? expectedFolder) - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging(expectedFolder); - - // Act - var actualValue = parseResult.GetValue(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir); - - // Assert - Assert.Equal(expectedFolder, actualValue); - } - - [Fact] - public void BindOptions_WithSupportLoggingFolder_ReturnsCorrectlyConfiguredOptions() - { - // Arrange - var logFolder = "/tmp/mcp-support-logs"; - var parseResult = CreateParseResultWithSupportLogging(logFolder); - - // Act - var options = GetBoundOptions(parseResult); - - // Assert - Assert.Equal(logFolder, options.SupportLoggingFolder); - } - - [Fact] - public void BindOptions_WithoutSupportLoggingFolder_ReturnsCorrectlyConfiguredOptions() - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging(null); - - // Act - var options = GetBoundOptions(parseResult); - - // Assert - Assert.Null(options.SupportLoggingFolder); - } - - [Fact] - public void AllOptionsRegistered_IncludesSupportLoggingToFolder() - { - // Arrange & Act - var command = _command.GetCommand(); - - // Assert - var hasSupportLoggingFolderOption = command.Options.Any(o => - o.Name == ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name); - Assert.True(hasSupportLoggingFolderOption, "DangerouslyWriteSupportLogsToDir option should be registered"); } [Fact] @@ -397,67 +341,6 @@ public void Validate_WithNamespaceAndTool_ReturnsInvalidResult() Assert.Contains("--namespace and --tool options cannot be used together", string.Join('\n', result.Errors)); } - [Fact] - public void Validate_WithSupportLoggingFolderWhitespace_ReturnsInvalidResult() - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging(" "); - var commandResult = parseResult.CommandResult; - - // Act - var result = _command.Validate(commandResult, null); - - // Assert - Assert.False(result.IsValid); - Assert.Contains("The --dangerously-write-support-logs-to-dir option requires a valid folder path", string.Join('\n', result.Errors)); - } - - [Fact] - public void Validate_WithValidSupportLoggingFolder_ReturnsValidResult() - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging("/tmp/mcp-support-logs"); - var commandResult = parseResult.CommandResult; - - // Act - var result = _command.Validate(commandResult, null); - - // Assert - Assert.True(result.IsValid); - Assert.Empty(result.Errors); - } - - [Fact] - public void Validate_WithoutSupportLoggingFolder_ReturnsValidResult() - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging(null); - var commandResult = parseResult.CommandResult; - - // Act - var result = _command.Validate(commandResult, null); - - // Assert - Assert.True(result.IsValid); - Assert.Empty(result.Errors); - } - - [Fact] - public async Task ExecuteAsync_WithSupportLoggingFolderWhitespace_ReturnsValidationError() - { - // Arrange - var parseResult = CreateParseResultWithSupportLogging(" "); - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var context = new CommandContext(serviceProvider); - - // Act - var response = await _command.ExecuteAsync(context, parseResult, TestContext.Current.CancellationToken); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.Status); - Assert.Contains("The --dangerously-write-support-logs-to-dir option requires a valid folder path", response.Message); - } - [Fact] public async Task ExecuteAsync_WithNamespaceAndTool_ReturnsValidationError() { @@ -837,22 +720,6 @@ private ParseResult CreateParseResultWithMinimalOptions() return _command.GetCommand().Parse([]); } - private ParseResult CreateParseResultWithSupportLogging(string? folderPath) - { - var args = new List - { - "--transport", "stdio" - }; - - if (folderPath is not null) - { - args.Add("--dangerously-write-support-logs-to-dir"); - args.Add(folderPath); - } - - return _command.GetCommand().Parse([.. args]); - } - private ParseResult CreateParseResultWithToolsAndMode(string[] tools, string mode) { var args = new List diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Services/Telemetry/TelemetryServiceTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Services/Telemetry/TelemetryServiceTests.cs index c63f559141..5187f854cd 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Services/Telemetry/TelemetryServiceTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Services/Telemetry/TelemetryServiceTests.cs @@ -36,7 +36,6 @@ public TelemetryServiceTests() _mockOptions.Value.Returns(_testConfiguration); _mockServiceOptions = Substitute.For>(); - _mockServiceOptions.Value.Returns(new ServiceStartOptions()); _mockInformationProvider = Substitute.For(); _mockInformationProvider.GetMacAddressHash().Returns(Task.FromResult(TestMacAddressHash)); diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/CommandTestsBase.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/CommandTestsBase.cs index dcc683ee0e..e24dbcba90 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/CommandTestsBase.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/CommandTestsBase.cs @@ -45,8 +45,8 @@ public virtual async ValueTask InitializeAsync() ResourceBaseName = "Sanitized", SubscriptionName = "Sanitized", TenantName = "Sanitized", - ResourceGroupName = "Sanitized", - TestMode = TestMode.Playback + TestMode = TestMode.Playback, + ResourceGroupName = "Sanitized" }; protected virtual async ValueTask LoadSettingsAsync() diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/IRecordingPathResolver.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/IRecordingPathResolver.cs new file mode 100644 index 0000000000..b513f2d7b3 --- /dev/null +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/IRecordingPathResolver.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Azure.Mcp.Tests.Client.Helpers; + +/// +/// Abstraction for resolving recording asset paths and session directories. +/// Enables tests to substitute custom paths when exercising record/playback infrastructure. +/// +public interface IRecordingPathResolver +{ + string RepositoryRoot { get; } + + string GetSessionDirectory(Type testType, string? variantSuffix = null); + + string GetAssetsJson(Type testType); +} diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/LiveTestSettingsFixture.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/LiveTestSettingsFixture.cs index 24a33f0a1c..98c4bc526d 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/LiveTestSettingsFixture.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/Helpers/LiveTestSettingsFixture.cs @@ -10,23 +10,10 @@ namespace Azure.Mcp.Tests.Client.Helpers { public class LiveTestSettingsFixture : IAsyncLifetime { - private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() - { - PropertyNameCaseInsensitive = true, - Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } - }; - public LiveTestSettings Settings { get; private set; } = new(); public virtual async ValueTask InitializeAsync() { - // If the TestMode is Playback, skip loading other settings. Skipping will match behaviors in CI when resources aren't deployed, - // as content is recorded. - if (Settings.TestMode == Tests.Helpers.TestMode.Playback) - { - return; - } - var testSettingsFileName = ".testsettings.json"; var directory = Path.GetDirectoryName(typeof(LiveTestSettingsFixture).Assembly.Location); @@ -37,7 +24,13 @@ public virtual async ValueTask InitializeAsync() { var content = await File.ReadAllTextAsync(testSettingsFilePath); - Settings = JsonSerializer.Deserialize(content, s_jsonSerializerOptions) + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } + }; + + Settings = JsonSerializer.Deserialize(content, options) ?? throw new Exception("Unable to deserialize live test settings"); foreach (var (key, value) in Settings.EnvironmentVariables) diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/RecordedCommandTestsBase.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/RecordedCommandTestsBase.cs index 0da52b32e5..25ff3a60d6 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/RecordedCommandTestsBase.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Tests/Client/RecordedCommandTestsBase.cs @@ -242,9 +242,10 @@ public async Task StartProxyAsync(TestProxyFixture fixture) private void PopulateDefaultSanitizers() { - // Registering a few common sanitizers for values that we know will be universally present and cleaned up if (EnableDefaultSanitizerAdditions) { + // Sanitize out the resource basename by default! + // This implies that tests shouldn't use this baseresourcename as part of their validation logic, as sanitization will replace it with "Sanitized" and cause confusion. GeneralRegexSanitizers.Add(new GeneralRegexSanitizer(new GeneralRegexSanitizerBody() { Regex = Settings.ResourceBaseName, diff --git a/eng/pipelines/templates/jobs/integration.yml b/eng/pipelines/templates/jobs/integration.yml index 5761c2d9c6..f920240399 100644 --- a/eng/pipelines/templates/jobs/integration.yml +++ b/eng/pipelines/templates/jobs/integration.yml @@ -10,11 +10,6 @@ jobs: - job: PublishToDev displayName: Publish packages to dev feeds condition: and(succeeded(), ne(variables['Skip.PublishPackage'], 'true')) - pool: - # On linux, we'd need mono to run nuget, so just use windows - name: $(WINDOWSPOOL) - image: $(WINDOWSVMIMAGE) - os: windows steps: - checkout: self @@ -49,7 +44,6 @@ jobs: - task: PowerShell@2 displayName: 'Attach usage instructions to build summary' inputs: - pwsh: true targetType: 'filePath' filePath: 'eng/scripts/Get-PackageUsageText.ps1' arguments: > diff --git a/eng/pipelines/templates/jobs/release.yml b/eng/pipelines/templates/jobs/release.yml index 05eb523eca..5fbd133974 100644 --- a/eng/pipelines/templates/jobs/release.yml +++ b/eng/pipelines/templates/jobs/release.yml @@ -90,7 +90,6 @@ jobs: - task: PowerShell@2 displayName: Increment version inputs: - pwsh: true targetType: filePath filePath: $(Build.SourcesDirectory)/eng/scripts/Update-Version.ps1 arguments: > diff --git a/eng/pipelines/templates/variables/image.yml b/eng/pipelines/templates/variables/image.yml index c9dd2252b5..d6ace0e2d2 100644 --- a/eng/pipelines/templates/variables/image.yml +++ b/eng/pipelines/templates/variables/image.yml @@ -9,7 +9,7 @@ variables: value: Azure Pipelines - name: LINUXVMIMAGE - value: ubuntu-24.04 + value: ubuntu-22.04 - name: WINDOWSVMIMAGE value: windows-2022 - name: MACVMIMAGE diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 903b306b1c..48bef66203 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -2,10 +2,17 @@ The Azure MCP Server updates automatically by default whenever a new release comes out 🚀. We ship updates twice a week on Tuesdays and Thursdays 😊 -## 2.0.0-beta.8 (Unreleased) +## 2.0.0-beta.9 (Unreleased) ### Features Added +- Added Azure Storage Sync (StorageSync) module with 24 commands for managing cloud synchronization of file shares: + - **StorageSyncService** commands (5): Create, Delete, Get, List, Update + - **RegisteredServer** commands (5): Get, List, Register, Unregister, Update + - **SyncGroup** commands (4): Create, Delete, Get, List + - **CloudEndpoint** commands (5): Create, Delete, Get, List, TriggerChangeDetection + - **ServerEndpoint** commands (5): Create, Delete, Get, List, Update + - Added support logging capability with `--dangerously-write-support-logs-to-dir` option for troubleshooting and support scenarios. When enabled, detailed debug-level logs are written to automatically-generated timestamped log files (e.g., `azmcp_20251202_143052.log`) in the specified folder. All telemetry is automatically disabled when support logging is enabled to prevent sensitive debug information from being sent to telemetry endpoints. - Replace hard-coded strings for Azure.Mcp.Server with ones from IConfiguration. [[#1269](https://github.com/microsoft/mcp/pull/1269)] @@ -18,7 +25,6 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Other Changes - Switched to the new `Azure.Monitor.Query.Logs` package to query logs from Azure Monitor. [[#1309](https://github.com/microsoft/mcp/pull/1309)] -- Move Azure AI Best Practices tool into Best Practice namespace [[#1323](https://github.com/microsoft/mcp/pull/1323)] #### Dependency updates @@ -26,6 +32,12 @@ The Azure MCP Server updates automatically by default whenever a new release com - Updated `Microsoft.Azure.Mcp.AzTypes.Internal.Compact` from `0.2.802` to `0.2.804`. [[#1348](https://github.com/microsoft/mcp/pull/1348)] +## 2.0.0-beta.8 (2025-12-11) + +### Bugs Fixed + +- Fixed an issue where the AI Best Practices tool would get called instead of the Best Practices tool. [[#1323](https://github.com/microsoft/mcp/pull/1323)] + ## 2.0.0-beta.7 (2025-11-25) ### Bugs Fixed diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index aed6de218d..e66f8ea358 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -106,7 +106,7 @@ All Azure MCP tools in a single server. The Azure MCP Server implements the [MCP Install Azure MCP Server using either an IDE extension or package manager. Choose one method below. -> [!IMPORTANT] +> [!IMPORTANT] > Authenticate to Azure before running the Azure MCP server. See the [Authentication guide](https://github.com/microsoft/mcp/blob/main/docs/Authentication.md) for authentication methods and instructions. ## IDE @@ -127,7 +127,7 @@ Compatible with both the [Stable](https://code.visualstudio.com/download) and [I - If Visual Studio 2026 is already installed, open the **Visual Studio Installer** and select the **Modify** button, which displays the available workloads. 1. On the Workloads tab, select **Azure and AI development** and select **GitHub Copilot**. 1. Click **install while downloading** to complete the installation. - + For more information, visit [Install GitHub Copilot for Azure in Visual Studio 2026](https://aka.ms/ghcp4a/vs2026) ### Visual Studio 2022 @@ -239,7 +239,7 @@ Install the .NET Tool: [Azure.Mcp](https://www.nuget.org/packages/Azure.Mcp). ```bash dotnet tool install Azure.Mcp ``` -or +or ```bash dotnet tool install Azure.Mcp --version ``` @@ -382,7 +382,7 @@ Microsoft Foundry and Microsoft Copilot Studio require remote MCP server endpoin * Create Microsoft Foundry agent threads * List Microsoft Foundry agent threads * Get messages of a Microsoft Foundry thread - + ### 🔎 Azure AI Search * "What indexes do I have in my Azure AI Search service 'mysvc'?" @@ -534,7 +534,7 @@ Microsoft Foundry and Microsoft Copilot Studio require remote MCP server endpoin ## Complete List of Supported Azure Services -The Azure MCP Server provides tools for interacting with **40+ Azure service areas**: +The Azure MCP Server provides tools for interacting with **41+ Azure service areas**: - 🧮 **Microsoft Foundry** - AI model management, AI model deployment, and knowledge index management - 🔎 **Azure AI Search** - Search engine/vector database operations @@ -572,6 +572,7 @@ The Azure MCP Server provides tools for interacting with **40+ Azure service are - 🗄️ **Azure SQL Elastic Pool** - Database resource sharing - 🗄️ **Azure SQL Server** - Server administration - 💾 **Azure Storage** - Blob storage +- 🔄 **Azure Storage Sync** - Azure File Sync management operations - 📋 **Azure Subscription** - Subscription management - 🏗️ **Azure Terraform Best Practices** - Infrastructure as code guidance - 🖥️ **Azure Virtual Desktop** - Virtual desktop infrastructure diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index 69b07ffc70..9278314e47 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -1885,6 +1885,173 @@ azmcp storage blob upload --subscription \ --local-file-path ``` +### Azure Storage Sync Operations + +#### Storage Sync Service + +```bash +# Create a new Storage Sync Service for cloud file share synchronization +azmcp storagesync service create --subscription \ + --resource-group \ + --name \ + --location + +# Delete a Storage Sync Service (idempotent – succeeds even if the service does not exist) +azmcp storagesync service delete --subscription \ + --resource-group \ + --name + +# Get a specific Storage Sync Service or list all services. If --name is provided, returns a specific service; otherwise, lists all services. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync service get --subscription \ + [--resource-group ] \ + [--name ] + +# Update an existing Storage Sync Service configuration +azmcp storagesync service update --subscription \ + --resource-group \ + --name \ + [--tags ] +``` + +#### Sync Group + +```bash +# Create a new Sync Group within a Storage Sync Service +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync syncgroup create --subscription \ + --resource-group \ + --service \ + --name + +# Delete a Sync Group (idempotent – succeeds even if the group does not exist) +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync syncgroup delete --subscription \ + --resource-group \ + --service \ + --name + +# Get a specific Sync Group or list all sync groups. If --name is provided, returns a specific sync group; otherwise, lists all sync groups in the Storage Sync Service. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync syncgroup get --subscription \ + --resource-group \ + --service \ + [--name ] +``` + +#### Cloud Endpoint + +```bash +# Create a new Cloud Endpoint within a Sync Group +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync cloudendpoint create --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --name \ + --storage-account \ + --share + +# Delete a Cloud Endpoint (idempotent – succeeds even if the endpoint does not exist) +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync cloudendpoint delete --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --name + +# Get a specific Cloud Endpoint or list all cloud endpoints. If --name is provided, returns a specific cloud endpoint; otherwise, lists all cloud endpoints in the Sync Group. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync cloudendpoint get --subscription \ + --resource-group \ + --service \ + --syncgroup \ + [--name ] + +# Trigger change detection on a Cloud Endpoint +azmcp storagesync cloudendpoint changedetection --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --name \ + [--directory-path ] +``` + +#### Registered Server + +```bash +# Get a specific Registered Server or list all registered servers. If --server is provided, returns a specific registered server; otherwise, lists all registered servers in the Storage Sync Service. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync registeredserver get --subscription \ + --resource-group \ + --service \ + [--server ] + +# Register a new server with a Storage Sync Service +# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync registeredserver register --subscription \ + --resource-group \ + --service \ + --server \ + --server-id + +# Unregister a server from a Storage Sync Service +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync registeredserver unregister --subscription \ + --resource-group \ + --service \ + --server + +# Update a Registered Server configuration +# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync registeredserver update --subscription \ + --resource-group \ + --service \ + --server \ + [--certificate ] +``` + +#### Server Endpoint + +```bash +# Create a new Server Endpoint within a Sync Group +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync serverendpoint create --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --server \ + --name \ + --server-local-path + +# Delete a Server Endpoint (idempotent – succeeds even if the endpoint does not exist) +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync serverendpoint delete --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --name + +# Get a specific Server Endpoint or list all server endpoints. If --name is provided, returns a specific server endpoint; otherwise, lists all server endpoints in the Sync Group. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync serverendpoint get --subscription \ + --resource-group \ + --service \ + --syncgroup \ + [--name ] + +# Update a Server Endpoint configuration +# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp storagesync serverendpoint update --subscription \ + --resource-group \ + --service \ + --syncgroup \ + --name \ + [--cloud-tiering ] \ + [--tiering-policy-days ] \ + [--tiering-policy-volume-free-percent ] +``` + ### Azure Subscription Management ```bash diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index c059e67977..b48e803194 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -625,6 +625,40 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | storage_blob_get | Show me the blobs in the blob container in the storage account | | storage_blob_upload | Upload file to storage blob in container in storage account | +## Azure Storage Sync + +| Tool Name | Test Prompt | +|:----------|:----------| +| storagesync_service_create | Create a new Storage Sync Service named in resource group at location | +| storagesync_service_delete | Delete the Storage Sync Service from resource group | +| storagesync_service_get | Get the details of Storage Sync Service in resource group | +| storagesync_service_get | List all Storage Sync Services in resource group | +| storagesync_service_list | List all Storage Sync Services in my subscription | +| storagesync_service_list | Show me all Storage Sync Services in resource group | +| storagesync_service_update | Update Storage Sync Service with new tags | +| storagesync_registeredserver_get | Get the details of registered server in service | +| storagesync_registeredserver_get | List all registered servers in service | +| storagesync_registeredserver_list | List all registered servers in service in resource group | +| storagesync_registeredserver_register | Register a new server with service using server ID | +| storagesync_registeredserver_unregister | Unregister server from service | +| storagesync_registeredserver_update | Update registered server configuration in service | +| storagesync_syncgroup_create | Create a new sync group named in service | +| storagesync_syncgroup_delete | Delete the sync group from service | +| storagesync_syncgroup_get | Get the details of sync group in service | +| storagesync_syncgroup_list | List all sync groups in service in resource group | +| storagesync_cloudendpoint_changedetection | Trigger change detection on cloud endpoint in sync group in service | +| storagesync_cloudendpoint_create | Create a new cloud endpoint named for Azure file share in storage account | +| storagesync_cloudendpoint_delete | Delete the cloud endpoint from sync group | +| storagesync_cloudendpoint_get | Get the details of cloud endpoint in sync group | +| storagesync_cloudendpoint_get | List all cloud endpoints in sync group | +| storagesync_cloudendpoint_list | List all cloud endpoints in sync group in service | +| storagesync_serverendpoint_create | Create a new server endpoint on server pointing to local path in sync group | +| storagesync_serverendpoint_delete | Delete the server endpoint from sync group | +| storagesync_serverendpoint_get | Get the details of server endpoint in sync group | +| storagesync_serverendpoint_get | List all server endpoints in sync group | +| storagesync_serverendpoint_list | List all server endpoints in sync group in service | +| storagesync_serverendpoint_update | Update server endpoint with cloud tiering enabled and tiering policy in sync group | + ## Azure Subscription Management | Tool Name | Test Prompt | diff --git a/servers/Azure.Mcp.Server/azureicon.png b/servers/Azure.Mcp.Server/images/azureicon.png similarity index 100% rename from servers/Azure.Mcp.Server/azureicon.png rename to servers/Azure.Mcp.Server/images/azureicon.png diff --git a/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj b/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj index 7ccc36c4ba..14c47f2939 100644 --- a/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj +++ b/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj @@ -1,12 +1,12 @@ - 2.0.0-beta.8 + 2.0.0-beta.9 azmcp Azure MCP Server Azure MCP Server - Model Context Protocol implementation for Azure https://github.com/Microsoft/mcp/blob/main/servers/Azure.Mcp.Server#readme $(RepoRoot)/servers/Azure.Mcp.Server/README.md - $(MSBuildThisFileDirectory)../azureicon.png + $(MSBuildThisFileDirectory)../images/azureicon.png com.microsoft/azure diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs index d3c21f81ff..7b77d56bbf 100644 --- a/servers/Azure.Mcp.Server/src/Program.cs +++ b/servers/Azure.Mcp.Server/src/Program.cs @@ -116,6 +116,7 @@ private static IAreaSetup[] RegisterAreas() new Azure.Mcp.Tools.SignalR.SignalRSetup(), new Azure.Mcp.Tools.Sql.SqlSetup(), new Azure.Mcp.Tools.Storage.StorageSetup(), + new Azure.Mcp.Tools.StorageSync.StorageSyncSetup(), new Azure.Mcp.Tools.VirtualDesktop.VirtualDesktopSetup(), new Azure.Mcp.Tools.Workbooks.WorkbooksSetup(), #if !BUILD_NATIVE diff --git a/servers/Azure.Mcp.Server/src/Properties/launchSettings.json b/servers/Azure.Mcp.Server/src/Properties/launchSettings.json index bffcb4ab1d..11295e1a47 100644 --- a/servers/Azure.Mcp.Server/src/Properties/launchSettings.json +++ b/servers/Azure.Mcp.Server/src/Properties/launchSettings.json @@ -22,4 +22,4 @@ } }, "$schema": "https://json.schemastore.org/launchsettings.json" -} \ No newline at end of file +} diff --git a/servers/Azure.Mcp.Server/vscode/CHANGELOG.md b/servers/Azure.Mcp.Server/vscode/CHANGELOG.md index c0ae4ae236..8952d7af5a 100644 --- a/servers/Azure.Mcp.Server/vscode/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/vscode/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 2.0.8 (2025-12-11) (pre-release) + +### Fixed + +- Fixed an issue where the AI Best Practices tool would get called instead of the Best Practices tool. [[#1323](https://github.com/microsoft/mcp/pull/1323)] + ## 2.0.7 (2025-11-25) (pre-release) ### Changed diff --git a/servers/Fabric.Mcp.Server/CHANGELOG.md b/servers/Fabric.Mcp.Server/CHANGELOG.md index ef5248f05c..260e2e35df 100644 --- a/servers/Fabric.Mcp.Server/CHANGELOG.md +++ b/servers/Fabric.Mcp.Server/CHANGELOG.md @@ -5,7 +5,17 @@ All notable changes to the Microsoft Fabric MCP Server will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.0.0-beta.4] (2025-12-16) +## 0.0.0-beta.5 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + +## 0.0.0-beta.4 (2025-12-16) ### Features Added @@ -20,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated API specifications for multiple items. -## [0.0.0-beta.3] (2025-12-04) +## 0.0.0-beta.3 (2025-12-04) ### Features Added @@ -64,4 +74,4 @@ Initial release of the Microsoft Fabric MCP Server in **Public Preview**. --- -For support, contributions, and feedback, see [SUPPORT](https://github.com/microsoft/mcp/blob/main/servers/Fabric.Mcp.Server/SUPPORT.md). \ No newline at end of file +For support, contributions, and feedback, see [SUPPORT](https://github.com/microsoft/mcp/blob/main/servers/Fabric.Mcp.Server/SUPPORT.md). diff --git a/servers/Fabric.Mcp.Server/src/Fabric.Mcp.Server.csproj b/servers/Fabric.Mcp.Server/src/Fabric.Mcp.Server.csproj index 134eedb39c..dfddf5495f 100644 --- a/servers/Fabric.Mcp.Server/src/Fabric.Mcp.Server.csproj +++ b/servers/Fabric.Mcp.Server/src/Fabric.Mcp.Server.csproj @@ -1,6 +1,6 @@ - + - 0.0.0-beta.4 + 0.0.0-beta.5 fabmcp Fabric MCP Server Microsoft Fabric MCP Server - Model Context Protocol implementation for Fabric diff --git a/servers/Fabric.Mcp.Server/vscode/package.json b/servers/Fabric.Mcp.Server/vscode/package.json index 7a6232f2b2..9f2aa19e33 100644 --- a/servers/Fabric.Mcp.Server/vscode/package.json +++ b/servers/Fabric.Mcp.Server/vscode/package.json @@ -52,10 +52,12 @@ "items": { "type": "string", "enum": [ - "publicapis" + "publicapis", + "onelake" ], "markdownEnumDescriptions": [ - "Fabric public APIs — Container registry management." + "Fabric public APIs — Fabric public APIs specifications and examples.", + "OneLake — Manage and interact with OneLake data lake storage." ] }, "uniqueItems": true, @@ -66,7 +68,7 @@ "type": "string", "enum": ["single", "namespace", "all"], "default": "all", - "markdownDescription": "Server Mode determines how tools are exposed: `single` collapses every tool (100+) into one (1) single tool that routes internally, `namespace` collapses all tools down into logical Fabric service namespace grouping, while `all` (default) exposes every MCP tool directly to the MCP client. We recommend all as the right balance between MCP tool count and tool selection accuracy. **Note:** Changes require restarting the MCP server if MCP Autostart is not configured (Command Palette → MCP: List Servers → Fabric MCP → Start/Restart)." + "markdownDescription": "Server Mode determines how tools are exposed: `single` collapses every tool into one single tool that routes internally, `namespace` collapses all tools down into logical Fabric service namespace grouping, while `all` (default) exposes every MCP tool directly to the MCP client. We recommend all as the right balance between MCP tool count and tool selection accuracy. **Note:** Changes require restarting the MCP server if MCP Autostart is not configured (Command Palette → MCP: List Servers → Fabric MCP → Start/Restart)." }, "fabricMcp.readOnly": { diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs index eeae72bf11..38732bc6e0 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs @@ -4,12 +4,12 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Aks.LiveTests; -public sealed class AksCommandTests(ITestOutputHelper output) - : CommandTestsBase(output) +public sealed class AksCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { [Fact] @@ -149,8 +149,8 @@ public async Task Should_get_specific_aks_cluster() // Get the first cluster's details var firstCluster = clusters.EnumerateArray().First(); - var clusterName = firstCluster.GetProperty("name").GetString()!; - var resourceGroupName = firstCluster.GetProperty("resourceGroupName").GetString()!; + var clusterName = RegisterOrRetrieveVariable("firstClusterName", firstCluster.GetProperty("name").GetString()!); + var resourceGroupName = RegisterOrRetrieveVariable("firstResourceGroupName", firstCluster.GetProperty("resourceGroupName").GetString()!); // Now test the get command var getResult = await CallToolAsync( @@ -172,33 +172,33 @@ public async Task Should_get_specific_aks_cluster() // Verify the cluster details var nameProperty = cluster.AssertProperty("name"); - Assert.Equal(clusterName, nameProperty.GetString()); + Assert.Equal(TestMode == Tests.Helpers.TestMode.Playback ? "Sanitized" : clusterName, nameProperty.GetString()); var rgProperty = cluster.AssertProperty("resourceGroupName"); Assert.Equal(resourceGroupName, rgProperty.GetString()); // Verify other common properties exist - Assert.True(cluster.TryGetProperty("subscriptionId", out _)); - Assert.True(cluster.TryGetProperty("location", out _)); + cluster.AssertProperty("subscriptionId"); + cluster.AssertProperty("location"); // Enriched cluster checks - Assert.True(cluster.TryGetProperty("id", out _)); - Assert.True(cluster.TryGetProperty("enableRbac", out _)); - Assert.True(cluster.TryGetProperty("skuName", out _)); - Assert.True(cluster.TryGetProperty("skuTier", out _)); - Assert.True(cluster.TryGetProperty("nodeResourceGroup", out _)); - Assert.True(cluster.TryGetProperty("maxAgentPools", out _)); - Assert.True(cluster.TryGetProperty("supportPlan", out _)); + cluster.AssertProperty("id"); + cluster.AssertProperty("enableRbac"); + cluster.AssertProperty("skuName"); + cluster.AssertProperty("skuTier"); + cluster.AssertProperty("nodeResourceGroup"); + cluster.AssertProperty("maxAgentPools"); + cluster.AssertProperty("supportPlan"); // Profiles present or null - Assert.True(cluster.TryGetProperty("networkProfile", out _)); - Assert.True(cluster.TryGetProperty("windowsProfile", out _)); - Assert.True(cluster.TryGetProperty("servicePrincipalProfile", out _)); - Assert.True(cluster.TryGetProperty("addonProfiles", out _)); - Assert.True(cluster.TryGetProperty("identityProfile", out _)); + cluster.AssertProperty("networkProfile"); + cluster.AssertProperty("windowsProfile"); + cluster.AssertProperty("servicePrincipalProfile"); + cluster.AssertProperty("addonProfiles"); + cluster.AssertProperty("identityProfile"); // Get-specific should return agentPoolProfiles (we populate on Get) - Assert.True(cluster.TryGetProperty("agentPoolProfiles", out var pools)); + var pools = cluster.AssertProperty("agentPoolProfiles"); Assert.Equal(JsonValueKind.Array, pools.ValueKind); } diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs index 031261b5c5..41c5033277 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs @@ -4,12 +4,12 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Aks.LiveTests; -public sealed class NodepoolCommandTests(ITestOutputHelper output) - : CommandTestsBase(output) +public sealed class NodepoolCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { [Fact] public async Task Should_list_nodepools_for_cluster() @@ -26,8 +26,8 @@ public async Task Should_list_nodepools_for_cluster() Assert.True(clusters.GetArrayLength() > 0, "Expected at least one AKS cluster for testing nodepool get command"); var firstCluster = clusters.EnumerateArray().First(); - var clusterName = firstCluster.GetProperty("name").GetString()!; - var resourceGroupName = firstCluster.GetProperty("resourceGroupName").GetString()!; + var clusterName = RegisterOrRetrieveVariable("firstClusterName", firstCluster.GetProperty("name").GetString()!); + var resourceGroupName = RegisterOrRetrieveVariable("firstResourceGroupName", firstCluster.GetProperty("resourceGroupName").GetString()!); // List node pools for that cluster var nodepoolResult = await CallToolAsync( diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs index 9927e69d90..7149e43206 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs @@ -4,12 +4,12 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Aks.LiveTests; -public sealed class NodepoolGetCommandTests(ITestOutputHelper output) - : CommandTestsBase(output) +public sealed class NodepoolGetCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { [Fact] public async Task Should_get_nodepool_for_cluster() @@ -26,8 +26,8 @@ public async Task Should_get_nodepool_for_cluster() Assert.True(clusters.GetArrayLength() > 0, "Expected at least one AKS cluster for testing nodepool get command"); var firstCluster = clusters.EnumerateArray().First(); - var clusterName = firstCluster.GetProperty("name").GetString()!; - var resourceGroupName = firstCluster.GetProperty("resourceGroupName").GetString()!; + var clusterName = RegisterOrRetrieveVariable("firstClusterName", firstCluster.GetProperty("name").GetString()!); + var resourceGroupName = RegisterOrRetrieveVariable("firstResourceGroupName", firstCluster.GetProperty("resourceGroupName").GetString()!); // Find a node pool to query var nodepoolList = await CallToolAsync( @@ -43,7 +43,7 @@ public async Task Should_get_nodepool_for_cluster() Assert.True(nodePools.GetArrayLength() > 0, "Expected at least one node pool in the cluster"); var firstPool = nodePools.EnumerateArray().First(); - var nodepoolName = firstPool.GetProperty("name").GetString()!; + var nodepoolName = RegisterOrRetrieveVariable("firstNodepoolName", firstPool.GetProperty("name").GetString()!); // Get details for that node pool var nodepoolGet = await CallToolAsync( @@ -62,7 +62,7 @@ public async Task Should_get_nodepool_for_cluster() var nodePool = nodePools.EnumerateArray().First(); Assert.Equal(JsonValueKind.Object, nodePool.ValueKind); - Assert.Equal(nodepoolName, nodePool.GetProperty("name").GetString()); + Assert.Equal(TestMode == Tests.Helpers.TestMode.Playback ? "Sanitized" : nodepoolName, nodePool.GetProperty("name").GetString()); if (nodePool.TryGetProperty("mode", out var modeProperty)) { @@ -74,12 +74,12 @@ public async Task Should_get_nodepool_for_cluster() Assert.False(string.IsNullOrEmpty(stateProperty.GetString())); } - Assert.True(nodePool.TryGetProperty("orchestratorVersion", out _)); - Assert.True(nodePool.TryGetProperty("currentOrchestratorVersion", out _)); - Assert.True(nodePool.TryGetProperty("enableAutoScaling", out _)); - Assert.True(nodePool.TryGetProperty("maxPods", out _)); - Assert.True(nodePool.TryGetProperty("osSKU", out _)); - Assert.True(nodePool.TryGetProperty("nodeImageVersion", out _)); + nodePool.AssertProperty("orchestratorVersion"); + nodePool.AssertProperty("currentOrchestratorVersion"); + nodePool.AssertProperty("enableAutoScaling"); + nodePool.AssertProperty("maxPods"); + nodePool.AssertProperty("osSKU"); + nodePool.AssertProperty("nodeImageVersion"); // Enriched node pool fields (presence/type checks) if (nodePool.TryGetProperty("tags", out var tags)) diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/assets.json b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/assets.json new file mode 100644 index 0000000000..26d8c0e9a4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.Aks.LiveTests", + "Tag": "Azure.Mcp.Tools.Aks.LiveTests_2f1814827b" +} diff --git a/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/AuthorizationCommandTests.cs b/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/AuthorizationCommandTests.cs index b508f8ec40..38f8a92f77 100644 --- a/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/AuthorizationCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/AuthorizationCommandTests.cs @@ -4,18 +4,19 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Authorization.LiveTests; -public class AuthorizationCommandTests(ITestOutputHelper output) - : CommandTestsBase(output) +public class AuthorizationCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { [Fact] public async Task Should_list_role_assignments() { - var scope = $"/subscriptions/{Settings.SubscriptionId}/resourceGroups/{Settings.ResourceGroupName}"; + var resourceGroupName = RegisterOrRetrieveVariable("resourceGroupName", Settings.ResourceGroupName); + var scope = $"/subscriptions/{Settings.SubscriptionId}/resourceGroups/{resourceGroupName}"; var result = await CallToolAsync( "role_assignment_list", new() diff --git a/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/assets.json b/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/assets.json new file mode 100644 index 0000000000..3fdba95297 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Authorization/tests/Azure.Mcp.Tools.Authorization.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.Authorization.LiveTests", + "Tag": "Azure.Mcp.Tools.Authorization.LiveTests_23629421af" +} diff --git a/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/FunctionAppCommandTests.cs b/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/FunctionAppCommandTests.cs index db12f5b800..eee5053a8a 100644 --- a/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/FunctionAppCommandTests.cs +++ b/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/FunctionAppCommandTests.cs @@ -4,12 +4,29 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; +using Azure.Mcp.Tests.Generated.Models; using Xunit; namespace Azure.Mcp.Tools.FunctionApp.LiveTests; -public sealed class FunctionAppCommandTests(ITestOutputHelper output) : CommandTestsBase(output) +public sealed class FunctionAppCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { + public override List BodyKeySanitizers => + [ + ..base.BodyKeySanitizers, + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.customDomainVerificationId")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.inboundIpAddress")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.possibleInboundIpAddresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.inboundIpv6Address")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.possibleInboundIpv6Addresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.ftpsHostName")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.outboundIpAddresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.possibleOutboundIpAddresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.outboundIpv6Addresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.possibleOutboundIpv6Addresses")), + new BodyKeySanitizer(new BodyKeySanitizerBody("$..properties.homeStamp")), + ]; [Fact] public async Task Should_list_function_apps_by_subscription() @@ -30,13 +47,13 @@ public async Task Should_list_function_apps_by_subscription() { Assert.Equal(JsonValueKind.Object, functionApp.ValueKind); - var nameProperty = functionApp.GetProperty("name"); + var nameProperty = functionApp.AssertProperty("name"); Assert.False(string.IsNullOrEmpty(nameProperty.GetString())); - var rgProperty = functionApp.GetProperty("resourceGroupName"); + var rgProperty = functionApp.AssertProperty("resourceGroupName"); Assert.False(string.IsNullOrEmpty(rgProperty.GetString())); - var aspProperty = functionApp.GetProperty("appServicePlanName"); + var aspProperty = functionApp.AssertProperty("appServicePlanName"); Assert.False(string.IsNullOrEmpty(aspProperty.GetString())); if (functionApp.TryGetProperty("location", out var locationProperty)) @@ -92,20 +109,26 @@ public async Task Should_validate_required_subscription_parameter() [Fact] public async Task Should_get_specific_function_app() { + var resourceGroupName = RegisterOrRetrieveVariable("resourceGroupName", Settings.ResourceGroupName); // List to obtain a real function app and its resource group var listResult = await CallToolAsync( "functionapp_get", new() { - { "subscription", Settings.SubscriptionId } + { "subscription", Settings.SubscriptionId }, + { "resource-group", resourceGroupName } }); var functionApps = listResult.AssertProperty("functionApps"); Assert.True(functionApps.GetArrayLength() > 0, "Expected at least one Function App for get command test"); var first = functionApps.EnumerateArray().First(); - var name = first.GetProperty("name").GetString()!; - var resourceGroup = first.GetProperty("resourceGroupName").GetString()!; + var name = RegisterOrRetrieveVariable("functionAppName", first.AssertProperty("name").GetString()!); + if (TestMode == Tests.Helpers.TestMode.Playback) + { + name = string.Concat("Sanitized", name.AsSpan(name.IndexOf('-'))); + } + var resourceGroup = first.AssertProperty("resourceGroupName").GetString(); var getResult = await CallToolAsync( "functionapp_get", @@ -123,8 +146,8 @@ public async Task Should_get_specific_function_app() var functionApp = functionApps.EnumerateArray().First(); Assert.Equal(JsonValueKind.Object, functionApp.ValueKind); - Assert.Equal(name, functionApp.GetProperty("name").GetString()); - Assert.Equal(resourceGroup, functionApp.GetProperty("resourceGroupName").GetString()); + Assert.Equal(TestMode == Tests.Helpers.TestMode.Playback ? "Sanitized" : name, functionApp.AssertProperty("name").GetString()); + Assert.Equal(resourceGroup, functionApp.AssertProperty("resourceGroupName").GetString()); // Common useful properties if (functionApp.TryGetProperty("location", out var loc)) { diff --git a/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/assets.json b/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/assets.json new file mode 100644 index 0000000000..4f0cfada03 --- /dev/null +++ b/tools/Azure.Mcp.Tools.FunctionApp/tests/Azure.Mcp.Tools.FunctionApp.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.FunctionApp.LiveTests", + "Tag": "Azure.Mcp.Tools.FunctionApp.LiveTests_bcd7a5b214" +} diff --git a/tools/Azure.Mcp.Tools.Marketplace/src/Services/MarketplaceService.cs b/tools/Azure.Mcp.Tools.Marketplace/src/Services/MarketplaceService.cs index 5470a89d76..7f7d2fb87a 100644 --- a/tools/Azure.Mcp.Tools.Marketplace/src/Services/MarketplaceService.cs +++ b/tools/Azure.Mcp.Tools.Marketplace/src/Services/MarketplaceService.cs @@ -223,30 +223,23 @@ CancellationToken cancellationToken ) { // Use Azure Core pipeline approach consistently - var clientOptions = AddDefaultPolicies(new MarketplaceClientOptions()); + using var httpClient = TenantService.GetClient(); + var clientOptions = ConfigureRetryPolicy( + AddDefaultPolicies(new MarketplaceClientOptions()), + retryPolicy); + clientOptions.Transport = new HttpClientTransport(httpClient); - // Configure retry policy if provided - if (retryPolicy != null) - { - clientOptions.Retry.MaxRetries = retryPolicy.MaxRetries; - clientOptions.Retry.Mode = retryPolicy.Mode; - clientOptions.Retry.Delay = TimeSpan.FromSeconds(retryPolicy.DelaySeconds); - clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(retryPolicy.MaxDelaySeconds); - clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(retryPolicy.NetworkTimeoutSeconds); - } - - // Create pipeline var pipeline = HttpPipelineBuilder.Build(clientOptions); string accessToken = (await GetArmAccessTokenAsync(tenantId: tenant, cancellationToken)).Token; ValidateRequiredParameters((nameof(accessToken), accessToken)); - var request = pipeline.CreateRequest(); + using var request = pipeline.CreateRequest(); request.Method = RequestMethod.Get; request.Uri.Reset(new Uri(url)); request.Headers.Add("Authorization", $"Bearer {accessToken}"); - var response = await pipeline.SendRequestAsync(request, cancellationToken); + using var response = await pipeline.SendRequestAsync(request, cancellationToken); if (!response.IsError) { diff --git a/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductGetCommandTests.cs b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductGetCommandTests.cs index c179e5ec78..0473bbbc55 100644 --- a/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductGetCommandTests.cs @@ -2,49 +2,19 @@ // Licensed under the MIT License. using System.Text.Json; -using Azure.Mcp.Core.Services.Azure.Authentication; -using Azure.Mcp.Core.Services.Azure.Tenant; -using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; -using Azure.Mcp.Tests.Helpers; -using Azure.Mcp.Tools.Marketplace.Services; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Marketplace.LiveTests; -public class ProductGetCommandTests : CommandTestsBase +public sealed class ProductGetCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { private const string ProductKey = "product"; private const string ProductId = "test_test_pmc2pc1.vmsr_uat_beta"; private const string Language = "en"; private const string Market = "US"; - private readonly MarketplaceService _marketplaceService; - private readonly ServiceProvider _httpClientProvider; - - public ProductGetCommandTests(ITestOutputHelper output) : base(output) - { - var memoryCache = new MemoryCache(Microsoft.Extensions.Options.Options.Create(new MemoryCacheOptions())); - var cacheService = new SingleUserCliCacheService(memoryCache); - var tokenProvider = new SingleIdentityTokenCredentialProvider(NullLoggerFactory.Instance); - _httpClientProvider = TestHttpClientFactoryProvider.Create(); - var httpClientFactory = _httpClientProvider.GetRequiredService(); - var tenantService = new TenantService(tokenProvider, cacheService, httpClientFactory); - _marketplaceService = new MarketplaceService(tenantService); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _httpClientProvider.Dispose(); - } - - base.Dispose(disposing); - } [Fact] public async Task Should_get_marketplace_product() diff --git a/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductListCommandTests.cs b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductListCommandTests.cs index 42a2930bfc..21efe1f1fe 100644 --- a/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/ProductListCommandTests.cs @@ -2,47 +2,17 @@ // Licensed under the MIT License. using System.Text.Json; -using Azure.Mcp.Core.Services.Azure.Authentication; -using Azure.Mcp.Core.Services.Azure.Tenant; -using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; -using Azure.Mcp.Tests.Helpers; -using Azure.Mcp.Tools.Marketplace.Services; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; +using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Marketplace.LiveTests; -public class ProductListCommandTests : CommandTestsBase +public sealed class ProductListCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) { private const string ProductsKey = "products"; private const string Language = "en"; - private readonly MarketplaceService _marketplaceService; - private readonly ServiceProvider _httpClientProvider; - - public ProductListCommandTests(ITestOutputHelper output) : base(output) - { - var memoryCache = new MemoryCache(Microsoft.Extensions.Options.Options.Create(new MemoryCacheOptions())); - var cacheService = new SingleUserCliCacheService(memoryCache); - var tokenProvider = new SingleIdentityTokenCredentialProvider(NullLoggerFactory.Instance); - _httpClientProvider = TestHttpClientFactoryProvider.Create(); - var httpClientFactory = _httpClientProvider.GetRequiredService(); - var tenantService = new TenantService(tokenProvider, cacheService, httpClientFactory); - _marketplaceService = new MarketplaceService(tenantService); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _httpClientProvider.Dispose(); - } - - base.Dispose(disposing); - } [Fact] public async Task Should_list_marketplace_products() diff --git a/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/assets.json b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/assets.json new file mode 100644 index 0000000000..f2731ac0cb --- /dev/null +++ b/tools/Azure.Mcp.Tools.Marketplace/tests/Azure.Mcp.Tools.Marketplace.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.Marketplace.LiveTests", + "Tag": "Azure.Mcp.Tools.Marketplace.LiveTests_0f59abc9d6" +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/AssemblyInfo.cs b/tools/Azure.Mcp.Tools.StorageSync/src/AssemblyInfo.cs new file mode 100644 index 0000000000..5229bcb0aa --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Azure.Mcp.Tools.StorageSync.UnitTests")] +[assembly: InternalsVisibleTo("Azure.Mcp.Tools.StorageSync.LiveTests")] diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Azure.Mcp.Tools.StorageSync.csproj b/tools/Azure.Mcp.Tools.StorageSync/src/Azure.Mcp.Tools.StorageSync.csproj new file mode 100644 index 0000000000..3a6b7a2e65 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Azure.Mcp.Tools.StorageSync.csproj @@ -0,0 +1,19 @@ + + + true + + + + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/BaseStorageSyncCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/BaseStorageSyncCommand.cs new file mode 100644 index 0000000000..b04dd8b5b5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/BaseStorageSyncCommand.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Tools.StorageSync.Options; + +namespace Azure.Mcp.Tools.StorageSync.Commands; + +/// +/// Base command class for all Storage Sync commands. +/// Provides common command infrastructure and option registration. +/// +public abstract class BaseStorageSyncCommand< + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions> + : SubscriptionCommand where TOptions : BaseStorageSyncOptions, new() +{ + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + // Additional option registration can be added here for common Storage Sync options + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointCreateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointCreateCommand.cs new file mode 100644 index 0000000000..7caee8389d --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointCreateCommand.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; + +public sealed class CloudEndpointCreateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Create Cloud Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "df0d4ae3-519a-44f1-ad30-d25a0985e9c2"; + + public override string Name => "create"; + + public override string Description => "Create a new cloud endpoint in a sync group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.StorageAccountResourceId.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.AzureFileShareName.AsRequired()); + } + + protected override CloudEndpointCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.CloudEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.Name.Name); + options.StorageAccountResourceId = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.StorageAccountResourceId.Name); + options.AzureFileShareName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.AzureFileShareName.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Creating cloud endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.CloudEndpointName); + + var endpoint = await _service.CreateCloudEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.CloudEndpointName!, + options.StorageAccountResourceId!, + options.AzureFileShareName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new CloudEndpointCreateCommandResult(endpoint); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.CloudEndpointCreateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating cloud endpoint"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(CloudEndpointCreateCommandResult))] + internal record CloudEndpointCreateCommandResult(CloudEndpointDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointDeleteCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointDeleteCommand.cs new file mode 100644 index 0000000000..14ae41065b --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointDeleteCommand.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; + +public sealed class CloudEndpointDeleteCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Delete Cloud Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "f5e76906-cc2a-41a4-b4f9-498221aaaf2e"; + + public override string Name => "delete"; + + public override string Description => "Delete a cloud endpoint from a sync group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.Name.AsRequired()); + } + + protected override CloudEndpointDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.CloudEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Deleting cloud endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.CloudEndpointName); + + await _service.DeleteCloudEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.CloudEndpointName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Cloud endpoint deleted successfully"; + var results = new CloudEndpointDeleteCommandResult("Cloud endpoint deleted successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.CloudEndpointDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting cloud endpoint"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(CloudEndpointDeleteCommandResult))] + internal record CloudEndpointDeleteCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointGetCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointGetCommand.cs new file mode 100644 index 0000000000..c1f848455e --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointGetCommand.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; + +public sealed class CloudEndpointGetCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Get Cloud Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "25dd8bb3-5ba3-4c0d-993d-54917f63d52e"; + + public override string Name => "get"; + + public override string Description => "Get details about a specific cloud endpoint or list all cloud endpoints. If --cloud-endpoint-name is provided, returns a specific cloud endpoint; otherwise, lists all cloud endpoints in the sync group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.Name.AsOptional()); + } + + protected override CloudEndpointGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.CloudEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + // If cloud endpoint name is provided, get specific endpoint + if (!string.IsNullOrEmpty(options.CloudEndpointName)) + { + _logger.LogInformation("Getting cloud endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.CloudEndpointName); + + var endpoint = await _service.GetCloudEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.CloudEndpointName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + if (endpoint == null) + { + context.Response.Status = HttpStatusCode.NotFound; + context.Response.Message = "Cloud endpoint not found"; + return context.Response; + } + + var singleResult = new CloudEndpointGetCommandResult([endpoint]); + context.Response.Results = ResponseResult.Create(singleResult, StorageSyncJsonContext.Default.CloudEndpointGetCommandResult); + } + else + { + // List all cloud endpoints + _logger.LogInformation("Listing cloud endpoints. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName); + + var endpoints = await _service.ListCloudEndpointsAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new CloudEndpointGetCommandResult(endpoints ?? []); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.CloudEndpointGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting cloud endpoint(s)"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(CloudEndpointGetCommandResult))] + internal record CloudEndpointGetCommandResult(List Results); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommand.cs new file mode 100644 index 0000000000..4a3214c906 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommand.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; + +public sealed class CloudEndpointTriggerChangeDetectionCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Trigger Change Detection"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "96f096a2-d36f-4361-aa74-4e393e7f48a5"; + + public override string Name => "triggerchangedetection"; + + public override string Description => "Trigger change detection on a cloud endpoint to sync file changes."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.CloudEndpoint.Name.AsRequired()); + } + + protected override CloudEndpointTriggerChangeDetectionOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.CloudEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.CloudEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Triggering change detection. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.CloudEndpointName); + + await _service.TriggerChangeDetectionAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.CloudEndpointName!, + null, + null, + false, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Change detection triggered successfully"; + var results = new CloudEndpointTriggerChangeDetectionCommandResult("Change detection triggered successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.CloudEndpointTriggerChangeDetectionCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error triggering change detection"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(CloudEndpointTriggerChangeDetectionCommandResult))] + internal record CloudEndpointTriggerChangeDetectionCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerGetCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerGetCommand.cs new file mode 100644 index 0000000000..f098aea1ca --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerGetCommand.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; + +public sealed class RegisteredServerGetCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Get Registered Server"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "fe3b07c3-9a11-465e-bfb6-6461b85b2e52"; + + public override string Name => "get"; + + public override string Description => "Get details about a specific registered server or list all registered servers. If --server-id is provided, returns a specific registered server; otherwise, lists all registered servers in the Storage Sync Service."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.RegisteredServer.ServerId.AsOptional()); + } + + protected override RegisteredServerGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.RegisteredServerId = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.RegisteredServer.ServerId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + // If server ID is provided, get specific server + if (!string.IsNullOrEmpty(options.RegisteredServerId)) + { + _logger.LogInformation("Getting registered server. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, ServerId: {ServerId}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.RegisteredServerId); + + var server = await _service.GetRegisteredServerAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.RegisteredServerId!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + if (server == null) + { + context.Response.Status = HttpStatusCode.NotFound; + context.Response.Message = "Registered server not found"; + return context.Response; + } + + var singleResult = new RegisteredServerGetCommandResult([server]); + context.Response.Results = ResponseResult.Create(singleResult, StorageSyncJsonContext.Default.RegisteredServerGetCommandResult); + } + else + { + // List all registered servers + _logger.LogInformation("Listing registered servers. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName); + + var servers = await _service.ListRegisteredServersAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new RegisteredServerGetCommandResult(servers ?? []); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.RegisteredServerGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting registered server(s)"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(RegisteredServerGetCommandResult))] + internal record RegisteredServerGetCommandResult(List Results); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUnregisterCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUnregisterCommand.cs new file mode 100644 index 0000000000..8e0b086154 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUnregisterCommand.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; + +public sealed class RegisteredServerUnregisterCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Unregister Server"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "346661e1-64be-463a-96c6-3626966f55fa"; + + public override string Name => "unregister"; + + public override string Description => "Unregister a server from a Storage Sync service."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.RegisteredServer.ServerId.AsRequired()); + } + + protected override RegisteredServerUnregisterOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.RegisteredServerId = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.RegisteredServer.ServerId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Unregistering server. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, ServerId: {ServerId}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.RegisteredServerId); + + await _service.UnregisterServerAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.RegisteredServerId!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Server unregistered successfully"; + var results = new RegisteredServerUnregisterCommandResult("Server unregistered successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.RegisteredServerUnregisterCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error unregistering server"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(RegisteredServerUnregisterCommandResult))] + internal record RegisteredServerUnregisterCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUpdateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUpdateCommand.cs new file mode 100644 index 0000000000..8a6a4d254a --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/RegisteredServer/RegisteredServerUpdateCommand.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; + +public sealed class RegisteredServerUpdateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Update Registered Server"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "c443ed00-f17f-46a8-a5d3-df128aa1606b"; + + public override string Name => "update"; + + public override string Description => "Update properties of a registered server."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.RegisteredServer.ServerId.AsRequired()); + } + + protected override RegisteredServerUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.RegisteredServerId = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.RegisteredServer.ServerId.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Updating registered server. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, ServerId: {ServerId}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.RegisteredServerId); + + var server = await _service.UpdateServerAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.RegisteredServerId!, + null, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new RegisteredServerUpdateCommandResult(server); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.RegisteredServerUpdateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating registered server"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(RegisteredServerUpdateCommandResult))] + internal record RegisteredServerUpdateCommandResult(RegisteredServerDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointCreateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointCreateCommand.cs new file mode 100644 index 0000000000..fc0189a053 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointCreateCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; + +public sealed class ServerEndpointCreateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Create Server Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "fcbdf461-6fde-4cfb-a944-4a56a2be90e4"; + + public override string Name => "create"; + + public override string Description => "Create a new server endpoint in a sync group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.ServerResourceId.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.ServerLocalPath.AsRequired()); + } + + protected override ServerEndpointCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.ServerEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.Name.Name); + options.ServerResourceId = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.ServerResourceId.Name); + options.ServerLocalPath = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.ServerLocalPath.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Creating server endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.ServerEndpointName); + + var endpoint = await _service.CreateServerEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.ServerEndpointName!, + options.ServerResourceId!, + options.ServerLocalPath!, + false, + null, + null, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new ServerEndpointCreateCommandResult(endpoint); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.ServerEndpointCreateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating server endpoint"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(ServerEndpointCreateCommandResult))] + internal record ServerEndpointCreateCommandResult(ServerEndpointDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointDeleteCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointDeleteCommand.cs new file mode 100644 index 0000000000..175870480c --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointDeleteCommand.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; + +public sealed class ServerEndpointDeleteCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Delete Server Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "ef6c2aa9-bb64-4f94-b18b-018e04b504c9"; + + public override string Name => "delete"; + + public override string Description => "Delete a server endpoint from a sync group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.Name.AsRequired()); + } + + protected override ServerEndpointDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.ServerEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Deleting server endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.ServerEndpointName); + + await _service.DeleteServerEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.ServerEndpointName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Server endpoint deleted successfully"; + var results = new ServerEndpointDeleteCommandResult("Server endpoint deleted successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.ServerEndpointDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting server endpoint"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(ServerEndpointDeleteCommandResult))] + internal record ServerEndpointDeleteCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointGetCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointGetCommand.cs new file mode 100644 index 0000000000..4f3cc04c31 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointGetCommand.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; + +public sealed class ServerEndpointGetCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Get Server Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "cf197b94-6aa6-403b-8679-3a1ce5440ca3"; + + public override string Name => "get"; + + public override string Description => "Get details about a specific server endpoint or list all server endpoints. If --name is provided, returns a specific server endpoint; otherwise, lists all server endpoints in the Sync Group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.Name.AsOptional()); + } + + protected override ServerEndpointGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.ServerEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + // If server endpoint name is provided, get specific endpoint + if (!string.IsNullOrEmpty(options.ServerEndpointName)) + { + _logger.LogInformation("Getting server endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.ServerEndpointName); + + var endpoint = await _service.GetServerEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.ServerEndpointName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + if (endpoint == null) + { + context.Response.Status = HttpStatusCode.NotFound; + context.Response.Message = "Server endpoint not found"; + return context.Response; + } + + var singleResult = new ServerEndpointGetCommandResult([endpoint]); + context.Response.Results = ResponseResult.Create(singleResult, StorageSyncJsonContext.Default.ServerEndpointGetCommandResult); + } + else + { + // List all server endpoints + _logger.LogInformation("Listing server endpoints. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName); + + var endpoints = await _service.ListServerEndpointsAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new ServerEndpointGetCommandResult(endpoints ?? []); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.ServerEndpointGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting server endpoint(s)"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(ServerEndpointGetCommandResult))] + internal record ServerEndpointGetCommandResult(List Results); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointUpdateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointUpdateCommand.cs new file mode 100644 index 0000000000..1f2948b3e9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/ServerEndpoint/ServerEndpointUpdateCommand.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; + +public sealed class ServerEndpointUpdateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Update Server Endpoint"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "7b35bb46-0a34-4e44-9d7c-148e9992b445"; + + public override string Name => "update"; + + public override string Description => "Update properties of a server endpoint."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.ServerEndpoint.Name.AsRequired()); + } + + protected override ServerEndpointUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + options.ServerEndpointName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.ServerEndpoint.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Updating server endpoint. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}, EndpointName: {EndpointName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName, options.ServerEndpointName); + + var endpoint = await _service.UpdateServerEndpointAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.ServerEndpointName!, + null, + null, + null, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new ServerEndpointUpdateCommandResult(endpoint); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.ServerEndpointUpdateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating server endpoint"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(ServerEndpointUpdateCommandResult))] + internal record ServerEndpointUpdateCommandResult(ServerEndpointDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceCreateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceCreateCommand.cs new file mode 100644 index 0000000000..72fc3bb8ac --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceCreateCommand.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; + +public sealed class StorageSyncServiceCreateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Create Storage Sync Service"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "7c76387f-c62e-48d1-af3b-d444d6b3b79c"; + + public override string Name => "create"; + + public override string Description => "Create a new Azure Storage Sync service in the specified resource group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Location.AsRequired()); + } + + protected override StorageSyncServiceCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Name = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.Location = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Location.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Creating storage sync service. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.Name); + + var service = await _service.CreateStorageSyncServiceAsync( + options.Subscription!, + options.ResourceGroup!, + options.Name!, + options.Location!, + null, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new StorageSyncServiceCreateCommandResult(service); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.StorageSyncServiceCreateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating storage sync service"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(StorageSyncServiceCreateCommandResult))] + internal record StorageSyncServiceCreateCommandResult(StorageSyncServiceDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceDeleteCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceDeleteCommand.cs new file mode 100644 index 0000000000..054190ba55 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceDeleteCommand.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; + +public sealed class StorageSyncServiceDeleteCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Delete Storage Sync Service"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "a7dcf4e2-fd1d-4d0a-acd3-f56ea5eceef6"; + + public override string Name => "delete"; + + public override string Description => "Delete an Azure Storage Sync service and all its associated resources."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + } + + protected override StorageSyncServiceDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Name = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Deleting storage sync service. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.Name); + + await _service.DeleteStorageSyncServiceAsync( + options.Subscription!, + options.ResourceGroup!, + options.Name!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Storage sync service deleted successfully"; + var results = new StorageSyncServiceDeleteCommandResult("Storage sync service deleted successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.StorageSyncServiceDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting storage sync service"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(StorageSyncServiceDeleteCommandResult))] + internal record StorageSyncServiceDeleteCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceGetCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceGetCommand.cs new file mode 100644 index 0000000000..abc874a122 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceGetCommand.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; + +public sealed class StorageSyncServiceGetCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Get Storage Sync Service"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "77734a55-8290-4c16-8b37-cf37277f018f"; + + public override string Name => "get"; + + public override string Description => "Get details about a specific Azure Storage Sync service or list all services. If --name is provided, returns a specific service; otherwise, lists all services in the subscription or resource group."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsOptional()); + } + + protected override StorageSyncServiceGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Name = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + // If name is provided, get specific service + if (!string.IsNullOrEmpty(options.Name)) + { + if (string.IsNullOrEmpty(options.ResourceGroup)) + { + context.Response.Status = HttpStatusCode.BadRequest; + context.Response.Message = "Resource group is required when getting a specific storage sync service by name"; + return context.Response; + } + + _logger.LogInformation("Getting storage sync service. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.Name); + + var service = await _service.GetStorageSyncServiceAsync( + options.Subscription!, + options.ResourceGroup!, + options.Name!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + if (service == null) + { + context.Response.Status = HttpStatusCode.NotFound; + context.Response.Message = "Storage sync service not found"; + return context.Response; + } + + var singleResult = new StorageSyncServiceGetCommandResult([service]); + context.Response.Results = ResponseResult.Create(singleResult, StorageSyncJsonContext.Default.StorageSyncServiceGetCommandResult); + } + else + { + // List all services + _logger.LogInformation("Listing storage sync services. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}", + options.Subscription, options.ResourceGroup); + + var services = await _service.ListStorageSyncServicesAsync( + options.Subscription!, + options.ResourceGroup, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new StorageSyncServiceGetCommandResult(services ?? []); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.StorageSyncServiceGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting storage sync service(s)"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(StorageSyncServiceGetCommandResult))] + internal record StorageSyncServiceGetCommandResult(List Results); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceUpdateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceUpdateCommand.cs new file mode 100644 index 0000000000..41b0beb163 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/StorageSyncService/StorageSyncServiceUpdateCommand.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; + +public sealed class StorageSyncServiceUpdateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Update Storage Sync Service"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "15db4769-1941-4b1e-9514-867b0f68eb2c"; + + public override string Name => "update"; + + public override string Description => "Update properties of an existing Azure Storage Sync service."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + } + + protected override StorageSyncServiceUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Name = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Updating storage sync service. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.Name); + + var service = await _service.UpdateStorageSyncServiceAsync( + options.Subscription!, + options.ResourceGroup!, + options.Name!, + null, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new StorageSyncServiceUpdateCommandResult(service); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.StorageSyncServiceUpdateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating storage sync service"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(StorageSyncServiceUpdateCommandResult))] + internal record StorageSyncServiceUpdateCommandResult(StorageSyncServiceDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupCreateCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupCreateCommand.cs new file mode 100644 index 0000000000..c14504aa43 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupCreateCommand.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; + +public sealed class SyncGroupCreateCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Create Sync Group"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "3572833c-4fc2-4bb9-9eed-52ae8b8899b8"; + + public override string Name => "create"; + + public override string Description => "Create a new sync group in a Storage Sync service."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + } + + protected override SyncGroupCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Creating sync group. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName); + + var syncGroup = await _service.CreateSyncGroupAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new SyncGroupCreateCommandResult(syncGroup); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.SyncGroupCreateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating sync group"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(SyncGroupCreateCommandResult))] + internal record SyncGroupCreateCommandResult(SyncGroupDataSchema Result); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupDeleteCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupDeleteCommand.cs new file mode 100644 index 0000000000..e119aeea79 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupDeleteCommand.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; + +public sealed class SyncGroupDeleteCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Delete Sync Group"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "c8f91bd7-ea1d-4af4-9703-fe83c43b34b5"; + + public override string Name => "delete"; + + public override string Description => "Delete a sync group and all its associated endpoints."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = true, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsRequired()); + } + + protected override SyncGroupDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + _logger.LogInformation("Deleting sync group. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName); + + await _service.DeleteSyncGroupAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Message = "Sync group deleted successfully"; + var results = new SyncGroupDeleteCommandResult("Sync group deleted successfully"); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.SyncGroupDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting sync group"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(SyncGroupDeleteCommandResult))] + internal record SyncGroupDeleteCommandResult(string Message); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupGetCommand.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupGetCommand.cs new file mode 100644 index 0000000000..36cf875a82 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Commands/SyncGroup/SyncGroupGetCommand.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.Mcp.Tools.StorageSync.Options; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; + +public sealed class SyncGroupGetCommand(ILogger logger, IStorageSyncService service) : BaseStorageSyncCommand +{ + private const string CommandTitle = "Get Sync Group"; + private readonly IStorageSyncService _service = service; + private readonly ILogger _logger = logger; + + public override string Id => "95ce2336-19e6-40fb-a3ea-e2a76772036b"; + + public override string Name => "get"; + + public override string Description => "Get details about a specific sync group or list all sync groups. If --sync-group-name is provided, returns a specific sync group; otherwise, lists all sync groups in the Storage Sync service."; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.StorageSyncService.Name.AsRequired()); + command.Options.Add(StorageSyncOptionDefinitions.SyncGroup.Name.AsOptional()); + } + + protected override SyncGroupGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.StorageSyncServiceName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.StorageSyncService.Name.Name); + options.SyncGroupName = parseResult.GetValueOrDefault(StorageSyncOptionDefinitions.SyncGroup.Name.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + // If sync group name is provided, get specific sync group + if (!string.IsNullOrEmpty(options.SyncGroupName)) + { + _logger.LogInformation("Getting sync group. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}, GroupName: {GroupName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName, options.SyncGroupName); + + var syncGroup = await _service.GetSyncGroupAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.SyncGroupName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + if (syncGroup == null) + { + context.Response.Status = HttpStatusCode.NotFound; + context.Response.Message = "Sync group not found"; + return context.Response; + } + + var singleResult = new SyncGroupGetCommandResult([syncGroup]); + context.Response.Results = ResponseResult.Create(singleResult, StorageSyncJsonContext.Default.SyncGroupGetCommandResult); + } + else + { + // List all sync groups + _logger.LogInformation("Listing sync groups. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ServiceName: {ServiceName}", + options.Subscription, options.ResourceGroup, options.StorageSyncServiceName); + + var syncGroups = await _service.ListSyncGroupsAsync( + options.Subscription!, + options.ResourceGroup!, + options.StorageSyncServiceName!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + var results = new SyncGroupGetCommandResult(syncGroups ?? []); + context.Response.Results = ResponseResult.Create(results, StorageSyncJsonContext.Default.SyncGroupGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting sync group(s)"); + HandleException(context, ex); + } + + return context.Response; + } + + [JsonSerializable(typeof(SyncGroupGetCommandResult))] + internal record SyncGroupGetCommandResult(List Results); +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.StorageSync/src/GlobalUsings.cs new file mode 100644 index 0000000000..b41cc886b4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.CommandLine; diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Models/CloudEndpointDataSchema.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Models/CloudEndpointDataSchema.cs new file mode 100644 index 0000000000..4b5ca25901 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Models/CloudEndpointDataSchema.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.ResourceManager.StorageSync; + +namespace Azure.Mcp.Tools.StorageSync.Models; + +/// +/// Data transfer object for Cloud Endpoint information. +/// +public sealed record CloudEndpointDataSchema( + [property: JsonPropertyName("id")] string? Id = null, + [property: JsonPropertyName("name")] string? Name = null, + [property: JsonPropertyName("type")] string? Type = null, + [property: JsonPropertyName("storageAccountResourceId")] string? StorageAccountResourceId = null, + [property: JsonPropertyName("azureFileShareName")] string? AzureFileShareName = null, + [property: JsonPropertyName("storageAccountTenantId")] string? StorageAccountTenantId = null, + [property: JsonPropertyName("partnershipId")] string? PartnershipId = null, + [property: JsonPropertyName("provisioningState")] string? ProvisioningState = null, + [property: JsonPropertyName("lastOperationName")] string? LastOperationName = null, + [property: JsonPropertyName("lastSyncTime")] DateTimeOffset? LastSyncTime = null) +{ + /// + /// Default constructor for deserialization. + /// + public CloudEndpointDataSchema() : this(null, null, null, null, null, null, null, null, null, null) { } + + /// + /// Creates a CloudEndpointDataSchema from a CloudEndpointResource. + /// + public static CloudEndpointDataSchema FromResource(CloudEndpointResource resource) + { + var data = resource.Data; + return new CloudEndpointDataSchema( + data.Id.ToString(), + data.Name, + data.ResourceType.ToString(), + data.StorageAccountResourceId?.ToString(), + data.AzureFileShareName, + null, + data.PartnershipId?.ToString() + ); + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Models/RegisteredServerDataSchema.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Models/RegisteredServerDataSchema.cs new file mode 100644 index 0000000000..46fdc7de50 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Models/RegisteredServerDataSchema.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.ResourceManager.StorageSync; + +namespace Azure.Mcp.Tools.StorageSync.Models; + +/// +/// Data transfer object for Registered Server information. +/// +public sealed record RegisteredServerDataSchema( + [property: JsonPropertyName("id")] string? Id = null, + [property: JsonPropertyName("name")] string? Name = null, + [property: JsonPropertyName("type")] string? Type = null, + [property: JsonPropertyName("serverName")] string? ServerName = null, + [property: JsonPropertyName("serverOsVersion")] string? ServerOSVersion = null, + [property: JsonPropertyName("agentVersion")] string? AgentVersion = null, + [property: JsonPropertyName("provisioningState")] string? ProvisioningState = null, + [property: JsonPropertyName("serverRole")] string? ServerRole = null, + [property: JsonPropertyName("clusterName")] string? ClusterName = null, + [property: JsonPropertyName("serverCertificate")] string? ServerCertificate = null) +{ + /// + /// Default constructor for deserialization. + /// + public RegisteredServerDataSchema() : this(null, null, null, null, null, null, null, null, null, null) { } + + /// + /// Creates a RegisteredServerDataSchema from a StorageSyncRegisteredServerResource. + /// + public static RegisteredServerDataSchema FromResource(StorageSyncRegisteredServerResource resource) + { + var data = resource.Data; + return new RegisteredServerDataSchema( + data.Id.ToString(), + data.Name, + data.ResourceType.ToString(), + null, + null, + data.AgentVersion, + null, + null, + null, + data.ServerCertificate?.ToString() + ); + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Models/ServerEndpointDataSchema.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Models/ServerEndpointDataSchema.cs new file mode 100644 index 0000000000..5198e2f5e5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Models/ServerEndpointDataSchema.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.ResourceManager.StorageSync; + +namespace Azure.Mcp.Tools.StorageSync.Models; + +/// +/// Data transfer object for Server Endpoint information. +/// +public sealed record ServerEndpointDataSchema( + [property: JsonPropertyName("id")] string? Id = null, + [property: JsonPropertyName("name")] string? Name = null, + [property: JsonPropertyName("type")] string? Type = null, + [property: JsonPropertyName("serverResourceId")] string? ServerResourceId = null, + [property: JsonPropertyName("serverLocalPath")] string? ServerLocalPath = null, + [property: JsonPropertyName("cloudTiering")] string? CloudTiering = null, + [property: JsonPropertyName("volumeFreeSpacePercent")] int? VolumeFreeSpacePercent = null, + [property: JsonPropertyName("tierFilesOlderThanDays")] int? TierFilesOlderThanDays = null, + [property: JsonPropertyName("syncStatus")] string? SyncStatus = null, + [property: JsonPropertyName("provisioningState")] string? ProvisioningState = null, + [property: JsonPropertyName("lastOperationName")] string? LastOperationName = null, + [property: JsonPropertyName("lastSyncSuccess")] DateTimeOffset? LastSyncSuccess = null) +{ + /// + /// Default constructor for deserialization. + /// + public ServerEndpointDataSchema() : this(null, null, null, null, null, null, null, null, null, null, null, null) { } + + /// + /// Creates a ServerEndpointDataSchema from a StorageSyncServerEndpointResource. + /// + public static ServerEndpointDataSchema FromResource(StorageSyncServerEndpointResource resource) + { + var data = resource.Data; + return new ServerEndpointDataSchema( + data.Id.ToString(), + data.Name, + data.ResourceType.ToString(), + data.ServerResourceId?.ToString(), + data.ServerLocalPath, + data.CloudTiering?.ToString(), + data.VolumeFreeSpacePercent, + data.TierFilesOlderThanDays + ); + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Models/StorageSyncServiceDataSchema.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Models/StorageSyncServiceDataSchema.cs new file mode 100644 index 0000000000..ee1542d3ce --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Models/StorageSyncServiceDataSchema.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.ResourceManager.StorageSync; + +namespace Azure.Mcp.Tools.StorageSync.Models; + +/// +/// Data transfer object for Storage Sync service information. +/// +public sealed record StorageSyncServiceDataSchema( + [property: JsonPropertyName("id")] string? Id = null, + [property: JsonPropertyName("name")] string? Name = null, + [property: JsonPropertyName("type")] string? Type = null, + [property: JsonPropertyName("location")] string? Location = null, + [property: JsonPropertyName("tags")] Dictionary? Tags = null, + [property: JsonPropertyName("properties")] StorageSyncServicePropertiesSchema? Properties = null) +{ + /// + /// Default constructor for deserialization. + /// + public StorageSyncServiceDataSchema() : this(null, null, null, null, null, null) { } + + /// + /// Creates a StorageSyncServiceDataSchema from a StorageSyncServiceResource. + /// + public static StorageSyncServiceDataSchema FromResource(StorageSyncServiceResource resource) + { + var data = resource.Data; + return new StorageSyncServiceDataSchema( + data.Id.ToString(), + data.Name, + data.ResourceType.ToString(), + data.Location.ToString(), + new Dictionary(data.Tags ?? new Dictionary()), + new StorageSyncServicePropertiesSchema(data.IncomingTrafficPolicy?.ToString()) + ); + } +} + +/// +/// Storage Sync service properties. +/// +public sealed record StorageSyncServicePropertiesSchema( + [property: JsonPropertyName("incomingTrafficPolicy")] string? IncomingTrafficPolicy = null) +{ +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Models/SyncGroupDataSchema.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Models/SyncGroupDataSchema.cs new file mode 100644 index 0000000000..50c2046828 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Models/SyncGroupDataSchema.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.ResourceManager.StorageSync; + +namespace Azure.Mcp.Tools.StorageSync.Models; + +/// +/// Data transfer object for Sync Group information. +/// +public sealed record SyncGroupDataSchema( + [property: JsonPropertyName("id")] string? Id = null, + [property: JsonPropertyName("name")] string? Name = null, + [property: JsonPropertyName("type")] string? Type = null, + [property: JsonPropertyName("uniqueId")] string? UniqueId = null) +{ + /// + /// Default constructor for deserialization. + /// + public SyncGroupDataSchema() : this(null, null, null, null) { } + + /// + /// Creates a SyncGroupDataSchema from a StorageSyncGroupResource. + /// + public static SyncGroupDataSchema FromResource(StorageSyncGroupResource resource) + { + var data = resource.Data; + return new SyncGroupDataSchema( + data.Id.ToString(), + data.Name, + data.ResourceType.ToString(), + data.UniqueId?.ToString() + ); + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/BaseStorageSyncOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/BaseStorageSyncOptions.cs new file mode 100644 index 0000000000..9b91773f0e --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/BaseStorageSyncOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Base options for all Storage Sync commands. +/// Provides common parameters used across the toolset. +/// +public abstract class BaseStorageSyncOptions : SubscriptionOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointCreateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointCreateOptions.cs new file mode 100644 index 0000000000..6132081c01 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointCreateOptions.cs @@ -0,0 +1,32 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for CloudEndpointCreateCommand. +/// +public class CloudEndpointCreateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the cloud endpoint name. + /// + public string? CloudEndpointName { get; set; } + + /// + /// Gets or sets the storage account resource ID. + /// + public string? StorageAccountResourceId { get; set; } + + /// + /// Gets or sets the Azure file share name. + /// + public string? AzureFileShareName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointDeleteOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointDeleteOptions.cs new file mode 100644 index 0000000000..96658b0cc3 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointDeleteOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for CloudEndpointDeleteCommand. +/// +public class CloudEndpointDeleteOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the cloud endpoint name. + /// + public string? CloudEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointGetOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointGetOptions.cs new file mode 100644 index 0000000000..bfa7e006a0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointGetOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for CloudEndpointGetCommand. +/// +public class CloudEndpointGetOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the cloud endpoint name. + /// + public string? CloudEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointListOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointListOptions.cs new file mode 100644 index 0000000000..5b1f30d4f9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointListOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for CloudEndpointListCommand. +/// +public class CloudEndpointListOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointTriggerChangeDetectionOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointTriggerChangeDetectionOptions.cs new file mode 100644 index 0000000000..647c5cd1e4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/CloudEndpoint/CloudEndpointTriggerChangeDetectionOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for CloudEndpointTriggerChangeDetectionCommand. +/// +public class CloudEndpointTriggerChangeDetectionOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the cloud endpoint name. + /// + public string? CloudEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerGetOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerGetOptions.cs new file mode 100644 index 0000000000..f9984e05d7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerGetOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for RegisteredServerGetCommand. +/// +public class RegisteredServerGetOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the registered server ID. + /// + public string? RegisteredServerId { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerListOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerListOptions.cs new file mode 100644 index 0000000000..b2f928a833 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerListOptions.cs @@ -0,0 +1,12 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for RegisteredServerListCommand. +/// +public class RegisteredServerListOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUnregisterOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUnregisterOptions.cs new file mode 100644 index 0000000000..84b0893781 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUnregisterOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for RegisteredServerUnregisterCommand. +/// +public class RegisteredServerUnregisterOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the registered server ID. + /// + public string? RegisteredServerId { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUpdateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUpdateOptions.cs new file mode 100644 index 0000000000..fd2688ef70 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/RegisteredServer/RegisteredServerUpdateOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for RegisteredServerUpdateCommand. +/// +public class RegisteredServerUpdateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the registered server ID. + /// + public string? RegisteredServerId { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointCreateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointCreateOptions.cs new file mode 100644 index 0000000000..c4c3a2aeb1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointCreateOptions.cs @@ -0,0 +1,32 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for ServerEndpointCreateCommand. +/// +public class ServerEndpointCreateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the server endpoint name. + /// + public string? ServerEndpointName { get; set; } + + /// + /// Gets or sets the server resource ID. + /// + public string? ServerResourceId { get; set; } + + /// + /// Gets or sets the server local path. + /// + public string? ServerLocalPath { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointDeleteOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointDeleteOptions.cs new file mode 100644 index 0000000000..5545870849 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointDeleteOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for ServerEndpointDeleteCommand. +/// +public class ServerEndpointDeleteOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the server endpoint name. + /// + public string? ServerEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointGetOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointGetOptions.cs new file mode 100644 index 0000000000..fbaf5db784 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointGetOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for ServerEndpointGetCommand. +/// +public class ServerEndpointGetOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the server endpoint name. + /// + public string? ServerEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointListOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointListOptions.cs new file mode 100644 index 0000000000..e76868bb0d --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointListOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for ServerEndpointListCommand. +/// +public class ServerEndpointListOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointUpdateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointUpdateOptions.cs new file mode 100644 index 0000000000..b727a38b8d --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/ServerEndpoint/ServerEndpointUpdateOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for ServerEndpointUpdateCommand. +/// +public class ServerEndpointUpdateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } + + /// + /// Gets or sets the server endpoint name. + /// + public string? ServerEndpointName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncOptionDefinitions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncOptionDefinitions.cs new file mode 100644 index 0000000000..1264660d4f --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncOptionDefinitions.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Static definitions for all Storage Sync command options. +/// Provides centralized option definitions used across commands. +/// +public static class StorageSyncOptionDefinitions +{ + /// + /// Storage Sync Service options. + /// + public static class StorageSyncService + { + public const string NameName = "name"; + public const string LocationName = "location"; + public const string IncomingTrafficPolicyName = "incoming-traffic-policy"; + public const string TagsName = "tags"; + + public static readonly Option Name = new($"--{NameName}", "-n") + { + Description = "The name of the storage sync service", + Required = true + }; + + public static readonly Option Location = new($"--{LocationName}", "-l") + { + Description = "The Azure region/location name (e.g., EastUS, WestEurope)", + Required = true + }; + + public static readonly Option IncomingTrafficPolicy = new($"--{IncomingTrafficPolicyName}") + { + Description = "Incoming traffic policy for the service (AllowAllTraffic or AllowVirtualNetworksOnly)" + }; + + public static readonly Option Tags = new($"--{TagsName}") + { + Description = "Tags to assign to the service (space-separated key=value pairs)" + }; + } + + /// + /// Sync Group options. + /// + public static class SyncGroup + { + public const string NameName = "sync-group-name"; + + public static readonly Option Name = new($"--{NameName}", "-sg") + { + Description = "The name of the sync group", + Required = true + }; + } + + /// + /// Cloud Endpoint options. + /// + public static class CloudEndpoint + { + public const string NameName = "cloud-endpoint-name"; + public const string StorageAccountResourceIdName = "storage-account-resource-id"; + public const string AzureFileShareNameName = "azure-file-share-name"; + public const string DirectoryPathName = "directory-path"; + public const string RecursiveName = "recursive"; + + public static readonly Option Name = new($"--{NameName}", "-ce") + { + Description = "The name of the cloud endpoint", + Required = true + }; + + public static readonly Option StorageAccountResourceId = new($"--{StorageAccountResourceIdName}") + { + Description = "The resource ID of the Azure storage account", + Required = true + }; + + public static readonly Option AzureFileShareName = new($"--{AzureFileShareNameName}") + { + Description = "The name of the Azure file share", + Required = true + }; + + public static readonly Option DirectoryPath = new($"--{DirectoryPathName}") + { + Description = "The directory path for change detection" + }; + + public static readonly Option Recursive = new($"--{RecursiveName}", "-r") + { + Description = "Recursively include subdirectories for change detection" + }; + } + + /// + /// Server Endpoint options. + /// + public static class ServerEndpoint + { + public const string NameName = "server-endpoint-name"; + public const string ServerResourceIdName = "server-resource-id"; + public const string ServerLocalPathName = "server-local-path"; + public const string CloudTieringName = "cloud-tiering"; + public const string VolumeFreeSpacePercentName = "volume-free-space-percent"; + public const string TierFilesOlderThanDaysName = "tier-files-older-than-days"; + + public static readonly Option Name = new($"--{NameName}", "-se") + { + Description = "The name of the server endpoint", + Required = true + }; + + public static readonly Option ServerResourceId = new($"--{ServerResourceIdName}") + { + Description = "The resource ID of the registered server", + Required = true + }; + + public static readonly Option ServerLocalPath = new($"--{ServerLocalPathName}") + { + Description = "The local folder path on the server for syncing", + Required = true + }; + + public static readonly Option CloudTiering = new($"--{CloudTieringName}", "-ct") + { + Description = "Enable cloud tiering on this endpoint" + }; + + public static readonly Option VolumeFreeSpacePercent = new($"--{VolumeFreeSpacePercentName}") + { + Description = "Volume free space percentage to maintain (1-99, default 20)" + }; + + public static readonly Option TierFilesOlderThanDays = new($"--{TierFilesOlderThanDaysName}") + { + Description = "Archive files not accessed for this many days" + }; + } + + /// + /// Registered Server options. + /// + public static class RegisteredServer + { + public const string Id = "server-id"; + public const string Name = "server-name"; + + public static readonly Option ServerId = new($"--{Id}") + { + Description = "The ID/name of the registered server", + Required = true + }; + + public static readonly Option ServerName = new($"--{Name}") + { + Description = "The name of the registered server", + Required = true + }; + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceCreateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceCreateOptions.cs new file mode 100644 index 0000000000..25fed567d8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceCreateOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for StorageSyncServiceCreateCommand. +/// +public class StorageSyncServiceCreateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the name of the storage sync service to create. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the location for the service. + /// + public string? Location { get; set; } + + /// + /// Gets or sets tags for the resource. + /// + public Dictionary? Tags { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceDeleteOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceDeleteOptions.cs new file mode 100644 index 0000000000..6c12d35bc9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceDeleteOptions.cs @@ -0,0 +1,12 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for StorageSyncServiceDeleteCommand. +/// +public class StorageSyncServiceDeleteOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the name of the storage sync service. + /// + public string? Name { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceGetOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceGetOptions.cs new file mode 100644 index 0000000000..6bb613243b --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceGetOptions.cs @@ -0,0 +1,12 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for StorageSyncServiceGetCommand. +/// +public class StorageSyncServiceGetOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the name of the storage sync service. + /// + public string? Name { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceListOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceListOptions.cs new file mode 100644 index 0000000000..29ded58aef --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceListOptions.cs @@ -0,0 +1,8 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for StorageSyncServiceListCommand. +/// +public class StorageSyncServiceListOptions : BaseStorageSyncOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceUpdateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceUpdateOptions.cs new file mode 100644 index 0000000000..5babd00f9c --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/StorageSyncService/StorageSyncServiceUpdateOptions.cs @@ -0,0 +1,22 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for StorageSyncServiceUpdateCommand. +/// +public class StorageSyncServiceUpdateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the name of the storage sync service. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the incoming traffic policy. + /// + public string? IncomingTrafficPolicy { get; set; } + + /// + /// Gets or sets tags for the resource. + /// + public Dictionary? Tags { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupCreateOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupCreateOptions.cs new file mode 100644 index 0000000000..3f15d1c396 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupCreateOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for SyncGroupCreateCommand. +/// +public class SyncGroupCreateOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupDeleteOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupDeleteOptions.cs new file mode 100644 index 0000000000..a145096257 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupDeleteOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for SyncGroupDeleteCommand. +/// +public class SyncGroupDeleteOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupGetOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupGetOptions.cs new file mode 100644 index 0000000000..b300b47801 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupGetOptions.cs @@ -0,0 +1,17 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for SyncGroupGetCommand. +/// +public class SyncGroupGetOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } + + /// + /// Gets or sets the sync group name. + /// + public string? SyncGroupName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupListOptions.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupListOptions.cs new file mode 100644 index 0000000000..1b25f196ec --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Options/SyncGroup/SyncGroupListOptions.cs @@ -0,0 +1,12 @@ +namespace Azure.Mcp.Tools.StorageSync.Options; + +/// +/// Options for SyncGroupListCommand. +/// +public class SyncGroupListOptions : BaseStorageSyncOptions +{ + /// + /// Gets or sets the storage sync service name. + /// + public string? StorageSyncServiceName { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Services/IStorageSyncService.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Services/IStorageSyncService.cs new file mode 100644 index 0000000000..3acf8ec90b --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Services/IStorageSyncService.cs @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.StorageSync.Models; + +namespace Azure.Mcp.Tools.StorageSync.Services; + +/// +/// Service interface for Storage Sync operations. +/// Defines all methods for managing Azure File Sync resources. +/// +public interface IStorageSyncService +{ + #region Storage Sync Service Operations + + /// + /// Lists all storage sync services in a subscription or resource group. + /// + Task> ListStorageSyncServicesAsync( + string subscription, + string? resourceGroup = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Gets a specific storage sync service. + /// + Task GetStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Creates a new storage sync service. + /// + Task CreateStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string location, + Dictionary? tags = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Updates a storage sync service. + /// + Task UpdateStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + Dictionary? properties = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a storage sync service. + /// + Task DeleteStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + #endregion + + #region Sync Group Operations + + /// + /// Lists all sync groups in a storage sync service. + /// + Task> ListSyncGroupsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Gets a specific sync group. + /// + Task GetSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Creates a new sync group. + /// + Task CreateSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a sync group. + /// + Task DeleteSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + #endregion + + #region Cloud Endpoint Operations + + /// + /// Lists all cloud endpoints in a sync group. + /// + Task> ListCloudEndpointsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Gets a specific cloud endpoint. + /// + Task GetCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Creates a new cloud endpoint. + /// + Task CreateCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string storageAccountResourceId, + string azureFileShareName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a cloud endpoint. + /// + Task DeleteCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Triggers change detection on a cloud endpoint. + /// + Task TriggerChangeDetectionAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? directoryPath = null, + string[]? filePaths = null, + bool recursive = false, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + #endregion + + #region Server Endpoint Operations + + /// + /// Lists all server endpoints in a sync group. + /// + Task> ListServerEndpointsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Gets a specific server endpoint. + /// + Task GetServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Creates a new server endpoint. + /// + Task CreateServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string serverResourceId, + string serverLocalPath, + bool enableCloudTiering = false, + int? volumeFreeSpacePercent = null, + int? tierFilesOlderThanDays = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Updates a server endpoint's configuration. + /// + Task UpdateServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + bool? enableCloudTiering = null, + int? volumeFreeSpacePercent = null, + int? tierFilesOlderThanDays = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a server endpoint. + /// + Task DeleteServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + #endregion + + #region Registered Server Operations + + /// + /// Lists all servers registered to a storage sync service. + /// + Task> ListRegisteredServersAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Gets a specific registered server. + /// + Task GetRegisteredServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Registers a new server to a storage sync service. + /// + Task RegisterServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Unregisters a server from a storage sync service. + /// + Task UnregisterServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + /// + /// Updates a registered server. + /// + Task UpdateServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + Dictionary? properties = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + + #endregion +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/Services/StorageSyncService.cs b/tools/Azure.Mcp.Tools.StorageSync/src/Services/StorageSyncService.cs new file mode 100644 index 0000000000..52afd47424 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/Services/StorageSyncService.cs @@ -0,0 +1,1103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.Mcp.Tools.StorageSync.Models; +using Azure.ResourceManager.StorageSync; +using Azure.ResourceManager.StorageSync.Models; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Tools.StorageSync.Services; + +/// +/// Implementation of IStorageSyncService. +/// +public sealed class StorageSyncService( + ISubscriptionService subscriptionService, + ITenantService tenantService, + ILogger logger) : BaseAzureResourceService(subscriptionService, tenantService), IStorageSyncService +{ + private readonly ILogger _logger = logger; + + public async Task> ListStorageSyncServicesAsync( + string subscription, + string? resourceGroup = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters((nameof(subscription), subscription)); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + + var services = new List(); + + if (!string.IsNullOrEmpty(resourceGroup)) + { + Azure.ResourceManager.Resources.ResourceGroupResource resourceGroupResource; + try + { + var response = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + resourceGroupResource = response.Value; + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, + "Resource group not found when listing Storage Sync services. ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + resourceGroup, subscription); + return []; + } + + var collection = resourceGroupResource.GetStorageSyncServices(); + await foreach (var serviceResource in collection) + { + services.Add(StorageSyncServiceDataSchema.FromResource(serviceResource)); + } + } + else + { + await foreach (var serviceResource in subscriptionResource.GetStorageSyncServicesAsync(cancellationToken)) + { + services.Add(StorageSyncServiceDataSchema.FromResource(serviceResource)); + } + } + + return services; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error listing Storage Sync services. ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + resourceGroup, subscription); + throw; + } + } + + public async Task GetStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + return StorageSyncServiceDataSchema.FromResource(serviceResource.Value); + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, + "Storage Sync service not found. Service: {Service}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + storageSyncServiceName, resourceGroup, subscription); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + storageSyncServiceName, resourceGroup, subscription); + throw; + } + } + + public async Task CreateStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string location, + Dictionary? tags = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(location), location) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.StorageSyncServiceCreateOrUpdateContent(new Azure.Core.AzureLocation(location)); + if (tags != null) + { + foreach (var tag in tags) + { + content.Tags.Add(tag.Key, tag.Value); + } + } + + var operation = await resourceGroupResource.Value.GetStorageSyncServices().CreateOrUpdateAsync( + WaitUntil.Completed, + storageSyncServiceName, + content, + cancellationToken); + + _logger.LogInformation( + "Successfully created Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}, Location: {Location}", + storageSyncServiceName, resourceGroup, location); + + return StorageSyncServiceDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error creating Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + storageSyncServiceName, resourceGroup, subscription); + throw; + } + } + + public async Task UpdateStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + Dictionary? properties = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + var patch = new Azure.ResourceManager.StorageSync.Models.StorageSyncServicePatch(); + if (properties != null) + { + if (properties.TryGetValue("tags", out var tagsObj) && tagsObj is Dictionary tags) + { + foreach (var tag in tags) + { + patch.Tags[tag.Key] = tag.Value; + } + } + } + + var operation = await serviceResource.Value.UpdateAsync(WaitUntil.Completed, patch, cancellationToken); + + _logger.LogInformation( + "Successfully updated Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}", + storageSyncServiceName, resourceGroup); + + return StorageSyncServiceDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error updating Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + storageSyncServiceName, resourceGroup, subscription); + throw; + } + } + + public async Task DeleteStorageSyncServiceAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + await serviceResource.Value.DeleteAsync(WaitUntil.Completed, cancellationToken); + + _logger.LogInformation( + "Successfully deleted Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}", + storageSyncServiceName, resourceGroup); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error deleting Storage Sync service. Service: {Service}, ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + storageSyncServiceName, resourceGroup, subscription); + throw; + } + } + + // Sync Group Operations + public async Task> ListSyncGroupsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + var syncGroups = new List(); + await foreach (var syncGroupResource in serviceResource.Value.GetStorageSyncGroups()) + { + syncGroups.Add(SyncGroupDataSchema.FromResource(syncGroupResource)); + } + + return syncGroups; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing Sync Groups"); + throw; + } + } + + public async Task GetSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + return SyncGroupDataSchema.FromResource(syncGroupResource.Value); + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, "Sync Group not found: {SyncGroup}", syncGroupName); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Sync Group: {SyncGroup}", syncGroupName); + throw; + } + } + + public async Task CreateSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.StorageSyncGroupCreateOrUpdateContent(); + var operation = await serviceResource.Value.GetStorageSyncGroups().CreateOrUpdateAsync(WaitUntil.Completed, syncGroupName, content, cancellationToken); + + _logger.LogInformation("Successfully created Sync Group: {SyncGroup}", syncGroupName); + return SyncGroupDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Sync Group: {SyncGroup}", syncGroupName); + throw; + } + } + + public async Task DeleteSyncGroupAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + await syncGroupResource.Value.DeleteAsync(WaitUntil.Completed, cancellationToken); + + _logger.LogInformation("Successfully deleted Sync Group: {SyncGroup}", syncGroupName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Sync Group: {SyncGroup}", syncGroupName); + throw; + } + } + + // Cloud Endpoint Operations + public async Task> ListCloudEndpointsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + var endpoints = new List(); + await foreach (var endpointResource in syncGroupResource.Value.GetCloudEndpoints()) + { + endpoints.Add(CloudEndpointDataSchema.FromResource(endpointResource)); + } + + return endpoints; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing Cloud Endpoints"); + throw; + } + } + + public async Task GetCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(cloudEndpointName), cloudEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetCloudEndpoints().GetAsync(cloudEndpointName, cancellationToken); + + return CloudEndpointDataSchema.FromResource(endpointResource.Value); + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, "Cloud Endpoint not found: {Endpoint}", cloudEndpointName); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Cloud Endpoint: {Endpoint}", cloudEndpointName); + throw; + } + } + + public async Task CreateCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string storageAccountResourceId, + string azureFileShareName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(cloudEndpointName), cloudEndpointName), + (nameof(storageAccountResourceId), storageAccountResourceId), + (nameof(azureFileShareName), azureFileShareName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.CloudEndpointCreateOrUpdateContent + { + StorageAccountResourceId = new Azure.Core.ResourceIdentifier(storageAccountResourceId), + AzureFileShareName = azureFileShareName + }; + var operation = await syncGroupResource.Value.GetCloudEndpoints().CreateOrUpdateAsync( + WaitUntil.Completed, cloudEndpointName, content, cancellationToken); + + _logger.LogInformation("Successfully created Cloud Endpoint: {Endpoint}", cloudEndpointName); + return CloudEndpointDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Cloud Endpoint: {Endpoint}", cloudEndpointName); + throw; + } + } + + public async Task DeleteCloudEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(cloudEndpointName), cloudEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetCloudEndpoints().GetAsync(cloudEndpointName, cancellationToken); + + await endpointResource.Value.DeleteAsync(WaitUntil.Completed, cancellationToken); + + _logger.LogInformation("Successfully deleted Cloud Endpoint: {Endpoint}", cloudEndpointName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Cloud Endpoint: {Endpoint}", cloudEndpointName); + throw; + } + } + + public async Task TriggerChangeDetectionAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string cloudEndpointName, + string? directoryPath = null, + string[]? filePaths = null, + bool recursive = false, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(cloudEndpointName), cloudEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetCloudEndpoints().GetAsync(cloudEndpointName, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.TriggerChangeDetectionContent + { + DirectoryPath = directoryPath, + ChangeDetectionMode = recursive ? Azure.ResourceManager.StorageSync.Models.ChangeDetectionMode.Recursive : Azure.ResourceManager.StorageSync.Models.ChangeDetectionMode.Default + }; + + if (filePaths != null) + { + foreach (var path in filePaths) + { + content.Paths.Add(path); + } + } + + await endpointResource.Value.TriggerChangeDetectionAsync(WaitUntil.Completed, content, cancellationToken); + + _logger.LogInformation("Successfully triggered change detection for Cloud Endpoint: {Endpoint}", cloudEndpointName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error triggering change detection for Cloud Endpoint: {Endpoint}", cloudEndpointName); + throw; + } + } + + // Server Endpoint Operations + public async Task> ListServerEndpointsAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + var endpoints = new List(); + await foreach (var endpointResource in syncGroupResource.Value.GetStorageSyncServerEndpoints()) + { + endpoints.Add(ServerEndpointDataSchema.FromResource(endpointResource)); + } + + return endpoints; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing Server Endpoints"); + throw; + } + } + + public async Task GetServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(serverEndpointName), serverEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetStorageSyncServerEndpoints().GetAsync(serverEndpointName, cancellationToken); + + return ServerEndpointDataSchema.FromResource(endpointResource.Value); + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, "Server Endpoint not found: {Endpoint}", serverEndpointName); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Server Endpoint: {Endpoint}", serverEndpointName); + throw; + } + } + + public async Task CreateServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string serverResourceId, + string serverLocalPath, + bool enableCloudTiering = false, + int? volumeFreeSpacePercent = null, + int? tierFilesOlderThanDays = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(serverEndpointName), serverEndpointName), + (nameof(serverResourceId), serverResourceId), + (nameof(serverLocalPath), serverLocalPath) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.StorageSyncServerEndpointCreateOrUpdateContent + { + ServerResourceId = new Azure.Core.ResourceIdentifier(serverResourceId), + ServerLocalPath = serverLocalPath, + CloudTiering = enableCloudTiering ? Azure.ResourceManager.StorageSync.Models.StorageSyncFeatureStatus.On : Azure.ResourceManager.StorageSync.Models.StorageSyncFeatureStatus.Off, + VolumeFreeSpacePercent = volumeFreeSpacePercent, + TierFilesOlderThanDays = tierFilesOlderThanDays + }; + + var operation = await syncGroupResource.Value.GetStorageSyncServerEndpoints().CreateOrUpdateAsync( + WaitUntil.Completed, serverEndpointName, content, cancellationToken); + + _logger.LogInformation("Successfully created Server Endpoint: {Endpoint}", serverEndpointName); + return ServerEndpointDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating Server Endpoint: {Endpoint}", serverEndpointName); + throw; + } + } + + public async Task UpdateServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + bool? cloudTiering = null, + int? volumeFreeSpacePercent = null, + int? tierFilesOlderThanDays = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(serverEndpointName), serverEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetStorageSyncServerEndpoints().GetAsync(serverEndpointName, cancellationToken); + + var patch = new Azure.ResourceManager.StorageSync.Models.StorageSyncServerEndpointPatch(); + if (cloudTiering.HasValue) + { + patch.CloudTiering = cloudTiering.Value ? Azure.ResourceManager.StorageSync.Models.StorageSyncFeatureStatus.On : Azure.ResourceManager.StorageSync.Models.StorageSyncFeatureStatus.Off; + } + if (volumeFreeSpacePercent.HasValue) + { + patch.VolumeFreeSpacePercent = volumeFreeSpacePercent; + } + if (tierFilesOlderThanDays.HasValue) + { + patch.TierFilesOlderThanDays = tierFilesOlderThanDays; + } + + var operation = await endpointResource.Value.UpdateAsync(WaitUntil.Completed, patch, cancellationToken); + + _logger.LogInformation("Successfully updated Server Endpoint: {Endpoint}", serverEndpointName); + return ServerEndpointDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Server Endpoint: {Endpoint}", serverEndpointName); + throw; + } + } + + public async Task DeleteServerEndpointAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string syncGroupName, + string serverEndpointName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(syncGroupName), syncGroupName), + (nameof(serverEndpointName), serverEndpointName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var syncGroupResource = await serviceResource.Value.GetStorageSyncGroups().GetAsync(syncGroupName, cancellationToken); + var endpointResource = await syncGroupResource.Value.GetStorageSyncServerEndpoints().GetAsync(serverEndpointName, cancellationToken); + + await endpointResource.Value.DeleteAsync(WaitUntil.Completed, cancellationToken); + + _logger.LogInformation("Successfully deleted Server Endpoint: {Endpoint}", serverEndpointName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting Server Endpoint: {Endpoint}", serverEndpointName); + throw; + } + } + + // Registered Server Operations + public async Task> ListRegisteredServersAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName) + ); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + var servers = new List(); + await foreach (var serverResource in serviceResource.Value.GetStorageSyncRegisteredServers()) + { + servers.Add(RegisteredServerDataSchema.FromResource(serverResource)); + } + + return servers; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing Registered Servers"); + throw; + } + } + + public async Task GetRegisteredServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(registeredServerId), registeredServerId) + ); + + // Validate registeredServerId is a valid GUID + var serverGuid = CheckGuid(registeredServerId, nameof(registeredServerId)); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var serverResource = await serviceResource.Value.GetStorageSyncRegisteredServers().GetAsync(serverGuid, cancellationToken); + + return RegisteredServerDataSchema.FromResource(serverResource.Value); + } + catch (Azure.RequestFailedException reqEx) when (reqEx.Status == (int)HttpStatusCode.NotFound) + { + _logger.LogWarning(reqEx, "Registered Server not found: {Server}", registeredServerId); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting Registered Server: {Server}", registeredServerId); + throw; + } + } + + public async Task RegisterServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(registeredServerId), registeredServerId) + ); + + // Validate registeredServerId is a valid GUID + var serverGuid = CheckGuid(registeredServerId, nameof(registeredServerId)); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + + var content = new Azure.ResourceManager.StorageSync.Models.StorageSyncRegisteredServerCreateOrUpdateContent(); + var operation = await serviceResource.Value.GetStorageSyncRegisteredServers().CreateOrUpdateAsync( + WaitUntil.Completed, serverGuid, content, cancellationToken); + + _logger.LogInformation("Successfully registered Server: {Server}", registeredServerId); + return RegisteredServerDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering Server: {Server}", registeredServerId); + throw; + } + } + + public async Task UpdateServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + Dictionary? properties = null, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(registeredServerId), registeredServerId) + ); + + // Validate registeredServerId is a valid GUID + var serverGuid = CheckGuid(registeredServerId, nameof(registeredServerId)); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var serverResource = await serviceResource.Value.GetStorageSyncRegisteredServers().GetAsync(serverGuid, cancellationToken); + + var patch = new Azure.ResourceManager.StorageSync.Models.StorageSyncRegisteredServerPatch(); + // Add any patch-specific logic here if needed + + var operation = await serverResource.Value.UpdateAsync(WaitUntil.Completed, patch, cancellationToken); + + _logger.LogInformation("Successfully updated Registered Server: {Server}", registeredServerId); + return RegisteredServerDataSchema.FromResource(operation.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating Registered Server: {Server}", registeredServerId); + throw; + } + } + + public async Task UnregisterServerAsync( + string subscription, + string resourceGroup, + string storageSyncServiceName, + string registeredServerId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(storageSyncServiceName), storageSyncServiceName), + (nameof(registeredServerId), registeredServerId) + ); + + // Validate registeredServerId is a valid GUID + var serverGuid = CheckGuid(registeredServerId, nameof(registeredServerId)); + + try + { + var armClient = await CreateArmClientAsync(tenant, retryPolicy, null, cancellationToken); + var subscriptionResource = armClient.GetSubscriptionResource( + Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + var serviceResource = await resourceGroupResource.Value.GetStorageSyncServices().GetAsync(storageSyncServiceName, cancellationToken); + var serverResource = await serviceResource.Value.GetStorageSyncRegisteredServers().GetAsync(serverGuid, cancellationToken); + + await serverResource.Value.DeleteAsync(WaitUntil.Completed, cancellationToken); + + _logger.LogInformation("Successfully unregistered Server: {Server}", registeredServerId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error unregistering Server: {Server}", registeredServerId); + throw; + } + } + + /// + /// Validates and converts a string to a GUID. + /// + /// The string value to convert + /// The parameter name for error messages + /// The converted GUID + /// Thrown if the value is not a valid GUID + private static Guid CheckGuid(string value, string paramName) + { + if (!Guid.TryParse(value, out var guid)) + { + throw new ArgumentException($"'{paramName}' must be a valid GUID.", paramName); + } + + return guid; + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncJsonContext.cs b/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncJsonContext.cs new file mode 100644 index 0000000000..3b3b9c8682 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncJsonContext.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; + +namespace Azure.Mcp.Tools.StorageSync; + +/// +/// JSON serialization context for Storage Sync commands. +/// Required for AOT (Ahead-of-Time) compilation support. +/// +[JsonSerializable(typeof(StorageSyncServiceGetCommand.StorageSyncServiceGetCommandResult))] +[JsonSerializable(typeof(StorageSyncServiceCreateCommand.StorageSyncServiceCreateCommandResult))] +[JsonSerializable(typeof(StorageSyncServiceUpdateCommand.StorageSyncServiceUpdateCommandResult))] +[JsonSerializable(typeof(StorageSyncServiceDeleteCommand.StorageSyncServiceDeleteCommandResult))] +[JsonSerializable(typeof(RegisteredServerGetCommand.RegisteredServerGetCommandResult))] +[JsonSerializable(typeof(RegisteredServerUpdateCommand.RegisteredServerUpdateCommandResult))] +[JsonSerializable(typeof(RegisteredServerUnregisterCommand.RegisteredServerUnregisterCommandResult))] +[JsonSerializable(typeof(SyncGroupGetCommand.SyncGroupGetCommandResult))] +[JsonSerializable(typeof(SyncGroupCreateCommand.SyncGroupCreateCommandResult))] +[JsonSerializable(typeof(SyncGroupDeleteCommand.SyncGroupDeleteCommandResult))] +[JsonSerializable(typeof(CloudEndpointGetCommand.CloudEndpointGetCommandResult))] +[JsonSerializable(typeof(CloudEndpointCreateCommand.CloudEndpointCreateCommandResult))] +[JsonSerializable(typeof(CloudEndpointDeleteCommand.CloudEndpointDeleteCommandResult))] +[JsonSerializable(typeof(CloudEndpointTriggerChangeDetectionCommand.CloudEndpointTriggerChangeDetectionCommandResult))] +[JsonSerializable(typeof(ServerEndpointGetCommand.ServerEndpointGetCommandResult))] +[JsonSerializable(typeof(ServerEndpointCreateCommand.ServerEndpointCreateCommandResult))] +[JsonSerializable(typeof(ServerEndpointUpdateCommand.ServerEndpointUpdateCommandResult))] +[JsonSerializable(typeof(ServerEndpointDeleteCommand.ServerEndpointDeleteCommandResult))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +internal partial class StorageSyncJsonContext : JsonSerializerContext +{ +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncSetup.cs b/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncSetup.cs new file mode 100644 index 0000000000..8f4128a04a --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/src/StorageSyncSetup.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Mcp.Core.Areas; +using Microsoft.Mcp.Core.Commands; + +namespace Azure.Mcp.Tools.StorageSync; + +/// +/// Setup configuration for Azure Storage Sync MCP tools. +/// +public class StorageSyncSetup : IAreaSetup +{ + /// + /// Gets the namespace name for Storage Sync commands. + /// + public string Name => "storagesync"; + + /// + /// Gets the display title for the Storage Sync area. + /// + public string Title => "Manage Azure Storage Sync Services"; + + /// + /// Configures services for Storage Sync operations. + /// + public void ConfigureServices(IServiceCollection services) + { + // Register the service implementation + services.AddSingleton(); + + // Register StorageSyncService commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register RegisteredServer commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register SyncGroup commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register CloudEndpoint commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register ServerEndpoint commands + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + /// + /// Registers all Storage Sync commands into the command hierarchy. + /// + public CommandGroup RegisterCommands(IServiceProvider serviceProvider) + { + var storageSync = new CommandGroup(Name, + """ + Azure Storage Sync operations - Commands for managing Azure File Sync services, sync groups, cloud endpoints, + server endpoints, and registered servers. Use this tool to deploy, configure, and manage File Sync infrastructure + for hybrid cloud file synchronization scenarios. The tool supports listing, creating, updating, and deleting + resources across the Storage Sync service hierarchy. Each command requires appropriate Azure permissions and + subscription access. + """, + Title); + + // StorageSyncService subgroup + var storageSyncServiceGroup = new CommandGroup("service", + "Storage Sync Service operations - Create, get, update, and delete Storage Sync services in your Azure subscription."); + storageSync.AddSubGroup(storageSyncServiceGroup); + + storageSyncServiceGroup.AddCommand("get", serviceProvider.GetRequiredService()); + storageSyncServiceGroup.AddCommand("create", serviceProvider.GetRequiredService()); + storageSyncServiceGroup.AddCommand("update", serviceProvider.GetRequiredService()); + storageSyncServiceGroup.AddCommand("delete", serviceProvider.GetRequiredService()); + + // RegisteredServer subgroup + var registeredServerGroup = new CommandGroup("registeredserver", + "Registered Server operations - Get, update, and unregister servers in your Storage Sync service."); + storageSync.AddSubGroup(registeredServerGroup); + + registeredServerGroup.AddCommand("get", serviceProvider.GetRequiredService()); + registeredServerGroup.AddCommand("update", serviceProvider.GetRequiredService()); + registeredServerGroup.AddCommand("unregister", serviceProvider.GetRequiredService()); + + // SyncGroup subgroup + var syncGroupGroup = new CommandGroup("syncgroup", + "Sync Group operations - Create, get, and delete sync groups in your Storage Sync service."); + storageSync.AddSubGroup(syncGroupGroup); + + syncGroupGroup.AddCommand("get", serviceProvider.GetRequiredService()); + syncGroupGroup.AddCommand("create", serviceProvider.GetRequiredService()); + syncGroupGroup.AddCommand("delete", serviceProvider.GetRequiredService()); + + // CloudEndpoint subgroup + var cloudEndpointGroup = new CommandGroup("cloudendpoint", + "Cloud Endpoint operations - Create, get, delete, and manage cloud endpoints in your sync groups."); + storageSync.AddSubGroup(cloudEndpointGroup); + + cloudEndpointGroup.AddCommand("get", serviceProvider.GetRequiredService()); + cloudEndpointGroup.AddCommand("create", serviceProvider.GetRequiredService()); + cloudEndpointGroup.AddCommand("delete", serviceProvider.GetRequiredService()); + cloudEndpointGroup.AddCommand("triggerChangeDetection", serviceProvider.GetRequiredService()); + + // ServerEndpoint subgroup + var serverEndpointGroup = new CommandGroup("serverendpoint", + "Server Endpoint operations - Create, get, update, and delete server endpoints in your sync groups."); + storageSync.AddSubGroup(serverEndpointGroup); + + serverEndpointGroup.AddCommand("get", serviceProvider.GetRequiredService()); + serverEndpointGroup.AddCommand("create", serviceProvider.GetRequiredService()); + serverEndpointGroup.AddCommand("update", serviceProvider.GetRequiredService()); + serverEndpointGroup.AddCommand("delete", serviceProvider.GetRequiredService()); + + return storageSync; + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/Azure.Mcp.Tools.StorageSync.LiveTests.csproj b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/Azure.Mcp.Tools.StorageSync.LiveTests.csproj new file mode 100644 index 0000000000..0f06a032a0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/Azure.Mcp.Tools.StorageSync.LiveTests.csproj @@ -0,0 +1,17 @@ + + + true + Exe + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/StorageSyncCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/StorageSyncCommandTests.cs new file mode 100644 index 0000000000..4de0f02c74 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/StorageSyncCommandTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Tests; +using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Attributes; +using Azure.Mcp.Tests.Client.Helpers; +using Azure.Mcp.Tests.Generated.Models; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.LiveTests; + +public class StorageSyncCommandTests(ITestOutputHelper output, TestProxyFixture fixture) : RecordedCommandTestsBase(output, fixture) +{ + public override List BodyRegexSanitizers => [ + // Sanitizes all URLs to remove actual service names + new BodyRegexSanitizer(new BodyRegexSanitizerBody() { + Regex = "(?<=http://|https://)(?[^/?\\.]+)", + GroupForReplace = "host", + }) + ]; + + [Fact] + public async Task Should_list_storage_sync_services() + { + var result = await CallToolAsync( + "storagesync_service_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName } + }); + + var services = result.AssertProperty("results"); + Assert.Equal(JsonValueKind.Array, services.ValueKind); + } + + [Fact] + public async Task Should_get_storage_sync_service() + { + var result = await CallToolAsync( + "storagesync_service_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "name", Settings.ResourceBaseName } + }); + + var service = result.AssertProperty("result"); + Assert.NotEqual(JsonValueKind.Null, service.ValueKind); + } + + [Fact] + public async Task Should_list_sync_groups() + { + var result = await CallToolAsync( + "storagesync_syncgroup_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "name", Settings.ResourceBaseName } + }); + + var syncGroups = result.AssertProperty("results"); + Assert.Equal(JsonValueKind.Array, syncGroups.ValueKind); + } + + [Fact] + public async Task Should_list_cloud_endpoints() + { + var result = await CallToolAsync( + "storagesync_cloudendpoint_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "name", Settings.ResourceBaseName }, + { "sync-group-name", $"{Settings.ResourceBaseName}-sg" } + }); + + var endpoints = result.AssertProperty("results"); + Assert.Equal(JsonValueKind.Array, endpoints.ValueKind); + } + + [Fact] + public async Task Should_list_registered_servers() + { + var result = await CallToolAsync( + "storagesync_registeredserver_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "name", Settings.ResourceBaseName } + }); + + //var servers = result.AssertProperty("results"); + // TODO : Waiting on Service backend QFE fix + // Assert.Equal(JsonValueKind.Array, servers.ValueKind); + } + + [Fact] + public async Task Should_list_server_endpoints() + { + var result = await CallToolAsync( + "storagesync_serverendpoint_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "name", Settings.ResourceBaseName }, + { "sync-group-name", $"{Settings.ResourceBaseName}-sg" } + }); + + var endpoints = result.AssertProperty("results"); + Assert.Equal(JsonValueKind.Array, endpoints.ValueKind); + } +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/assets.json b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/assets.json new file mode 100644 index 0000000000..14a71952bd --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.StorageSync.LiveTests", + "Tag": "Azure.Mcp.Tools.StorageSync.LiveTests_6248b14302" +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Azure.Mcp.Tools.StorageSync.UnitTests.csproj b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Azure.Mcp.Tools.StorageSync.UnitTests.csproj new file mode 100644 index 0000000000..9a1a21603b --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Azure.Mcp.Tools.StorageSync.UnitTests.csproj @@ -0,0 +1,17 @@ + + + true + Exe + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointCreateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointCreateCommandTests.cs new file mode 100644 index 0000000000..cb838c89a1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointCreateCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.CloudEndpoint; + +public class CloudEndpointCreateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly CloudEndpointCreateCommand _command; + + public CloudEndpointCreateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("create", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("create", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointDeleteCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointDeleteCommandTests.cs new file mode 100644 index 0000000000..9e341ce04b --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointDeleteCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.CloudEndpoint; + +public class CloudEndpointDeleteCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly CloudEndpointDeleteCommand _command; + + public CloudEndpointDeleteCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("delete", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("delete", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointGetCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointGetCommandTests.cs new file mode 100644 index 0000000000..efa52562a5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointGetCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.CloudEndpoint; + +public class CloudEndpointGetCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly CloudEndpointGetCommand _command; + + public CloudEndpointGetCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("get", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("get", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommandTests.cs new file mode 100644 index 0000000000..b0ed953547 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/CloudEndpoint/CloudEndpointTriggerChangeDetectionCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.CloudEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.CloudEndpoint; + +public class CloudEndpointTriggerChangeDetectionCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly CloudEndpointTriggerChangeDetectionCommand _command; + + public CloudEndpointTriggerChangeDetectionCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("triggerchangedetection", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("triggerchangedetection", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerGetCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerGetCommandTests.cs new file mode 100644 index 0000000000..823e896e63 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerGetCommandTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.RegisteredServer; + +public class RegisteredServerGetCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly RegisteredServerGetCommand _command; + + public RegisteredServerGetCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("get", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("get", _command.Name); + } +} + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUnregisterCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUnregisterCommandTests.cs new file mode 100644 index 0000000000..612f3c4d2c --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUnregisterCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.RegisteredServer; + +public class RegisteredServerUnregisterCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly RegisteredServerUnregisterCommand _command; + + public RegisteredServerUnregisterCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("unregister", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("unregister", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUpdateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUpdateCommandTests.cs new file mode 100644 index 0000000000..b2fc7aff18 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/RegisteredServer/RegisteredServerUpdateCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.RegisteredServer; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.RegisteredServer; + +public class RegisteredServerUpdateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly RegisteredServerUpdateCommand _command; + + public RegisteredServerUpdateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("update", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("update", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointCreateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointCreateCommandTests.cs new file mode 100644 index 0000000000..22a0024290 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointCreateCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.ServerEndpoint; + +public class ServerEndpointCreateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly ServerEndpointCreateCommand _command; + + public ServerEndpointCreateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("create", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("create", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointDeleteCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointDeleteCommandTests.cs new file mode 100644 index 0000000000..f7205dd0d8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointDeleteCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.ServerEndpoint; + +public class ServerEndpointDeleteCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly ServerEndpointDeleteCommand _command; + + public ServerEndpointDeleteCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("delete", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("delete", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointGetCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointGetCommandTests.cs new file mode 100644 index 0000000000..5297cb5547 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointGetCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.ServerEndpoint; + +public class ServerEndpointGetCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly ServerEndpointGetCommand _command; + + public ServerEndpointGetCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("get", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("get", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointUpdateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointUpdateCommandTests.cs new file mode 100644 index 0000000000..d19fbdd005 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/ServerEndpoint/ServerEndpointUpdateCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.ServerEndpoint; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.ServerEndpoint; + +public class ServerEndpointUpdateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly ServerEndpointUpdateCommand _command; + + public ServerEndpointUpdateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("update", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("update", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceCreateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceCreateCommandTests.cs new file mode 100644 index 0000000000..df4293514a --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceCreateCommandTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.StorageSyncService; + +/// +/// Unit tests for StorageSyncServiceCreateCommand. +/// +public class StorageSyncServiceCreateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly StorageSyncServiceCreateCommand _command; + + public StorageSyncServiceCreateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("create", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("create", _command.Name); + } + + [Fact] + public void Title_ReturnsCorrectValue() + { + Assert.Equal("Create Storage Sync Service", _command.Title); + } +} + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceDeleteCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceDeleteCommandTests.cs new file mode 100644 index 0000000000..a4dc9588da --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceDeleteCommandTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.StorageSyncService; + +public class StorageSyncServiceDeleteCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly StorageSyncServiceDeleteCommand _command; + + public StorageSyncServiceDeleteCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("delete", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("delete", _command.Name); + } +} + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceGetCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceGetCommandTests.cs new file mode 100644 index 0000000000..dc75e8ef6a --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceGetCommandTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.StorageSyncService; + +public class StorageSyncServiceGetCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly StorageSyncServiceGetCommand _command; + + public StorageSyncServiceGetCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("get", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("get", _command.Name); + } +} + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceUpdateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceUpdateCommandTests.cs new file mode 100644 index 0000000000..7299f402df --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/StorageSyncService/StorageSyncServiceUpdateCommandTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.StorageSyncService; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.StorageSyncService; + +public class StorageSyncServiceUpdateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly StorageSyncServiceUpdateCommand _command; + + public StorageSyncServiceUpdateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("update", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("update", _command.Name); + } +} + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupCreateCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupCreateCommandTests.cs new file mode 100644 index 0000000000..3ad2d5e336 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupCreateCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.SyncGroup; + +public class SyncGroupCreateCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly SyncGroupCreateCommand _command; + + public SyncGroupCreateCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("create", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("create", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupDeleteCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupDeleteCommandTests.cs new file mode 100644 index 0000000000..7d9b5ebd87 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupDeleteCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.SyncGroup; + +public class SyncGroupDeleteCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly SyncGroupDeleteCommand _command; + + public SyncGroupDeleteCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("delete", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("delete", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupGetCommandTests.cs b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupGetCommandTests.cs new file mode 100644 index 0000000000..8c0d5cb9cf --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/Azure.Mcp.Tools.StorageSync.UnitTests/Commands/SyncGroup/SyncGroupGetCommandTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.StorageSync.Commands.SyncGroup; +using Azure.Mcp.Tools.StorageSync.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.StorageSync.UnitTests.Commands.SyncGroup; + +public class SyncGroupGetCommandTests +{ + private readonly IStorageSyncService _service; + private readonly ILogger _logger; + private readonly SyncGroupGetCommand _command; + + public SyncGroupGetCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + _command = new(_logger, _service); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.NotNull(command); + Assert.Equal("get", command.Name); + } + + [Fact] + public void Name_ReturnsCorrectValue() + { + Assert.Equal("get", _command.Name); + } +} + + diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources-post.ps1 new file mode 100644 index 0000000000..3b6e4ed0a2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources-post.ps1 @@ -0,0 +1,110 @@ +param( + [string] $TenantId, + [string] $TestApplicationId, + [string] $ResourceGroupName, + [string] $BaseName, + [hashtable] $DeploymentOutputs +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/../../../eng/common/scripts/common.ps1" +. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1" + +$testSettings = New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot + +$storageSyncServiceName = $BaseName + +Write-Host "Setting up Storage Sync Service for testing: $storageSyncServiceName" -ForegroundColor Yellow + +try { + # Check if Storage Sync Service exists + $storageSyncService = Get-AzStorageSyncService -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -ErrorAction SilentlyContinue + + if (-not $storageSyncService) { + Write-Warning "Storage Sync Service '$storageSyncServiceName' not found in resource group '$ResourceGroupName'" + return + } + + Write-Host "Storage Sync Service found: $($storageSyncService.Id)" -ForegroundColor Green + + # Check if running as administrator + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if (-not $isAdmin) { + $errorMessage = @" +ERROR: Register server failed due to insufficient privileges. Run this command instead: + +Start-Process pwsh -Verb RunAs -ArgumentList "-NoExit -Command cd $PSScriptRoot\..\..\..; ./eng/scripts/Deploy-TestResources.ps1 -Paths StorageSync" +"@ + Write-Error $errorMessage -ErrorAction Stop + } + + # Import Storage Sync module and reset server + Import-Module "C:\Program Files\Azure\StorageSyncAgent\StorageSync.Management.ServerCmdlets.dll" + Reset-StorageSyncServer -Force -ErrorAction SilentlyContinue + + # Register a RegisteredServer (Note: This requires the Storage Sync Agent to be installed on a server) + $registeredServer = $storageSyncService | Register-AzStorageSyncServer + Write-Host "Attempted to register server with Storage Sync Service (requires Storage Sync Agent installed)" -ForegroundColor Gray + + # Get Sync Group + $syncGroupName = "$BaseName-sg" + $syncGroup = Get-AzStorageSyncGroup -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -SyncGroupName $syncGroupName -ErrorAction SilentlyContinue + + if ($syncGroup) { + Write-Host "Sync Group found: $syncGroupName" -ForegroundColor Green + } + else { + Write-Warning "Sync Group '$syncGroupName' not found" + } + + # Get Cloud Endpoint if it exists + $cloudEndpointName = "$BaseName-ce" + $cloudEndpoint = Get-AzStorageSyncCloudEndpoint -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -SyncGroupName $syncGroupName -Name $cloudEndpointName -ErrorAction SilentlyContinue + + if ($cloudEndpoint) { + Write-Host "Cloud Endpoint found: $cloudEndpointName" -ForegroundColor Green + Write-Host " - Azure File Share: $($cloudEndpoint.AzureFileShareName)" -ForegroundColor Gray + Write-Host " - Status: $($cloudEndpoint.LastOperationName)" -ForegroundColor Gray + } + else { + Write-Host "Cloud Endpoint '$cloudEndpointName' not yet available (this is normal during initial setup)" -ForegroundColor Yellow + } + + # Get Registered Server if it exists + $registeredServerId = $registeredServer.ServerId + $registeredServer = Get-AzStorageSyncServer -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -ServerId $registeredServerId -ErrorAction SilentlyContinue + + if ($registeredServer) { + Write-Host "Registered Server found: $registeredServerId" -ForegroundColor Green + Write-Host " - Server Id: $($registeredServer.ServerId)" -ForegroundColor Gray + Write-Host " - Friendly Name: $($registeredServer.FriendlyName)" -ForegroundColor Gray + } + else { + Write-Host "Registered Server '$registeredServerId' not yet available (requires Storage Sync Agent)" -ForegroundColor Yellow + } + + # create a new server endpoint if needed + $serverEndpointName = "$BaseName-sep" + $serverLocalPath = "D:\$serverEndpointName" + + New-AzStorageSyncServerEndpoint -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -SyncGroupName $syncGroupName -Name $serverEndpointName -ServerResourceId $registeredServer.ResourceId -ServerLocalPath $serverLocalPath -ErrorAction SilentlyContinue | Out-Null + + # Get Server Endpoint if it exists + $serverEndpoint = Get-AzStorageSyncServerEndpoint -ResourceGroupName $ResourceGroupName -StorageSyncServiceName $storageSyncServiceName -SyncGroupName $syncGroupName -Name $serverEndpointName -ErrorAction SilentlyContinue + + if ($serverEndpoint) { + Write-Host "Server Endpoint found: $serverEndpointName" -ForegroundColor Green + Write-Host " - Server Local Path: $($serverEndpoint.ServerLocalPath)" -ForegroundColor Gray + Write-Host " - Cloud Tiering: $($serverEndpoint.CloudTiering)" -ForegroundColor Gray + } + else { + Write-Host "Server Endpoint '$serverEndpointName' not yet available (requires active registered server)" -ForegroundColor Yellow + } + + Write-Host "Storage Sync Service setup completed successfully" -ForegroundColor Green +} +catch { + Write-Error "Error setting up Storage Sync Service: $_" -ErrorAction Stop +} diff --git a/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources.bicep new file mode 100644 index 0000000000..ee64a1f7c7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.StorageSync/tests/test-resources.bicep @@ -0,0 +1,93 @@ +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(24) +@description('The base resource name.') +param baseName string = resourceGroup().name + +@description('The location of the resource. By default, this is the same as the resource group.') +param location string = resourceGroup().location + +@description('The storage account name for cloud endpoint.') +param storageAccountName string = 'samcpstoragesync' + +// Storage Sync Service +resource storageSyncService 'Microsoft.StorageSync/storageSyncServices@2022-06-01' = { + name: baseName + location: location + properties: { + incomingTrafficPolicy: 'AllowAllTraffic' + } +} + +// Sync Group +resource syncGroup 'Microsoft.StorageSync/storageSyncServices/syncGroups@2022-06-01' = { + name: '${baseName}-sg-${uniqueString(resourceGroup().id)}' + parent: storageSyncService + properties: { + } +} + +// Storage Account +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageAccountName + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + } + } +} + +// Role Assignment - Reader and Data Access +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, '9469b9f5-6722-4481-a2b2-14ed560b706f') + scope: storageAccount + properties: { + principalId: '9469b9f5-6722-4481-a2b2-14ed560b706f' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349') + principalType: 'ServicePrincipal' + } +} + +// File Share +resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-01-01' = { + name: '${storageAccount.name}/default/${baseName}-share-${substring(uniqueString(resourceGroup().id), 0, 6)}' + properties: { + accessTier: 'TransactionOptimized' + shareQuota: 100 + } +} + +// Cloud Endpoint +resource cloudEndpoint 'Microsoft.StorageSync/storageSyncServices/syncGroups/cloudEndpoints@2022-06-01' = { + name: '${baseName}-ce' + parent: syncGroup + properties: { + storageAccountResourceId: storageAccount.id + azureFileShareName: '${baseName}-share-${substring(uniqueString(resourceGroup().id), 0, 6)}' + storageAccountTenantId: subscription().tenantId + } + dependsOn: [ + fileShare + ] +} + +// Outputs for testing +output storageSyncServiceName string = storageSyncService.name +output storageSyncServiceId string = storageSyncService.id +output syncGroupName string = syncGroup.name +output syncGroupId string = syncGroup.id +output storageAccountName string = storageAccount.name +output storageAccountId string = storageAccount.id +output fileShareName string = '${baseName}-share-${substring(uniqueString(resourceGroup().id), 0, 6)}' +output cloudEndpointName string = cloudEndpoint.name +output cloudEndpointId string = cloudEndpoint.id