diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Azure.Mcp.Tools.AzureBestPractices.csproj b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Azure.Mcp.Tools.AzureBestPractices.csproj index f453c9bf32..634302fc23 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Azure.Mcp.Tools.AzureBestPractices.csproj +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Azure.Mcp.Tools.AzureBestPractices.csproj @@ -7,12 +7,11 @@ - + - \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs index a76454e574..15598ef9c2 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs @@ -26,7 +26,7 @@ namespace Azure.Mcp.Tools.AzureBestPractices.Commands; ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class AIAppBestPracticesCommand(ILogger logger) : BaseCommand +public sealed class AIAppBestPracticesCommand(ILogger logger) : BaseCommand> { private readonly ILogger _logger = logger; private static readonly string s_bestPracticesText = LoadBestPracticesText(); @@ -51,9 +51,7 @@ private static string LoadEmbeddedText(string fileName) return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); } - protected override EmptyOptions BindOptions(ParseResult parseResult) => new(); - - public override Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + public override Task ExecuteAsync(CommandContext context, EmptyOptions options, CancellationToken cancellationToken) { try { diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs index a0df574519..abb3090f5e 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs @@ -7,7 +7,4 @@ namespace Azure.Mcp.Tools.AzureBestPractices.Commands; [JsonSerializable(typeof(List))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -internal partial class AzureBestPracticesJsonContext : JsonSerializerContext -{ - -} +internal partial class AzureBestPracticesJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs index f1e6ae6c13..0e73a485d2 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Concurrent; using System.Net; using System.Reflection; using System.Text; using Azure.Mcp.Tools.AzureBestPractices.Options; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Commands; -using Microsoft.Mcp.Core.Extensions; using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Models.Command; @@ -32,70 +32,48 @@ it belongs to the Azure Best Practices category. ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class BestPracticesCommand(ILogger logger) : BaseCommand +public sealed class BestPracticesCommand(ILogger logger) : BaseCommand> { private readonly ILogger _logger = logger; - private static readonly Dictionary s_bestPracticesCache = new(); + private static readonly ConcurrentDictionary s_bestPracticesCache = []; - protected override void RegisterOptions(Command command) + public override void ValidateOptions(BestPracticesOptions options, ValidationResult validationResult) { - command.Options.Add(BestPracticesOptionDefinitions.Resource); - command.Options.Add(BestPracticesOptionDefinitions.Action); - command.Validators.Add(commandResult => + base.ValidateOptions(options, validationResult); + + if (string.IsNullOrWhiteSpace(options.Resource) || string.IsNullOrWhiteSpace(options.Action)) + { + validationResult.Errors.Add("Both resource and action parameters are required."); + } + else { - commandResult.TryGetValue(BestPracticesOptionDefinitions.Resource, out string? resource); - commandResult.TryGetValue(BestPracticesOptionDefinitions.Action, out string? action); + bool validResource = options.Resource == "general" || options.Resource == "azurefunctions" || options.Resource == "static-web-app" || options.Resource == "coding-agent"; + bool validAction = options.Action == "all" || options.Action == "code-generation" || options.Action == "deployment"; - if (string.IsNullOrWhiteSpace(resource) || string.IsNullOrWhiteSpace(action)) + if (!validResource) { - commandResult.AddError("Both resource and action parameters are required."); + validationResult.Errors.Add("Invalid resource. Must be 'general', 'azurefunctions', 'static-web-app', or 'coding-agent'."); } - else + if (!validAction) { - bool validResource = resource == "general" || resource == "azurefunctions" || resource == "static-web-app" || resource == "coding-agent"; - bool validAction = action == "all" || action == "code-generation" || action == "deployment"; - - if (!validResource) - { - commandResult.AddError("Invalid resource. Must be 'general', 'azurefunctions', 'static-web-app', or 'coding-agent'."); - } - if (!validAction) - { - commandResult.AddError("Invalid action. Must be 'all', 'code-generation' or 'deployment'."); - } - if (resource == "static-web-app" && action != "all") - { - commandResult.AddError("The 'static-web-app' resource only supports 'all' action."); - } - if (resource == "coding-agent" && action != "all") - { - commandResult.AddError("The 'coding-agent' resource only supports 'all' action."); - } + validationResult.Errors.Add("Invalid action. Must be 'all', 'code-generation' or 'deployment'."); } - }); - } - - protected override BestPracticesOptions BindOptions(ParseResult parseResult) - { - return new BestPracticesOptions - { - Resource = parseResult.GetValueOrDefault(BestPracticesOptionDefinitions.Resource.Name), - Action = parseResult.GetValueOrDefault(BestPracticesOptionDefinitions.Action.Name) - }; + if (options.Resource == "static-web-app" && options.Action != "all") + { + validationResult.Errors.Add("The 'static-web-app' resource only supports 'all' action."); + } + if (options.Resource == "coding-agent" && options.Action != "all") + { + validationResult.Errors.Add("The 'coding-agent' resource only supports 'all' action."); + } + } } - public override Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + public override Task ExecuteAsync(CommandContext context, BestPracticesOptions options, CancellationToken cancellationToken) { - if (!Validate(parseResult.CommandResult, context.Response).IsValid) - { - return Task.FromResult(context.Response); - } - - var options = BindOptions(parseResult); - try { - var resourceFileName = GetResourceFileName(options.Resource!, options.Action!); + var resourceFileName = GetResourceFileName(options.Resource, options.Action); var bestPractices = GetBestPracticesText(resourceFileName); context.Response.Status = HttpStatusCode.OK; diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/GlobalUsings.cs deleted file mode 100644 index b41cc886b4..0000000000 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/GlobalUsings.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -global using System.CommandLine; diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptionDefinitions.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptionDefinitions.cs deleted file mode 100644 index 863a405761..0000000000 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptionDefinitions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Azure.Mcp.Tools.AzureBestPractices.Options; - -public static class BestPracticesOptionDefinitions -{ - public const string ResourceName = "resource"; - public const string ActionName = "action"; - - public static readonly Option Resource = new( - $"--{ResourceName}" - ) - { - Description = "The Azure resource type for which to get best practices. Options: 'general' (general Azure), 'azurefunctions' (Azure Functions), 'static-web-app' (Azure Static Web Apps), 'coding-agent' (Coding Agent).", - Required = true - }; - - public static readonly Option Action = new( - $"--{ActionName}" - ) - { - Description = "The action type for the best practices. Options: 'all', 'code-generation', 'deployment'. Note: 'static-web-app' and 'coding-agent' resources only supports 'all'.", - Required = true - }; -} diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptions.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptions.cs index 9e2fedba01..1dc720dd59 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptions.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Options/BestPracticesOptions.cs @@ -1,10 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Mcp.Core.Options; + namespace Azure.Mcp.Tools.AzureBestPractices.Options; public sealed class BestPracticesOptions { - public string? Resource { get; set; } - public string? Action { get; set; } + [Option(Description = "The Azure resource type for which to get best practices. Options: 'general' (general Azure), 'azurefunctions' (Azure Functions), 'static-web-app' (Azure Static Web Apps), 'coding-agent' (Coding Agent).")] + public required string Resource { get; set; } + + [Option(Description = "The action type for the best practices. Options: 'all', 'code-generation', 'deployment'. Note: 'static-web-app' and 'coding-agent' resources only supports 'all'.")] + public required string Action { get; set; } } diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs index bc85c88605..a029b70e20 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs @@ -169,7 +169,7 @@ public async Task ExecuteAsync_MissingResource_ReturnsBadRequest() // Assert Assert.NotNull(response); Assert.Equal(HttpStatusCode.BadRequest, response.Status); - Assert.Contains("Both resource and action parameters are required", response.Message); + Assert.Contains("Missing Required options: --resource", response.Message); } [Fact] @@ -180,6 +180,6 @@ public async Task ExecuteAsync_MissingAction_ReturnsBadRequest() // Assert Assert.NotNull(response); Assert.Equal(HttpStatusCode.BadRequest, response.Status); - Assert.Contains("Both resource and action parameters are required", response.Message); + Assert.Contains("Missing Required options: --action", response.Message); } } diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Azure.Mcp.Tools.AzureTerraformBestPractices.csproj b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Azure.Mcp.Tools.AzureTerraformBestPractices.csproj index f453c9bf32..634302fc23 100644 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Azure.Mcp.Tools.AzureTerraformBestPractices.csproj +++ b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Azure.Mcp.Tools.AzureTerraformBestPractices.csproj @@ -7,12 +7,11 @@ - + - \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs index 57ce7f2390..fe441e8652 100644 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs @@ -3,7 +3,6 @@ using System.Net; using System.Reflection; -using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Commands; using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Models.Command; @@ -25,9 +24,8 @@ the Azure Best Practices category. ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class AzureTerraformBestPracticesGetCommand(ILogger logger) : BaseCommand +public sealed class AzureTerraformBestPracticesGetCommand() : BaseCommand> { - private readonly ILogger _logger = logger; private static readonly string s_bestPracticesText = LoadBestPracticesText(); private static string GetBestPracticesText() => s_bestPracticesText; @@ -39,9 +37,7 @@ private static string LoadBestPracticesText() return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); } - protected override EmptyOptions BindOptions(ParseResult parseResult) => new(); - - public override Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + public override Task ExecuteAsync(CommandContext context, EmptyOptions options, CancellationToken cancellationToken) { var bestPractices = GetBestPracticesText(); context.Response.Status = HttpStatusCode.OK; diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/GlobalUsings.cs deleted file mode 100644 index b41cc886b4..0000000000 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/GlobalUsings.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -global using System.CommandLine;