Skip to main content
Glama

Azure MCP Server

Official
MIT License
1,161
  • Linux
  • Apple
new-command.md77.8 kB
<!-- Copyright (c) Microsoft Corporation. <!-- Licensed under the MIT License. --> # Implementing a New Command in Azure MCP This document provides a comprehensive guide for implementing commands in Azure MCP following established patterns. ## Area Pattern: Organizing code by area All new Azure services and their commands should use the Area pattern: - **Area code** goes in `areas/{area-name}/src/AzureMcp.{AreaName}` (e.g., `areas/storage/src/AzureMcp.Storage`) - **Tests** go in `areas/{area-name}/tests`, divided into UnitTests and LiveTests: - `areas/{area-name}/tests/AzureMcp.{AreaName}.UnitTests` - `areas/{area-name}/tests/AzureMcp.{AreaName}.LiveTests` This keeps all code, options, models, and tests for an area together. See `areas/storage` for a reference implementation. ## ⚠️ Test Infrastructure Requirements **CRITICAL DECISION POINT**: Does your command interact with Azure resources? ### **Azure Service Commands (REQUIRES Test Infrastructure)** If your command interacts with Azure resources (storage accounts, databases, VMs, etc.): - ✅ **MUST create** `areas/{area-name}/tests/test-resources.bicep` - ✅ **MUST create** `areas/{area-name}/tests/test-resources-post.ps1` (required even if basic template) - ✅ **MUST include** RBAC role assignments for test application - ✅ **MUST validate** with `az bicep build --file areas/{area-name}/tests/test-resources.bicep` - ✅ **MUST test deployment** with `./eng/scripts/Deploy-TestResources.ps1 -Area {area-name}` ### **Non-Azure Commands (No Test Infrastructure Needed)** If your command is a wrapper/utility (CLI tools, best practices, documentation): - ❌ **Skip** Bicep template creation - ❌ **Skip** live test infrastructure - ✅ **Focus on** unit tests and mock-based testing **Examples of each type**: - **Azure Service Commands**: ACR Registry List, SQL Database Show, Storage Account List - **Non-Azure Commands**: Azure CLI wrapper, Best Practices guidance, Documentation tools ## Command Architecture ### Command Design Principles 1. **Command Interface** - `IBaseCommand` serves as the root interface with core command capabilities: - `Name`: Command name for CLI display - `Description`: Detailed command description - `Title`: Human-readable command title - `GetCommand()`: Retrieves System.CommandLine command definition - `ExecuteAsync()`: Executes command logic - `Validate()`: Validates command inputs 2. **Command Hierarchy** All commands must implement the hierarchy pattern: ``` IBaseCommand └── BaseCommand └── GlobalCommand<TOptions> └── SubscriptionCommand<TOptions> └── Service-specific base commands (e.g., BaseSqlCommand) └── Resource-specific commands (e.g., SqlIndexRecommendCommand) ``` IMPORTANT: - Commands use primary constructors with ILogger injection - Classes are always sealed unless explicitly intended for inheritance - Commands inheriting from SubscriptionCommand must handle subscription parameters - Service-specific base commands should add service-wide options - Commands return `ToolMetadata` property to define their behavioral characteristics 3. **Command Pattern** Commands follow the Model-Context-Protocol (MCP) pattern with this naming convention: ``` azmcp <azure service> <resource> <operation> ``` Example: `azmcp storage container list` Where: - `azure service`: Azure service name (lowercase, e.g., storage, cosmos, kusto) - `resource`: Resource type (singular noun, lowercase) - `operation`: Action to perform (verb, lowercase) Each command is: - In code, to avoid ambiguity between service classes and Azure services, we refer to Azure services as Areas - Registered in the RegisterCommands method of its area's `areas/{area-name}/src/AzureMcp.{AreaName}/{AreaName}Setup.cs` file - Organized in a hierarchy of command groups - Documented with a title, description and examples - Validated before execution - Returns a standardized response format **IMPORTANT**: Command group names cannot contain underscores. Use camelCase or concatenated names or dash separator instead: - ✅ Good: `new CommandGroup("entraadmin", "Entra admin operations")` - ✅ Good: `new CommandGroup("resourcegroup", "Resource group operations")` - ✅ Good:`new CommandGroup("entra-admin", "Entra admin operations")` - ❌ Bad: `new CommandGroup("entra_admin", "Entra admin operations")` **AVOID ANTI-PATTERNS**: When designing commands, avoid mixing resource names with operations in a single command. Instead, use proper command group hierarchy: - ✅ Good: `azmcp postgres server param set` (command groups: server → param, operation: set) - ❌ Bad: `azmcp postgres server setparam` (mixed operation `setparam` at same level as resource operations) - ✅ Good: `azmcp storage container permission set` - ❌ Bad: `azmcp storage container setpermission` This pattern improves discoverability, maintains consistency, and allows for better grouping of related operations. ### Required Files A complete command requires: 1. OptionDefinitions static class: `areas/{area-name}/src/AzureMcp.{AreaName}/Options/{AreaName}OptionDefinitions.cs` 2. Options class: `areas/{area-name}/src/AzureMcp.{AreaName}/Options/{Resource}/{Operation}Options.cs` 3. Command class: `areas/{area-name}/src/AzureMcp.{AreaName}/Commands/{Resource}/{Resource}{Operation}Command.cs` 4. Service interface: `areas/{area-name}/src/AzureMcp.{AreaName}/Services/I{ServiceName}Service.cs` 5. Service implementation: `areas/{area-name}/src/AzureMcp.{AreaName}/Services/{ServiceName}Service.cs` - It's common for an area to have a single service class named after the area but some areas will have multiple service classes 6. Unit test: `areas/{area-name}/tests/AzureMcp.{AreaName}.UnitTests/{Resource}/{Resource}{Operation}CommandTests.cs` 7. Integration test: `areas/{area-name}/tests/AzureMcp.{AreaName}.LiveTests/{AreaName}CommandTests.cs` 8. Command registration in RegisterCommands(): `areas/{area-name}/src/AzureMcp.{AreaName}/{AreaName}Setup.cs` 9. Area registration in RegisterAreas(): `core/src/AzureMcp.Cli/Program.cs` 10. **Live test infrastructure** (for Azure service commands): - Bicep template: `/areas/{area-name}/tests/test-resources.bicep` - Post-deployment script: `/areas/{area-name}/tests/test-resources-post.ps1` (required, even if basic template) ### File and Class Naming Convention **IMPORTANT**: All command files and classes must follow the **ObjectVerb** naming pattern for consistency and discoverability: **Pattern**: `{Resource}{SubResource}{Operation}Command` **Examples**: - ✅ `ServerListCommand` (Resource: Server, Operation: List) - ✅ `ServerConfigGetCommand` (Resource: Server, SubResource: Config, Operation: Get) - ✅ `ServerParamSetCommand` (Resource: Server, SubResource: Param, Operation: Set) - ✅ `TableSchemaGetCommand` (Resource: Table, SubResource: Schema, Operation: Get) - ✅ `DatabaseListCommand` (Resource: Database, Operation: List) **Anti-patterns to avoid**: - ❌ `GetConfigCommand` (missing resource prefix) - ❌ `GetParamCommand` (missing resource prefix) - ❌ `GetSchemaCommand` (missing resource prefix) **Apply this pattern to**: - Command class names: `ServerConfigGetCommand`, `ServerParamSetCommand` - Options class names: `ServerConfigGetOptions`, `ServerParamSetOptions` - Test class names: `ServerConfigGetCommandTests`, `ServerParamSetCommandTests` - File names: `ServerConfigGetCommand.cs`, `ServerParamSetOptions.cs` This convention ensures: - Clear identification of the resource being operated on - Logical grouping of related operations - Consistent file organization and naming - Better IDE intellisense and code navigation - Easier maintenance and discovery **IMPORTANT**: If implementing a new area, you must also ensure: - The Azure Resource Manager package is added to `Directory.Packages.props` first - Models, base commands, and option definitions follow the established patterns - JSON serialization context includes all new model types - Service registration in the area setup ConfigureServices method - **Live test infrastructure**: Add Bicep template to `/areas/{area-name}/tests` - **Test resource deployment**: Ensure resources are properly configured with RBAC for test application - **Resource naming**: Follow consistent naming patterns - many services use just `baseName`, while others may need suffixes for disambiguation (e.g., `{baseName}-suffix`) - **Solution file integration**: Add new projects to `AzureMcp.sln` with proper GUID generation to avoid conflicts - **Program.cs registration**: Register the new area in `Program.cs` `RegisterAreas()` method in alphabetical order ## Implementation Guidelines ### 1. Azure Resource Manager Integration When creating commands that interact with Azure services, you'll need to: **Package Management:** For **Resource Graph queries** (using `BaseAzureResourceService`): - No additional packages required - `Azure.ResourceManager.ResourceGraph` is already included in the core project - Only add area-specific packages if you need direct ARM operations beyond Resource Graph queries - Example: `<PackageReference Include="Azure.ResourceManager.Sql" />` (only if needed for direct ARM operations) For **Direct ARM operations** (using `BaseAzureService`): - Add the appropriate Azure Resource Manager package to `Directory.Packages.props` - Example: `<PackageVersion Include="Azure.ResourceManager.Sql" Version="1.3.0" />` - Add the package reference in `AzureMcp.{AreaName}.csproj` - Example: `<PackageReference Include="Azure.ResourceManager.Sql" />` - **Version Consistency**: Ensure the package version in `Directory.Packages.props` matches across all projects - **Build Order**: Add the package to `Directory.Packages.props` first, then reference it in project files to avoid build errors **Service Base Class Selection:** Choose the appropriate base class for your service based on the operations needed: 1. **For Azure Resource Graph queries** (recommended for resource management operations): - Inherit from `BaseAzureResourceService` for services that need to query Azure Resource Graph - Automatically provides `ExecuteResourceQueryAsync<T>()` and `ExecuteSingleResourceQueryAsync<T>()` methods - Handles subscription resolution, tenant lookup, and Resource Graph query execution - Example: ```csharp public class MyService(ISubscriptionService subscriptionService, ITenantService tenantService) : BaseAzureResourceService(subscriptionService, tenantService), IMyService { public async Task<List<MyResource>> ListResourcesAsync( string resourceGroup, string subscription, RetryPolicyOptions? retryPolicy) { return await ExecuteResourceQueryAsync( "Microsoft.MyService/resources", resourceGroup, subscription, retryPolicy, ConvertToMyResourceModel); } public async Task<MyResource?> GetResourceAsync( string resourceName, string resourceGroup, string subscription, RetryPolicyOptions? retryPolicy) { return await ExecuteSingleResourceQueryAsync( "Microsoft.MyService/resources", resourceGroup, subscription, retryPolicy, ConvertToMyResourceModel, $"name =~ '{resourceName}'"); } private static MyResource ConvertToMyResourceModel(JsonElement item) { var data = MyResourceData.FromJson(item); return new MyResource( Name: data.ResourceName, Id: data.ResourceId, // Map other properties... ); } } ``` 2. **For direct Azure Resource Manager operations**: - Inherit from `BaseAzureService` for services that use ARM clients directly - Use when you need direct ARM resource manipulation (create, update, delete) - Example: ```csharp public class MyService(ISubscriptionService subscriptionService, ITenantService tenantService) : BaseAzureService(tenantService), IMyService { private readonly ISubscriptionService _subscriptionService = subscriptionService; public async Task<MyResource> GetResourceAsync(string subscription, ...) { var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); // Use subscriptionResource for direct ARM operations } } ``` **BaseAzureResourceService Benefits:** - Eliminates duplicate Resource Graph query code across services - Provides consistent error handling and validation - Handles subscription resolution and tenant lookup automatically - Supports additional KQL filter parameters for complex queries - Maintains AOT compatibility **API Pattern Discovery:** - Study existing services (e.g., Sql, Postgres, Redis) to understand resource access patterns - Use resource collections correctly: `.GetSqlServers().GetAsync(serverName)` not `.GetSqlServerAsync(serverName, cancellationToken)` - Check Azure SDK documentation for correct method signatures and property names **Common Azure Resource Manager Patterns:** ```csharp // Resource Graph pattern (via BaseAzureResourceService) var resources = await ExecuteResourceQueryAsync( "Microsoft.Sql/servers/databases", resourceGroup, subscription, retryPolicy, ConvertToSqlDatabaseModel, "name =~ 'mydb*'"); // Optional KQL filter // Direct ARM pattern (via BaseAzureService) var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy); var resourceGroupResource = await subscriptionResource .GetResourceGroupAsync(resourceGroup, cancellationToken); var sqlServerResource = await resourceGroupResource.Value .GetSqlServers() .GetAsync(serverName); var databaseResource = await sqlServerResource.Value .GetSqlDatabases() .GetAsync(databaseName); ``` **Property Access Issues:** - Azure SDK property names may differ from expected names (e.g., `CreatedOn` not `CreationDate`) - Check actual property availability using IntelliSense or SDK documentation - Some properties are objects that need `.ToString()` conversion (e.g., `Location.ToString()`) - Be aware of nullable properties and use appropriate null checks **Compilation Error Resolution:** - When you see `cannot convert from 'System.Threading.CancellationToken' to 'string'`, check method parameter order - For `'SqlDatabaseData' does not contain a definition for 'X'`, verify property names in the actual SDK types - Use existing service implementations as reference for correct property access patterns ### 2. Options Class ```csharp public class {Resource}{Operation}Options : Base{Area}Options { // Only add properties not in base class public string? NewOption { get; set; } } ``` IMPORTANT: - Inherit from appropriate base class (Base{Area}Options, GlobalOptions, etc.) - Never redefine properties from base classes - Make properties nullable if not required - Use consistent parameter names across services: - **CRITICAL**: Always use `subscription` (never `subscriptionId`) for subscription parameters - this allows the parameter to accept both subscription IDs and subscription names, which are resolved internally by `ISubscriptionService.GetSubscription()` - Use `resourceGroup` instead of `resourceGroupName` - Use singular nouns for resource names (e.g., `server` not `serverName`) - **Remove unnecessary "-name" suffixes**: Use `--account` instead of `--account-name`, `--container` instead of `--container-name`, etc. Only keep "-name" when it provides necessary disambiguation (e.g., `--subscription-name` to distinguish from global `--subscription`) - Keep parameter names consistent with Azure SDK parameters when possible - If services share similar operations (e.g., ListDatabases), use the same parameter order and names ### Resource Group Usage Pattern The `resource-group` option is defined globally once and is always parser-optional. Commands declare their logical need for it using helper methods instead of redefining or manually binding the option. Helpers (available in `BaseCommand`): ```csharp protected void UseResourceGroup(); // Optional filter – user may include it protected void RequireResourceGroup(); // Logically required – validation enforces presence protected string? GetResourceGroup(); // Convenience accessor with validation side-effects handled centrally ``` Key rules: - Do NOT create area-specific optional resource group options. - Do NOT override `_resourceGroupOption` or manually add `OptionDefinitions.Common.ResourceGroup` to commands. - Do NOT manually assign `options.ResourceGroup` in `BindOptions` – central binding in `GlobalCommand` handles this when a command calls either helper. - Validation for required resource group happens centrally (logical requirement), not at parser level. Usage examples inside `RegisterOptions`: ```csharp protected override void RegisterOptions(Command command) { base.RegisterOptions(command); RequireResourceGroup(); // Command cannot run without a resource group command.AddOption(_clusterNameOption); } protected override void RegisterOptions(Command command) { base.RegisterOptions(command); UseResourceGroup(); // Optional narrowing filter } ``` Binding example (no manual resource group assignment): ```csharp protected override ListServersOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.Server = parseResult.GetValueForOption(_serverOption); return options; // options.ResourceGroup already populated (or null) } ``` Accessing during execution: ```csharp var rg = options.ResourceGroup; // direct // or var rg2 = GetResourceGroup(); // helper (throws validation earlier if required & missing) ``` Rationale: - Eliminates duplicated option definitions. - Clear, declarative intent (Require vs Use) with minimal boilerplate. - Keeps parser surface stable while allowing logical enforcement. - Simplifies future extension if other global options adopt the same pattern. ### 3. Command Class ```csharp public sealed class {Resource}{Operation}Command(ILogger<{Resource}{Operation}Command> logger) : Base{Area}Command<{Resource}{Operation}Options> { private const string CommandTitle = "Human Readable Title"; private readonly ILogger<{Resource}{Operation}Command> _logger = logger; // Define options from OptionDefinitions private readonly Option<string> _newOption = {Area}OptionDefinitions.NewOption; public override string Name => "operation"; public override string Description => """ Detailed description of what the command does. Returns description of return format. Required options: - list required options """; public override string Title => CommandTitle; public override ToolMetadata Metadata => new() { Destructive = false, // Set to true for commands that modify resources ReadOnly = true // Set to false for commands that modify resources }; protected override void RegisterOptions(Command command) { base.RegisterOptions(command); command.AddOption(_newOption); } protected override {Resource}{Operation}Options BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.NewOption = parseResult.GetValueForOption(_newOption); return options; } public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult) { var options = BindOptions(parseResult); try { // Required validation step if (!Validate(parseResult.CommandResult, context.Response).IsValid) { return context.Response; } context.Activity?.WithSubscriptionTag(options); // Get the appropriate service from DI var service = context.GetService<I{Area}Service>(); // Call service operation(s) with required parameters var results = await service.{Operation}( options.RequiredParam!, // Required parameters end with ! options.OptionalParam, // Optional parameters are nullable options.Subscription!, // From SubscriptionCommand options.RetryPolicy); // From GlobalCommand // Set results if any were returned context.Response.Results = results?.Count > 0 ? ResponseResult.Create( new {Operation}CommandResult(results), {Area}JsonContext.Default.{Operation}CommandResult) : null; } catch (Exception ex) { // Log error with all relevant context _logger.LogError(ex, "Error in {Operation}. Required: {Required}, Optional: {Optional}, Options: {@Options}", Name, options.RequiredParam, options.OptionalParam, options); HandleException(context, ex); } return context.Response; } // Implementation-specific error handling protected override string GetErrorMessage(Exception ex) => ex switch { Azure.RequestFailedException reqEx when reqEx.Status == 404 => "Resource not found. Verify the resource exists and you have access.", Azure.RequestFailedException reqEx when reqEx.Status == 403 => $"Authorization failed accessing the resource. Details: {reqEx.Message}", Azure.RequestFailedException reqEx => reqEx.Message, _ => base.GetErrorMessage(ex) }; protected override int GetStatusCode(Exception ex) => ex switch { Azure.RequestFailedException reqEx => reqEx.Status, _ => base.GetStatusCode(ex) }; // Strongly-typed result records internal record {Resource}{Operation}CommandResult(List<ResultType> Results); } ### 4. Service Interface and Implementation Each area has its own service interface that defines the methods that commands will call. The interface will have an implementation that contains the actual logic. ```csharp public interface I<Area>Service { ... } ``` ```csharp public class <Area>Service(ISubscriptionService subscriptionService, ITenantService tenantService, ICacheService cacheService) : BaseAzureService(tenantService), I<Area>Service { ... } ``` ### Method Signature Consistency All interface methods should follow consistent formatting with proper line breaks and parameter alignment: ```csharp // Correct formatting - parameters aligned with line breaks Task<List<string>> GetStorageAccounts( string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null); // Incorrect formatting - all parameters on single line Task<List<string>> GetStorageAccounts(string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null); ``` **Formatting Rules:** - Parameters indented and aligned - Add blank lines between method declarations for visual separation - Maintain consistent indentation across all methods in the interface ### 5. Base Service Command Classes Each area has its own hierarchy of base command classes that inherit from `GlobalCommand` or `SubscriptionCommand`. Service classes that work with Azure resources should inject `ISubscriptionService` for subscription resolution. For example: ```csharp // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; using AzureMcp.Core.Commands; using AzureMcp.Core.Commands.Subscription; using AzureMcp.{Area}.Options; namespace AzureMcp.{Area}.Commands; // Base command for all service commands (if no members needed, use concise syntax) public abstract class Base{Area}Command< [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions> : SubscriptionCommand<TOptions> where TOptions : Base{Area}Options, new(); // Base command for all service commands (if members are needed, use full syntax) public abstract class Base{Area}Command< [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions> : SubscriptionCommand<TOptions> where TOptions : Base{Area}Options, new() { protected readonly Option<string> _commonOption = {Area}OptionDefinitions.CommonOption; protected readonly Option<string> _resourceGroupOption = OptionDefinitions.Common.ResourceGroup; protected virtual bool RequiresResourceGroup => true; protected override void RegisterOptions(Command command) { base.RegisterOptions(command); command.AddOption(_commonOption); // Add resource group option if required if (RequiresResourceGroup) { command.AddOption(_resourceGroupOption); } } protected override TOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.CommonOption = parseResult.GetValueForOption(_commonOption); if (RequiresResourceGroup) { options.ResourceGroup = parseResult.GetValueForOption(_resourceGroupOption); } return options; } } // Service implementation example with subscription resolution public class {Area}Service(ISubscriptionService subscriptionService, ITenantService tenantService) : BaseAzureService(tenantService), I{Area}Service { private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService)); public async Task<{Resource}> GetResourceAsync(string subscription, string resourceGroup, string resourceName, RetryPolicyOptions? retryPolicy) { // Always use subscription service for resolution var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); var resourceGroupResource = await subscriptionResource .GetResourceGroupAsync(resourceGroup, cancellationToken); // Continue with resource access... } } ``` ### 6. Unit Tests Unit tests follow a standardized pattern that tests initialization, validation, and execution: ```csharp public class {Resource}{Operation}CommandTests { private readonly IServiceProvider _serviceProvider; private readonly I{Area}Service _service; private readonly ILogger<{Resource}{Operation}Command> _logger; private readonly {Resource}{Operation}Command _command; private readonly CommandContext _context; private readonly Parser _parser; public {Resource}{Operation}CommandTests() { _service = Substitute.For<I{Area}Service>(); _logger = Substitute.For<ILogger<{Resource}{Operation}Command>>(); var collection = new ServiceCollection().AddSingleton(_service); _serviceProvider = collection.BuildServiceProvider(); _command = new(_logger); _context = new(_serviceProvider); _parser = new(_command.GetCommand()); } [Fact] public void Constructor_InitializesCommandCorrectly() { var command = _command.GetCommand(); Assert.Equal("operation", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); } [Theory] [InlineData("--required value", true)] [InlineData("--optional-param value --required value", true)] [InlineData("", false)] public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { // Arrange if (shouldSucceed) { _service.{Operation}(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>()) .Returns(new List<ResultType>()); } var parseResult = _parser.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries)); // Act var response = await _command.ExecuteAsync(_context, parseResult); // Assert Assert.Equal(shouldSucceed ? 200 : 400, response.Status); if (shouldSucceed) { Assert.NotNull(response.Results); Assert.Equal("Success", response.Message); } else { Assert.Contains("required", response.Message.ToLower()); } } [Fact] public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange _service.{Operation}(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>()) .Returns(Task.FromException<List<ResultType>>(new Exception("Test error"))); var parseResult = _parser.Parse(["--required", "value"]); // Act var response = await _command.ExecuteAsync(_context, parseResult); // Assert Assert.Equal(500, response.Status); Assert.Contains("Test error", response.Message); Assert.Contains("troubleshooting", response.Message); } } ``` ### 7. Integration Tests Integration tests inherit from `CommandTestsBase` and use test fixtures: ```csharp public class {Area}CommandTests(LiveTestFixture liveTestFixture, ITestOutputHelper output) : CommandTestsBase(liveTestFixture, output), IClassFixture<LiveTestFixture> { [Theory] [InlineData(AuthMethod.Credential)] [InlineData(AuthMethod.Key)] public async Task Should_{Operation}_{Resource}_WithAuth(AuthMethod authMethod) { // Arrange var result = await CallToolAsync( "azmcp_{area}_{resource}_{operation}", new() { { "subscription", Settings.Subscription }, { "resource-group", Settings.ResourceGroup }, { "auth-method", authMethod.ToString().ToLowerInvariant() } }); // Assert var items = result.AssertProperty("items"); Assert.Equal(JsonValueKind.Array, items.ValueKind); // Check results format foreach (var item in items.EnumerateArray()) { Assert.True(item.TryGetProperty("name", out _)); Assert.True(item.TryGetProperty("type", out _)); } } [Theory] [InlineData("--invalid-param")] [InlineData("--subscription invalidSub")] public async Task Should_Return400_WithInvalidInput(string args) { var result = await CallToolAsync( $"azmcp_{area}_{resource}_{operation} {args}"); Assert.Equal(400, result.GetProperty("status").GetInt32()); Assert.Contains("required", result.GetProperty("message").GetString()!.ToLower()); } } ``` ### 8. Command Registration ```csharp private void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory) { var service = new CommandGroup( "{area}", "{Area} operations"); rootGroup.AddSubGroup(service); var resource = new CommandGroup( "{resource}", "{Resource} operations"); service.AddSubGroup(resource); resource.AddCommand("{operation}", new {Resource}{Operation}Command( loggerFactory.CreateLogger<{Resource}{Operation}Command>())); } ``` **IMPORTANT**: Command group names cannot contain underscores. Use camelCase or concatenated names or dash separator instead: - ✅ Good: `"entraadmin"`, `"resourcegroup"`, `"storageaccount"`, `"entra-admin"` - ❌ Bad: `"entra_admin"`, `"resource_group"`, `"storage_account"` ### 9. Area Registration ```csharp private static IAreaSetup[] RegisterAreas() { return [ // Register core areas new AzureMcp.AzureBestPractices.AzureBestPracticesSetup(), new AzureMcp.Extension.ExtensionSetup(), // Register Azure service areas new AzureMcp.{Area}.{Area}Setup(), new AzureMcp.Storage.StorageSetup(), ]; } ``` The area list in `RegisterAreas()` should stay sorted alphabetically. ## Error Handling Commands in Azure MCP follow a standardized error handling approach using the base `HandleException` method inherited from `BaseCommand`. Here are the key aspects: ### 1. Status Code Mapping The base implementation returns 500 for all exceptions by default: ```csharp protected virtual int GetStatusCode(Exception ex) => 500; ``` Commands should override this to provide appropriate status codes: ```csharp protected override int GetStatusCode(Exception ex) => ex switch { Azure.RequestFailedException reqEx => reqEx.Status, // Use Azure-reported status Azure.Identity.AuthenticationFailedException => 401, // Unauthorized ValidationException => 400, // Bad request _ => base.GetStatusCode(ex) // Fall back to 500 }; ``` ### 2. Error Message Formatting The base implementation returns the exception message: ```csharp protected virtual string GetErrorMessage(Exception ex) => ex.Message; ``` Commands should override this to provide user-actionable messages: ```csharp protected override string GetErrorMessage(Exception ex) => ex switch { Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed. Please run 'az login' to sign in. Details: {authEx.Message}", Azure.RequestFailedException reqEx when reqEx.Status == 404 => "Resource not found. Verify the resource name and that you have access.", Azure.RequestFailedException reqEx when reqEx.Status == 403 => $"Access denied. Ensure you have appropriate RBAC permissions. Details: {reqEx.Message}", Azure.RequestFailedException reqEx => reqEx.Message, _ => base.GetErrorMessage(ex) }; ``` ### 3. Response Format The base `HandleException` method in BaseCommand handles the response formatting: ```csharp protected virtual void HandleException(CommandContext context, Exception ex) { context.Activity?.SetStatus(ActivityStatusCode.Error)?.AddTag(TagName.ErrorDetails, ex.Message); var response = context.Response; var result = new ExceptionResult( Message: ex.Message, StackTrace: ex.StackTrace, Type: ex.GetType().Name); response.Status = GetStatusCode(ex); response.Message = GetErrorMessage(ex) + ". To mitigate this issue, please refer to the troubleshooting guidelines here at https://aka.ms/azmcp/troubleshooting."; response.Results = ResponseResult.Create(result, JsonSourceGenerationContext.Default.ExceptionResult); } ``` Commands should call `HandleException(context, ex)` in their catch blocks. ### 4. Service-Specific Errors Commands should override error handlers to add service-specific mappings: ```csharp protected override string GetErrorMessage(Exception ex) => ex switch { // Add service-specific cases ResourceNotFoundException => "Resource not found. Verify name and permissions.", ServiceQuotaExceededException => "Service quota exceeded. Request quota increase.", _ => base.GetErrorMessage(ex) // Fall back to base implementation }; ``` ### 5. Error Context Logging Always log errors with relevant context information: ```csharp catch (Exception ex) { _logger.LogError(ex, "Error in {Operation}. Resource: {Resource}, Options: {@Options}", Name, resourceId, options); HandleException(context, ex); } ``` ### 6. Common Error Scenarios to Handle 1. **Authentication/Authorization** - Azure credential expiry - Missing RBAC permissions - Invalid connection strings 2. **Validation** - Missing required parameters - Invalid parameter formats - Conflicting options 3. **Resource State** - Resource not found - Resource locked/in use - Invalid resource state 4. **Service Limits** - Throttling/rate limits - Quota exceeded - Service capacity 5. **Network/Connectivity** - Service unavailable - Request timeouts - Network failures ## Testing Requirements ### Unit Tests Core test cases for every command: ```csharp [Theory] [InlineData("", false, "Missing required options")] // Validation [InlineData("--param invalid", false, "Invalid format")] // Input format [InlineData("--param value", true, null)] // Success case public async Task ExecuteAsync_ValidatesInput( string args, bool shouldSucceed, string expectedError) { var response = await ExecuteCommand(args); Assert.Equal(shouldSucceed ? 200 : 400, response.Status); if (!shouldSucceed) Assert.Contains(expectedError, response.Message); } [Fact] public async Task ExecuteAsync_HandlesServiceError() { // Arrange _service.Operation() .Returns(Task.FromException(new ServiceException("Test error"))); // Act var response = await ExecuteCommand("--param value"); // Assert Assert.Equal(500, response.Status); Assert.Contains("Test error", response.Message); Assert.Contains("troubleshooting", response.Message); } ``` **Running Tests Efficiently:** When developing new commands, run only your specific tests to save time: ```bash # Run all tests from the test project directory: pushd ./areas/your-area/tests/AzureMcp.YourArea.UnitTests #or .LiveTests # Run only tests for your specific command class dotnet test --filter "FullyQualifiedName~YourCommandNameTests" --verbosity normal # Example: Run only SQL AD Admin tests dotnet test --filter "FullyQualifiedName~EntraAdminListCommandTests" --verbosity normal # Run all tests for a specific area dotnet test --verbosity normal ``` ### Integration Tests Azure service commands requiring test resource deployment must add a bicep template, `tests/test-resources.bicep`, to their area directory. Additionally, all Azure service commands must include a `test-resources-post.ps1` file in the same directory, even if it contains only the basic template without custom logic. See `/areas/storage/tests/test-resources.bicep` and `/areas/storage/tests/test-resources-post.ps1` for canonical examples. #### Live Test Resource Infrastructure **1. Create Area Bicep Template (`/areas/{area-name}/tests/test-resources.bicep`)** Follow this pattern for your area's infrastructure: ```bicep targetScope = 'resourceGroup' @minLength(3) @maxLength(17) // Adjust based on service naming limits @description('The base resource name. Service names have specific length restrictions.') param baseName string = resourceGroup().name @description('The client OID to grant access to test resources.') param testApplicationOid string = deployer().objectId // The test infrastructure will only provide baseName and testApplicationOid. // Any additional parameters are for local deployments only and require default values. @description('The location of the resource. By default, this is the same as the resource group.') param location string = resourceGroup().location // Main service resource resource serviceResource 'Microsoft.{Provider}/{resourceType}@{apiVersion}' = { name: baseName location: location properties: { // Service-specific properties } // Child resources (databases, containers, etc.) resource testResource 'childResourceType@{apiVersion}' = { name: 'test{resource}' properties: { // Test resource properties } } } // Role assignment for test application resource serviceRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { scope: subscription() // Use appropriate built-in role for your service // See https://learn.microsoft.com/azure/role-based-access-control/built-in-roles name: '{role-guid}' } resource appServiceRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(serviceRoleDefinition.id, testApplicationOid, serviceResource.id) scope: serviceResource properties: { principalId: testApplicationOid roleDefinitionId: serviceRoleDefinition.id description: '{Role Name} for testApplicationOid' } } // Outputs for test consumption output serviceResourceName string = serviceResource.name output testResourceName string = serviceResource::testResource.name // Add other outputs as needed for tests ``` **Key Bicep Template Requirements:** - Use `baseName` parameter with appropriate length restrictions - Include `testApplicationOid` for RBAC assignments - Deploy test resources (databases, containers, etc.) needed for integration tests - Assign appropriate built-in roles to the test application - Output resource names and identifiers for test consumption **Cost and Resource Considerations:** - Use minimal SKUs (Basic, Standard S0, etc.) for cost efficiency - Deploy only resources needed for command testing - Consider using shared resources where possible - Set appropriate retention policies and limits - Use resource naming that clearly identifies test purposes **Common Resource Naming Patterns:** - Deployments are on a per-area basis. Name collisions should not occur across area templates. - Main service: `baseName` (most common, e.g., `mcp12345`) or `{baseName}{suffix}` if disambiguation needed - Child resources: `test{resource}` (e.g., `testdb`, `testcontainer`) - Follow Azure naming conventions and length limits - Ensure names are unique within resource group scope - Check existing `test-resources.bicep` files for consistent patterns **2. Required: Post-Deployment Script (`/areas/{area-name}/tests/test-resources-post.ps1`)** All Azure service commands must include this script, even if it contains only the basic template. Create with the standard template and add custom setup logic if needed: ```powershell #!/usr/bin/env pwsh # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #Requires -Version 6.0 #Requires -PSEdition Core [CmdletBinding()] param ( [Parameter(Mandatory)] [hashtable] $DeploymentOutputs, [Parameter(Mandatory)] [hashtable] $AdditionalParameters ) Write-Host "Running {Area} post-deployment setup..." try { # Extract outputs from deployment $serviceName = $DeploymentOutputs['{area}']['serviceResourceName']['value'] $resourceGroup = $AdditionalParameters['ResourceGroupName'] # Perform additional setup (e.g., create sample data, configure settings) Write-Host "Setting up test data for $serviceName..." # Example: Run Azure CLI commands for additional setup # az {service} {operation} --name $serviceName --resource-group $resourceGroup Write-Host "{Area} post-deployment setup completed successfully." } catch { Write-Error "Failed to complete {Area} post-deployment setup: $_" throw } ``` **4. Update Live Tests to Use Deployed Resources** Integration tests should use the deployed infrastructure: ```csharp [Trait("Area", "{Area}")] [Trait("Category", "Live")] public class {Area}CommandTests(LiveTestFixture liveTestFixture, ITestOutputHelper output) : CommandTestsBase(liveTestFixture, output), IClassFixture<LiveTestFixture> { [Fact] public async Task Should_Get{Resource}_Successfully() { // Use the deployed test resources var serviceName = Settings.ResourceBaseName; var resourceName = "test{resource}"; var result = await CallToolAsync( "azmcp_{area}_{resource}_show", new() { { "subscription", Settings.SubscriptionId }, { "resource-group", Settings.ResourceGroupName }, { "service-name", serviceName }, { "resource-name", resourceName } }); // Verify successful response var resource = result.AssertProperty("{resource}"); Assert.Equal(JsonValueKind.Object, resource.ValueKind); // Verify resource properties var name = resource.GetProperty("name").GetString(); Assert.Equal(resourceName, name); } [Theory] [InlineData("--invalid-param", new string[0])] [InlineData("--subscription", new[] { "invalidSub" })] [InlineData("--subscription", new[] { "sub", "--resource-group", "rg" })] // Missing required params public async Task Should_Return400_WithInvalidInput(string firstArg, string[] remainingArgs) { var allArgs = new[] { firstArg }.Concat(remainingArgs); var argsString = string.Join(" ", allArgs); var result = await CallToolAsync( "azmcp_{area}_{resource}_show", new() { { "args", argsString } }); // Should return validation error Assert.NotEqual(200, result.Status); } } ``` **5. Deploy and Test Resources** Use the deployment script with your area: ```powershell # Deploy test resources for your area ./eng/scripts/Deploy-TestResources.ps1 -Areas "{Area}" # Run live tests dotnet test --filter "Category=Live&Area={Area}" ``` Live test scenarios should include: ```csharp [Theory] [InlineData(AuthMethod.Credential)] // Default auth [InlineData(AuthMethod.Key)] // Key based auth public async Task Should_HandleAuth(AuthMethod method) { var result = await CallCommand(new() { { "auth-method", method.ToString() } }); // Verify auth worked Assert.Equal(200, result.Status); } [Theory] [InlineData("--invalid-value")] // Bad input [InlineData("--missing-required")] // Missing params public async Task Should_Return400_ForInvalidInput(string args) { var result = await CallCommand(args); Assert.Equal(400, result.Status); Assert.Contains("validation", result.Message.ToLower()); } ``` If your live test class needs to implement `IAsyncLifetime` or override `Dispose`, you must call `Dispose` on your base class: ```cs public class MyCommandTests(LiveTestFixture liveTestFixture, ITestOutputHelper output) : CommandTestsBase(liveTestFixture, output), IClassFixture<LiveTestFixture>, IAsyncLifetime { public ValueTask DisposeAsync() { base.Dispose(); return ValueTask.CompletedTask; } } ``` Failure to call `base.Dispose()` will prevent request and response data from `CallCommand` from being written to failing test results. ## Code Quality and Unused Using Statements ### Preventing Unused Using Statements Unused using statements are a common issue that clutters code and can lead to unnecessary dependencies. Here are strategies to prevent and detect them: #### 1. **Use Minimal Using Statements When Creating Files** When creating new C# files, start with only the using statements you actually need: ```csharp // Start minimal - only add what you actually use using AzureMcp.Core.Commands; using Microsoft.Extensions.Logging; // Add more using statements as you implement the code // Don't copy-paste using blocks from other files ``` #### 2. **Leverage ImplicitUsings** The project already has `<ImplicitUsings>enable</ImplicitUsings>` in `Directory.Build.props`, which automatically includes common using statements for .NET 9: **Implicit Using Statements (automatically included):** - `using System;` - `using System.Collections.Generic;` - `using System.IO;` - `using System.Linq;` - `using System.Net.Http;` - `using System.Threading;` - `using System.Threading.Tasks;` **Don't manually add these - they're already included!** #### 3. **Detection and Cleanup Commands** Use these commands to detect and remove unused using statements: ```powershell # Format specific area files (recommended during development) dotnet format --include="areas/{area-name}/**/*.cs" --verbosity normal # Format entire solution (use sparingly - takes longer) dotnet format ./AzureMcp.sln --verbosity normal # Check for analyzer warnings including unused usings dotnet build --verbosity normal | Select-String "warning" ``` #### 4. **Common Unused Using Patterns to Avoid** ❌ **Don't copy using blocks from other files:** ```csharp // Copied from another file but not all are needed using System.CommandLine; using System.CommandLine.Parsing; using AzureMcp.Acr.Commands; // ← May not be needed using AzureMcp.Acr.Options; // ← May not be needed using AzureMcp.Acr.Options.Registry; // ← May not be needed using AzureMcp.Acr.Services; // ... 15 more using statements ``` ✅ **Start minimal and add as needed:** ```csharp // Only what's actually used in this file using AzureMcp.Acr.Services; using Microsoft.Extensions.Logging; ``` ✅ **Add using statements for better readability:** ```csharp using Azure.ResourceManager.ContainerRegistry.Models; // Clean and readable - even if used only once public ContainerRegistryResource Resource { get; set; } // This is much better than: // public Azure.ResourceManager.ContainerRegistry.Models.ContainerRegistryResource Resource { get; set; } ``` #### 6. **Integration with Build Process** The project checklist already includes cleaning up unused using statements: - [ ] **Remove unnecessary using statements from all C# files** (use IDE cleanup or `dotnet format`) **Make this part of your development workflow:** 1. Write code with minimal using statements 2. Add using statements only as you need them 3. Run `dotnet format --include="areas/{area-name}/**/*.cs"` before committing 4. Use IDE features to clean up automatically ### Build Verification and AOT Compatibility After implementing your commands, verify that your implementation works correctly with both regular builds and AOT (Ahead-of-Time) compilation: **1. Regular Build Verification:** ```powershell # Build the solution dotnet build # Run specific tests dotnet test --filter "FullyQualifiedName~YourCommandTests" ``` **2. AOT Compilation Verification:** AOT (Ahead-of-Time) compilation is required for all new areas to ensure compatibility with native builds: ```powershell # Test AOT compatibility - this is REQUIRED for all new areas ./eng/scripts/Build-Local.ps1 -BuildNative ``` **Expected Outcome**: If your area is properly implemented, the build should succeed. However, if AOT compilation fails (which is very likely for new areas), follow these steps: **3. AOT Compilation Issue Resolution:** When AOT compilation fails for your new area, you need to exclude it from native builds: **Step 1: Move area setup under BuildNative condition in Program.cs** ```csharp // Find your area setup call in Program.cs // Move it inside the #if !BUILD_NATIVE block #if !BUILD_NATIVE // ... other area setups ... builder.Services.Add{YourArea}Setup(); // ← Move this line here #endif ``` **Step 2: Add ProjectReference-Remove condition in AzureMcp.Cli.csproj** ```xml <!-- Add this to core/src/AzureMcp.Cli/AzureMcp.Cli.csproj --> <ItemGroup Condition="'$(BuildNative)' == 'true'"> <ProjectReference Remove="..\..\areas\{area-name}\src\AzureMcp.{AreaName}\AzureMcp.{AreaName}.csproj" /> </ItemGroup> ``` **Step 3: Verify the fix** ```powershell # Test that AOT compilation now succeeds ./eng/scripts/Build-Local.ps1 -BuildNative # Verify regular build still works dotnet build ``` **Why AOT Compilation Often Fails:** - Azure SDK libraries may not be fully AOT-compatible - Reflection-based operations in service implementations - Third-party dependencies that don't support AOT - Dynamic JSON serialization without source generators **Important**: This is a common and expected issue for new Azure service areas. The exclusion pattern is the standard solution and doesn't impact regular builds or functionality. ## Common Implementation Issues and Solutions ### Service Method Design **Issue: Inconsistent method signatures across services** - **Solution**: Follow established patterns for method signatures with proper parameter alignment - **Pattern**: ```csharp // Correct - parameters aligned with line breaks Task<List<ResourceModel>> GetResources( string subscription, string? resourceGroup = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null); ``` **Issue: Wrong subscription resolution pattern** - **Solution**: Always use `ISubscriptionService.GetSubscription()` instead of manual ARM client creation - **Pattern**: ```csharp // Correct pattern var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); ``` ### Command Option Patterns **Issue: Manual resource group option duplication or binding** - **Problem**: Command redefines a resource group option or manually assigns `options.ResourceGroup`. - **Solution**: Remove duplicate definitions; call `UseResourceGroup()` or `RequireResourceGroup()` only; let central binding populate `options.ResourceGroup`. - **Pattern**: ```csharp protected override void RegisterOptions(Command command) { base.RegisterOptions(command); UseResourceGroup(); // or RequireResourceGroup(); command.AddOption(_otherOption); } protected override MyOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); // ResourceGroup already set if declared options.Other = parseResult.GetValueForOption(_otherOption); return options; } ``` ### Error Handling Patterns **Issue: Generic error handling without service-specific context** - **Solution**: Override base error handling methods for better user experience - **Pattern**: ```csharp protected override string GetErrorMessage(Exception ex) => ex switch { Azure.RequestFailedException reqEx when reqEx.Status == 404 => "Resource not found. Verify the resource exists and you have access.", Azure.RequestFailedException reqEx when reqEx.Status == 403 => $"Authorization failed. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; ``` **Issue: Missing HandleException call** - **Solution**: Always call `HandleException(context, ex)` in command catch blocks - **Pattern**: ```csharp catch (Exception ex) { _logger.LogError(ex, "Error in {Operation}", Name); HandleException(context, ex); } ``` ## Best Practices 1. Command Structure: - Make command classes sealed - Use primary constructors - Follow exact namespace hierarchy - Register all options in RegisterOptions - Handle all exceptions 2. Error Handling: - Return 400 for validation errors - Return 401 for authentication failures - Return 500 for unexpected errors - Return service-specific status codes from RequestFailedException - Add troubleshooting URL to error messages - Log errors with context information - Override GetErrorMessage and GetStatusCode for custom error handling 3. Response Format: - Always set Results property for success - Set Status and Message for errors - Use consistent JSON property names - Follow existing response patterns 4. Documentation: - Clear command description without repeating the service name (e.g., use "List and manage clusters" instead of "AKS operations - List and manage AKS clusters") - List all required options - Describe return format - Include examples in description - **Maintain alphabetical sorting in e2eTestPrompts.md**: Insert new test prompts in correct alphabetical position by Tool Name within each service section 5. Tool Description Quality Validation: - Test your command descriptions for quality using the validation tool located at `eng/tools/ToolDescriptionEvaluator` before submitting: - **Single prompt validation** (test one description against one prompt): ```bash dotnet run -- --validate --tool-description "Your command description here" --prompt "typical user request" ``` - **Multiple prompt validation** (test one description against multiple prompts): ```bash dotnet run -- --validate \ --tool-description "Lists all storage accounts in a subscription" \ --prompt "show me my storage accounts" \ --prompt "list storage accounts" \ --prompt "what storage do I have" ``` - **Custom tools and prompts files** (use your own files for comprehensive testing): ```bash # Prompts: # Use markdown format (same as docs/e2eTestPrompts.md): dotnet run -- --prompts-file my-prompts.md # Use JSON format: dotnet run -- --prompts-file my-prompts.json # Tools: # Use JSON format (same as eng/tools/ToolDescriptionEvaluator/tools.json): dotnet run -- --tools-file my-tools.json # Combine both: # Use custom tools and prompts files together: dotnet run -- --tools-file my-tools.json --prompts-file my-prompts.md ``` - Quality assessment guidelines: - Aim for your description to rank in the top 3 results (GOOD or EXCELLENT rating) - Test with multiple different prompts that users might use - Consider common synonyms and alternative phrasings in your descriptions - If validation shows POOR results or a confidence score of < 0.4, refine your description and test again - Custom prompts file formats: - **Markdown format**: Use same table format as `docs/e2eTestPrompts.md`: ```markdown | Tool Name | Test Prompt | |:----------|:----------| | azmcp-your-command | Your test prompt | | azmcp-your-command | Another test prompt | ``` - **JSON format**: Tool name as key, array of prompts as value: ```json { "azmcp-your-command": [ "Your test prompt", "Another test prompt" ] } ``` - Custom tools file format: - Use the JSON format returned by calling the server command `azmcp-tools-list` or found in `eng/tools/ToolDescriptionEvaluator/tools.json`. 6. Live Test Infrastructure: - Use minimal resource configurations for cost efficiency - Follow naming conventions: `baseName` (most common) or `{baseName}-{area}` if needed - Include proper RBAC assignments for test application - Output all necessary identifiers for test consumption - Use appropriate Azure service API versions - Consider resource location constraints and availability ## Common Pitfalls to Avoid 1. Do not: - **CRITICAL**: Use `subscriptionId` as parameter name - Always use `subscription` to support both IDs and names - **CRITICAL**: Re-defining the global `resource-group` option. Use `UseResourceGroup()` / `RequireResourceGroup()` instead. - **CRITICAL**: Skip live test infrastructure for Azure service commands - Create `test-resources.bicep` template early in development - Redefine base class properties in Options classes - Skip base.RegisterOptions() call - Skip base.Dispose() call - Use hardcoded option strings - Return different response formats - Leave command unregistered - Skip error handling - Miss required tests - Deploy overly expensive test resources - Forget to assign RBAC permissions to test application - Hard-code resource names in live tests - Use dashes in command group names 2. Always: - Create a static {Area}OptionDefinitions class for the area - **For resource group handling**: Call `UseResourceGroup()` (optional) or `RequireResourceGroup()` (required). Never redefine the option or assign it manually. - **For Azure service commands**: Create test infrastructure (`test-resources.bicep`) before implementing live tests - Use OptionDefinitions for options - Follow exact file structure - Implement all base members - Add both unit and integration tests - Register in area setup RegisterCommands method - Handle all error cases - Use primary constructors - Make command classes sealed - Include live test infrastructure for Azure services - Use consistent resource naming patterns (check existing `test-resources.bicep` files) - Output resource identifiers from Bicep templates - Use concatenated all lowercase names for command groups (no dashes) ### Troubleshooting Common Issues ### Project Setup and Integration Issues **Issue: Solution file GUID conflicts** - **Cause**: Duplicate project GUIDs in the solution file causing build failures - **Solution**: Generate unique GUIDs for new projects when adding to `AzureMcp.sln` - **Fix**: Use Visual Studio or `dotnet sln add` command to properly add projects with unique GUIDs - **Prevention**: Always check for GUID uniqueness when manually editing solution files **Issue: Missing package references cause compilation errors** - **Cause**: Azure Resource Manager package not added to `Directory.Packages.props` before being referenced - **Solution**: Add package version to `Directory.Packages.props` first, then reference in project files - **Fix**: 1. Add `<PackageVersion Include="Azure.ResourceManager.{Service}" Version="{version}" />` to `Directory.Packages.props` 2. Add `<PackageReference Include="Azure.ResourceManager.{Service}" />` to project file - **Prevention**: Follow the two-step package addition process documented in Implementation Guidelines **Issue: Missing live test infrastructure for Azure service commands** - **Cause**: Forgetting to create `test-resources.bicep` template during development - **Solution**: Create Bicep template early in development process, not as an afterthought - **Fix**: Create `areas/{area-name}/tests/test-resources.bicep` following established patterns - **Prevention**: Check "Test Infrastructure Requirements" section at top of this document before starting implementation - **Validation**: Run `az bicep build --file areas/{area-name}/tests/test-resources.bicep` to validate template **Issue: Pipeline fails with "SelfContainedPostScript is not supported if there is no test-resources-post.ps1"** - **Cause**: Missing required `test-resources-post.ps1` file for Azure service commands - **Solution**: Create the post-deployment script file, even if it contains only the basic template - **Fix**: Create `areas/{area-name}/tests/test-resources-post.ps1` using the standard template from existing areas - **Prevention**: All Azure service commands must include this file - it's required by the test infrastructure - **Note**: The file is mandatory even if no custom post-deployment logic is needed **Issue: Test project compilation errors with missing imports** - **Cause**: Missing using statements for test frameworks and core libraries - **Solution**: Add required imports for test projects: - `using System.Text.Json;` for JSON serialization - `using Xunit;` for test framework - `using NSubstitute;` for mocking - `using AzureMcp.Tests;` for test base classes - **Fix**: Review test project template and ensure all necessary imports are included - **Prevention**: Use existing test projects as templates for import statements ### Azure Resource Manager Compilation Errors **Issue: Subscription not properly resolved** - **Cause**: Using direct ARM client creation instead of subscription service - **Solution**: Always inject and use `ISubscriptionService.GetSubscription()` - **Fix**: Replace manual subscription resource creation with service call - **Pattern**: ```csharp // Wrong - manual creation var armClient = await CreateArmClientAsync(null, retryPolicy); var subscriptionResource = armClient.GetSubscriptionResource(new ResourceIdentifier($"/subscriptions/{subscription}")); // Correct - use service var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); ``` **Issue: `cannot convert from 'System.Threading.CancellationToken' to 'string'`** - **Cause**: Wrong parameter order in resource manager method calls - **Solution**: Check method signatures; many Azure SDK methods don't take CancellationToken as second parameter - **Fix**: Use `.GetAsync(resourceName)` instead of `.GetAsync(resourceName, cancellationToken)` **Issue: `'SqlDatabaseData' does not contain a definition for 'CreationDate'`** - **Cause**: Property names in Azure SDK differ from expected/documented names - **Solution**: Use IntelliSense to explore actual property names - **Common fixes**: - `CreationDate` → `CreatedOn` - `EarliestRestoreDate` → `EarliestRestoreOn` - `Edition` → `CurrentSku?.Name` **Issue: `Operator '?' cannot be applied to operand of type 'AzureLocation'`** - **Cause**: Some Azure SDK types are structs, not nullable reference types - **Solution**: Convert to string: `Location.ToString()` instead of `Location?.Name` **Issue: Wrong resource access pattern** - **Problem**: Using `.GetSqlServerAsync(name, cancellationToken)` - **Solution**: Use resource collections: `.GetSqlServers().GetAsync(name)` - **Pattern**: Always access through collections, not direct async methods ### Live Test Infrastructure Issues **Issue: Bicep template validation fails** - **Cause**: Invalid parameter constraints, missing required properties, or API version issues - **Solution**: Use `az bicep build --file areas/{area-name}/tests/test-resources.bicep` to validate template - **Fix**: Check Azure Resource Manager template reference for correct syntax and required properties **Issue: Live tests fail with "Resource not found"** - **Cause**: Test resources not deployed or wrong naming pattern used - **Solution**: Verify resource deployment and naming in Azure portal - **Fix**: Ensure live tests use `Settings.ResourceBaseName` pattern for resource names (or appropriate service-specific pattern) **Issue: Permission denied errors in live tests** - **Cause**: Missing or incorrect RBAC assignments in Bicep template - **Solution**: Verify role assignment scope and principal ID - **Fix**: Check that `testApplicationOid` is correctly passed and role definition GUID is valid **Issue: Deployment fails with template validation errors** - **Cause**: Parameter constraints, resource naming conflicts, or invalid configurations - **Solution**: - Review deployment logs and error messages - Use `./eng/scripts/Deploy-TestResources.ps1 -Area {area-name} -Debug` for verbose deployment logs including resource provider errors. ### Live Test Project Configuration Issues **Issue: Live tests fail with "MCP server process exited unexpectedly" and "azmcp.exe not found"** - **Cause**: Incorrect project configuration in `AzureMcp.{Area}.LiveTests.csproj` - **Common Problem**: Referencing the area project (`AzureMcp.{Area}`) instead of the CLI project - **Solution**: Live test projects must reference `AzureMcp.Cli.csproj` and include specific project properties - **Required Configuration**: ```xml <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\..\src\AzureMcp.{Area}\AzureMcp.{Area}.csproj" /> <ProjectReference Include="..\..\..\..\core\src\AzureMcp.Cli\AzureMcp.Cli.csproj" /> </ItemGroup> </Project> ``` - **Key Requirements**: - `OutputType=Exe` - Required for live test execution - `IsTestProject=true` - Marks as test project - Reference to `AzureMcp.Cli.csproj` - Provides the executable for MCP server - Reference to area project - Provides the commands to test - **Common fixes**: - Adjust `@minLength`/`@maxLength` for service naming limits - Ensure unique resource names within scope - Use supported API versions for resource types - Verify location support for specific resource types **Issue: High deployment costs during testing** - **Cause**: Using expensive SKUs or resource configurations - **Solution**: Use minimal configurations for test resources - **Best practices**: - SQL: Use Basic tier with small capacity - Storage: Use Standard LRS with minimal replication - Cosmos: Use serverless or minimal RU/s allocation - Always specify cost-effective options in Bicep templates ### Service Implementation Issues **Issue: JSON Serialization Context missing new types** - **Cause**: New model classes not included in `{Area}JsonContext` causing serialization failures - **Solution**: Add all new model types to the JSON serialization context - **Fix**: Update `{Area}JsonContext.cs` to include `[JsonSerializable(typeof(NewModelType))]` attributes - **Prevention**: Always update JSON context when adding new model classes **Issue: Area not registered in Program.cs** - **Cause**: New area setup not added to `RegisterAreas()` method in `Program.cs` - **Solution**: Add area registration to the array in alphabetical order - **Fix**: Add `new AzureMcp.{Area}.{Area}Setup(),` to the `RegisterAreas()` return array - **Prevention**: Follow the complete area setup checklist including Program.cs registration **Issue: Using required ResourceGroup option for optional filtering** - **Cause**: Using `OptionDefinitions.Common.ResourceGroup` which has `IsRequired = true` for commands that should support optional resource group filtering - **Solution**: Create custom optional resource group option in area's OptionDefinitions - **Fix**: 1. Add `OptionalResourceGroup` option with `IsRequired = false` to `{Area}OptionDefinitions.cs` 2. Override base `_resourceGroupOption` field with `new` keyword in command class 3. Use the pattern: `private readonly new Option<string> _resourceGroupOption = {Area}OptionDefinitions.OptionalResourceGroup;` - **Prevention**: Check if resource group should be optional (e.g., for list commands) and use the optional pattern - **Examples**: Extension (AZQR), Monitor (Metrics), and ACR areas all implement this pattern correctly **Issue: HandleException parameter mismatch** - **Cause**: Confusion about the correct HandleException signature - **Solution**: Always use `HandleException(context, ex)` - this is the correct signature in BaseCommand - **Fix**: The method signature is `HandleException(CommandContext context, Exception ex)`, not `HandleException(context.Response, ex)` **Issue: Missing AddSubscriptionInformation** - **Cause**: Subscription commands need telemetry context - **Solution**: Add `context.Activity?.WithSubscriptionTag(options);` or use `AddSubscriptionInformation(context.Activity, options);` **Issue: Service not registered in DI** - **Cause**: Forgot to register service in area setup - **Solution**: Add `services.AddSingleton<IServiceInterface, ServiceImplementation>();` in ConfigureServices ### Base Command Class Issues **Issue: Wrong logger type in base command constructor** - **Example**: `ILogger<BaseSqlCommand<TOptions>>` in `BaseDatabaseCommand` - **Solution**: Use correct generic type: `ILogger<BaseDatabaseCommand<TOptions>>` **Issue: Missing using statements for TrimAnnotations** - **Solution**: Add `using AzureMcp.Core.Commands;` for `TrimAnnotations.CommandAnnotations` ### AOT Compilation Issues **Issue: AOT compilation fails with runtime dependencies** - **Cause**: Some Azure SDK packages or dependencies are not AOT (Ahead-of-Time) compilation compatible - **Symptoms**: Build errors when running `./eng/scripts/Build-Local.ps1 -BuildNative` - **Solution**: Exclude non-AOT safe projects and packages for native builds - **Fix Steps**: 1. **Move area setup under conditional compilation** in `core/src/AzureMcp.Cli/Program.cs`: ```csharp #if !BUILD_NATIVE new AzureMcp.{Area}.{Area}Setup(), #endif ``` 2. **Add conditional project exclusion** in `core/src/AzureMcp.Cli/AzureMcp.Cli.csproj`: ```xml <ItemGroup Condition="'$(BuildNative)' == 'true'"> <ProjectReference Remove="..\..\..\areas\{area-name}\src\AzureMcp.{Area}\AzureMcp.{Area}.csproj" /> </ItemGroup> ``` 3. **Remove problematic package references** when building native (if applicable): ```xml <ItemGroup Condition="'$(BuildNative)' == 'true'"> <PackageReference Remove="ProblematicPackage" /> </ItemGroup> ``` - **Examples**: See Cosmos, Monitor, Postgres, Search, VirtualDesktop, and BicepSchema areas in Program.cs and AzureMcp.Cli.csproj - **Prevention**: Test AOT compilation early in development using `./eng/scripts/Build-Local.ps1 -BuildNative` - **Note**: Areas excluded from AOT builds are still available in regular builds and deployments ## Checklist Before submitting: ### Core Implementation - [ ] Options class follows inheritance pattern - [ ] Command class implements all required members - [ ] Command uses proper OptionDefinitions - [ ] Service interface and implementation complete - [ ] Unit tests cover all paths - [ ] Integration tests added - [ ] Command registered in area setup RegisterCommands method - [ ] Follows file structure exactly - [ ] Error handling implemented - [ ] Documentation complete ### **CRITICAL: Live Test Infrastructure (Required for Azure Service Commands)** **⚠️ MANDATORY for any command that interacts with Azure resources:** - [ ] **Live test infrastructure created** (`test-resources.bicep` template in `areas/{area-name}/tests`) - [ ] **Post-deployment script created** (`test-resources-post.ps1` in `areas/{area-name}/tests` - required even if basic template) - [ ] **Bicep template validated** with `az bicep build --file areas/{area-name}/tests/test-resources.bicep` - [ ] **Live test resource template tested** with `./eng/scripts/Deploy-TestResources.ps1 -Area {area-name}` - [ ] **RBAC permissions configured** for test application in Bicep template (use appropriate built-in roles) - [ ] **Live test project configuration correct**: - [ ] References `AzureMcp.Cli.csproj` (not just the area project) - [ ] Includes `OutputType=Exe` property - [ ] Includes `IsTestProject=true` property - [ ] **Live tests use deployed resources** via `Settings.ResourceBaseName` pattern - [ ] **Resource outputs defined** in Bicep template for test consumption - [ ] **Cost optimization verified** (use Basic/Standard SKUs, minimal configurations) **Skip this section ONLY if your command does not interact with Azure resources (e.g., CLI wrappers, best practices tools).** ### Package and Project Setup - [ ] Azure Resource Manager package added to both `Directory.Packages.props` and `AzureMcp.{Area}.csproj` - [ ] **Package version consistency**: Same version used in both `Directory.Packages.props` and project references - [ ] **Solution file integration**: Projects added to `AzureMcp.sln` with unique GUIDs (no GUID conflicts) - [ ] **Area registration**: Added to `Program.cs` `RegisterAreas()` method in alphabetical order - [ ] JSON serialization context includes all new model types ### Build and Code Quality - [ ] No compiler warnings - [ ] Tests pass (run specific tests: `dotnet test --filter "FullyQualifiedName~YourCommandTests"`) - [ ] Build succeeds with `dotnet build` - [ ] Code formatting applied with `dotnet format` - [ ] Spelling check passes with `.\eng\common\spelling\Invoke-Cspell.ps1` - [ ] **AOT compilation verified** with `./eng/scripts/Build-Local.ps1 -BuildNative` - [ ] **Clean up unused using statements**: Run `dotnet format --include="areas/{area-name}/**/*.cs"` to remove unnecessary imports and ensure consistent formatting - [ ] Fix formatting issues with `dotnet format ./AzureMcp.sln` and ensure no warnings - [ ] Identify unused properties for Azure Resource with `.\eng\scripts\Check-Unused-ResourceProperties.ps1` ### Azure SDK Integration - [ ] All Azure SDK property names verified and correct - [ ] Resource access patterns use collections (e.g., `.GetSqlServers().GetAsync()`) - [ ] Subscription resolution uses `ISubscriptionService.GetSubscription()` - [ ] Service constructor includes `ISubscriptionService` injection for Azure resources ### Documentation Requirements **REQUIRED**: All new commands must update the following documentation files: - [ ] **CHANGELOG.md**: Add entry under "Unreleased" section describing the new command(s) - [ ] **docs/azmcp-commands.md**: Add command documentation with description, syntax, parameters, and examples - [ ] **README.md**: Update the supported services table and add example prompts demonstrating the new command(s) in the appropriate area section - [ ] **eng/vscode/README.md**: Update the VSIX README with new service area (if applicable) and add sample prompts to showcase new command capabilities - [ ] **docs/e2eTestPrompts.md**: Add test prompts for end-to-end validation of the new command(s) - [ ] **.github/CODEOWNERS**: Add new area to CODEOWNERS file for proper ownership and review assignments **Documentation Standards**: - Use consistent command paths in all documentation (e.g., `azmcp sql db show`, not `azmcp sql database show`) - Organize example prompts by service in README.md under service-specific sections (e.g., `### 🗄️ Azure SQL Database`) - Place new commands in the appropriate area section, or create a new area section if needed - Provide clear, actionable examples that users can run with placeholder values - Include parameter descriptions and required vs optional indicators in azmcp-commands.md - Keep CHANGELOG.md entries concise but descriptive of the capability added - Add test prompts to e2eTestPrompts.md following the established naming convention and provide multiple prompt variations - **eng/vscode/README.md Updates**: When adding new services or commands, update the VSIX README to maintain accurate service coverage and compelling sample prompts for marketplace visibility - **IMPORTANT**: Maintain alphabetical sorting in e2eTestPrompts.md: - Service sections must be in alphabetical order by service name - Tool Names within each table must be sorted alphabetically - When adding new tools, insert them in the correct alphabetical position to maintain sort order

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Azure/azure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server