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;