Step-by-step workflow for adding a new command to the Azure Backup MCP toolset, ensuring it passes all validation gates before PR submission.
azmcp azurebackup <group> <operation> command.NET 10 SDK installed (see global.json)az login / Connect-AzAccount)upstream remote pointing to microsoft/mcp
upstream/main
Follow /servers/Azure.Mcp.Server/docs/new-command.md as the authoritative guide.
The Azure Backup toolset lives in tools/Azure.Mcp.Tools.AzureBackup/.
File: src/Options/{Group}/{Resource}{Operation}Options.cs
// Inherit from the appropriate base options class
public class MyNewOptions : BaseAzureBackupOptions
{
public string? MyParam { get; set; }
}
OptionDefinitions.Common.* for shared options (subscription, resourceGroup)AzureBackupOptionDefinitions.Vault and AzureBackupOptionDefinitions.VaultType for vault optionsAzureBackupOptionDefinitions if reusable across commands.AsRequired() / .AsOptional() extension methodsFile: src/Services/IAzureBackupService.cs and src/Services/AzureBackupService.cs
rsvOps or dppOps based on vault type using ResolveVaultTypeAsync
IRsvBackupOperations / RsvBackupOperations
IDppBackupOperations / DppBackupOperations
File: src/Commands/{Group}/{Resource}{Operation}Command.cs
Required patterns:
[CommandMetadata(...)] attribute (not property overrides)ILogger<T> and IAzureBackupService
RegisterOptions, BindOptions, ExecuteAsync
AzureBackupTelemetryTags.AddVaultTags(context.Activity, ...)
HandleException(context, ex) in catch blocksFile: src/AzureBackupSetup.cs
services.AddSingleton<MyNewCommand>() in ConfigureServices
group.AddCommand<MyNewCommand>(serviceProvider) in RegisterCommands
CommandGroup if needed for a new groupFile: src/Commands/AzureBackupJsonContext.cs
[JsonSerializable(typeof(MyNewCommand.MyResultType))] for AOT safetyBefore writing tests, validate all inputs are handled correctly:
Checklist:
ArgumentException with clear message when missingValidateSubscriptionFormat
ArgumentException.ThrowIfNullOrWhiteSpace
new ResourceIdentifier(...)
File: tests/Azure.Mcp.Tools.AzureBackup.Tests/{Group}/{Resource}{Operation}CommandTests.cs
public sealed class MyNewCommandTests : CommandUnitTestsBase<MyNewCommand, IAzureBackupService>
{
[Fact] public void Constructor_InitializesCommandCorrectly()
[Fact] public void BindOptions_BindsOptionsCorrectly()
[Fact] public async Task ExecuteAsync_ValidInput_ReturnsExpectedResult()
[Fact] public async Task ExecuteAsync_HandlesServiceErrors()
[Fact] public async Task ExecuteAsync_DeserializationValidation()
// Add per-parameter validation tests:
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task ExecuteAsync_InvalidVault_ThrowsArgumentException(string? vault)
// Add edge case tests specific to the command
}
dotnet test tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests `
/p:NuGetAudit=false `
--filter "Category!=Live&FullyQualifiedName~MyNewCommandTests"
Verify all tests pass before proceeding.
File: tests/Azure.Mcp.Tools.AzureBackup.Tests/AzureBackupCommandTests.cs
Azure Backup live tests use [Fact] on a RecordedCommandTestsBase subclass with CallToolAsync.
There is no [RecordedTest] attribute in this toolset.
[Fact]
public async Task MyNewCommand_RsvVault()
{
var result = await CallToolAsync(
"azurebackup", "mygroup", "myop",
new Dictionary<string, object>
{
["subscription"] = SubscriptionId,
["resourceGroup"] = ResourceGroupName,
["vault"] = DeploymentOutputs["AZUREBACKUP_RSV_VAULT_NAME"],
// add other params
});
Assert.NotNull(result);
// assert on result content
}
[Fact] for all live tests (not [RecordedTest] — that attribute is not used in Azure Backup)[LiveTestOnly] alongside [Fact] for long-running E2E tests that cannot reliably replayDeploymentOutputs (set in test-resources-post.ps1)If the new command requires new Azure resources:
tests/test-resources.bicep to add the resourcetests/test-resources-post.ps1 to output new deployment values./eng/scripts/Deploy-TestResources.ps1 -Paths AzureBackup
# Set to Record mode
$settings = @{
TestMode = "Record"
SubscriptionId = "<your-sub>"
TenantId = "<your-tenant>"
ResourceGroupName = "<your-rg>"
ResourceBaseName = "<your-base>"
} | ConvertTo-Json
$settings | Set-Content "tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests\.testsettings.json"
# Kill any stale proxy/server processes
Stop-Process -Name "Azure.Sdk.Tools.TestProxy","azmcp" -Force -ErrorAction SilentlyContinue
# Run tests in Record mode
dotnet test tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests `
/p:NuGetAudit=false --filter "FullyQualifiedName~MyNewCommand"
# Push recorded sessions to azure-sdk-assets
.proxy\Azure.Sdk.Tools.TestProxy push `
-a tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests\assets.json
This updates the Tag field in assets.json. Commit the updated assets.json.
# Switch to Playback mode
$settings = @{ TestMode = "Playback"; SubscriptionId = "..."; TenantId = "..."; ResourceGroupName = "..."; ResourceBaseName = "..." } | ConvertTo-Json
$settings | Set-Content "tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests\.testsettings.json"
Stop-Process -Name "Azure.Sdk.Tools.TestProxy","azmcp" -Force -ErrorAction SilentlyContinue
dotnet test tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests `
/p:NuGetAudit=false --filter "FullyQualifiedName~MyNewCommand"
All recorded tests must pass in Playback mode.
Run these checks in order. All must pass before creating a PR.
dotnet build tools\Azure.Mcp.Tools.AzureBackup\src\Azure.Mcp.Tools.AzureBackup.csproj /p:NuGetAudit=false
dotnet format Microsoft.Mcp.slnx --verify-no-changes `
--include "tools/Azure.Mcp.Tools.AzureBackup/**" `
--exclude-diagnostics IL2026 IL3050
If it fails, fix with:
dotnet format Microsoft.Mcp.slnx `
--include "tools/Azure.Mcp.Tools.AzureBackup/**" `
--exclude-diagnostics IL2026 IL3050
dotnet test tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests /p:NuGetAudit=false
dotnet test tools\Azure.Mcp.Tools.AzureBackup\tests\Azure.Mcp.Tools.AzureBackup.Tests /p:NuGetAudit=false
.\eng\common\spelling\Invoke-Cspell.ps1
If new technical terms are flagged, add them to .vscode/cspell.json.
./eng/scripts/Build-Local.ps1 -UsePaths -VerifyNpx
Azure Backup is marked IsAotCompatible=true, so also validate native compilation:
./eng/scripts/Build-Local.ps1 -BuildNative
If this fails for a new Azure SDK dependency, the toolset may need to be excluded
from native builds (see docs/aot-compatibility.md).
Run the ToolDescriptionEvaluator to verify the new tool's description is discoverable by AI agents.
$env:AOAI_ENDPOINT = "<your-aoai-endpoint>"
$env:TEXT_EMBEDDING_API_KEY = "<your-key>"
dotnet run --project eng/tools/ToolDescriptionEvaluator/src/ToolDescriptionEvaluator.csproj `
-- --tool-name "azurebackup_<group>_<operation>"
Target: Top 3 ranking with confidence score >= 0.4.
If the score is low, improve the command's Description in the [CommandMetadata] attribute:
File: servers/Azure.Mcp.Server/docs/azmcp-commands.md
Add the new command in alphabetical order within the azurebackup section.
Then regenerate the commands metadata:
./eng/scripts/Update-AzCommandsMetadata.ps1
This is required for CI validation.
File: servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Add 2-3 natural language prompts that should trigger the new tool, in alphabetical order.
Follow docs/changelog-entries.md instructions. Use the -ChangelogPath parameter pointing to
servers/Azure.Mcp.Server/CHANGELOG.md.
Before creating the PR, verify:
dotnet build succeeds with 0 errorsdotnet format --verify-no-changes passesassets.json updatedInvoke-Cspell.ps1 cleanAzureBackupSetup.cs
AzureBackupTelemetryTags
Update-AzCommandsMetadata.ps1
Build-Local.ps1 -BuildNative)git add tools/Azure.Mcp.Tools.AzureBackup/ servers/Azure.Mcp.Server/ README.md eng/vscode/README.md
git commit -m "feat(azurebackup): Add <group> <operation> command
<description of what the command does>"
git push origin <branch-name>
tools/Azure.Mcp.Tools.AzureBackup/
├── src/
│ ├── AzureBackupSetup.cs # Register here
│ ├── Commands/
│ │ ├── AzureBackupJsonContext.cs # AOT registration
│ │ └── {Group}/{Resource}{Operation}Command.cs # Command impl
│ ├── Options/
│ │ ├── AzureBackupOptionDefinitions.cs # Shared options
│ │ └── {Group}/{Resource}{Operation}Options.cs # Command options
│ ├── Services/
│ │ ├── IAzureBackupService.cs # Interface
│ │ ├── AzureBackupService.cs # Routing
│ │ ├── RsvBackupOperations.cs # RSV impl
│ │ └── DppBackupOperations.cs # DPP impl
│ └── Models/
│ └── AzureBackupTelemetryTags.cs # Telemetry
└── tests/
├── Azure.Mcp.Tools.AzureBackup.Tests/
│ ├── {Group}/{Resource}{Operation}CommandTests.cs
│ ├── AzureBackupCommandTests.cs # Add tests here
│ └── assets.json # Recording tag
├── test-resources.bicep # Azure infra
└── test-resources-post.ps1 # Post-deploy
Study these existing implementations as templates:
Commands/Vault/VaultGetCommand.cs
Commands/Policy/PolicyCreateCommand.cs
Commands/Governance/GovernanceSoftDeleteCommand.cs
Commands/Security/SecurityConfigureMuaCommand.cs
tests/Azure.Mcp.Tools.AzureBackup.Tests/Policy/PolicyCreateCommandTests.cs