Skip to content
Merged
41 changes: 40 additions & 1 deletion .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,45 @@
"navm",
"rego",
"virtualmachine",
"virtualnetwork"
"virtualnetwork",
"afsfileshare",
"azurebackup",
"iaasvmcontainerv",
"mydisk",
"myvm",
"protecteditem",
"slowvm",
"adls",
"alwayson",
"backupconfig",
"dbname",
"disasterrecovery",
"esan",
"fileshare",
"hana",
"lifecycles",
"pgflex",
"protectable",
"protectableitem",
"recoverypoint",
"saphana",
"saphanadatabase",
"saphanadbi",
"saphanadbinstance",
"saphanasystem",
"sqldatabase",
"sqlinstance",
"systemassigned",
"systemassigneduserassigned",
"userassigned",
"vaultconfig",
"vmname",
"azurebackuprg",
"diskname",
"georedundant",
"locallyredundant",
"undelete",
"undeletes",
"zoneredundant"
]
}
4 changes: 4 additions & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The Azure MCP Server updates automatically by default whenever a new release com

### Bugs Fixed

- Fixed `azurebackup vault create --vault-type dpp` to enable a System-Assigned Managed Identity by default. Without this, every subsequent `protecteditem protect` call against the new DPP vault would fail server-side with `VaultMSIUnauthorized`. Existing vaults are unaffected; use `azurebackup vault update --identity-type SystemAssigned` if needed.
- Fixed `azurebackup protecteditem protect` to surface the real outcome of the protect operation instead of returning a synthetic "Accepted" with a base64 job id that callers could not resolve. RSV now polls the underlying `ConfigureBackup` job to a terminal state and returns its real status (`Completed`, `CompletedWithWarnings`, `Failed`, `Cancelled`, or `InProgress` if polling exceeds the budget) along with any error details. DPP now waits for the protect operation to complete and reads back the backup instance, returning the actual `protectionStatus` (e.g. `ProtectionConfigured`); DPP responses no longer carry a misleading `jobId` because DPP protection is not surfaced as a job — use `azurebackup protecteditem get` or `list` to verify.
- Fixed `azurebackup protecteditem protect` for IaaS VM (RSV) so it submits the protect request directly instead of doing a 180-second container-discovery pre-poll. Freshly created VMs no longer time out; container registration is performed by the Recovery Services backend as part of accepting the protect request, matching the behavior of `az backup protection enable-for-vm`.

### Other Changes

## 3.0.0-beta.5 (2026-04-23)
Expand Down
4 changes: 2 additions & 2 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ azmcp appservice webapp diagnostic diagnose --subscription "my-subscription" \
#### Vault

```bash
# Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault or 'dpp' for a Backup vault (Data Protection). Returns the created vault details.
# Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault or 'dpp' for a Backup vault (Data Protection). For DPP vaults a System-Assigned Managed Identity is enabled by default so the vault can authenticate to protected datasources (storage accounts, disks, PG Flex, etc.) - change later with 'azurebackup vault update --identity-type ...' if needed. Returns the created vault details.
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp azurebackup vault create --subscription <subscription> \
--resource-group <resource-group> \
Expand Down Expand Up @@ -823,7 +823,7 @@ azmcp azurebackup protecteditem get --subscription <subscription> \
[--protected-item <protected-item>] \
[--container <container>]

# Enables backup protection for a resource by creating a protected item or backup instance.
# Enables backup protection for a resource by creating a protected item or backup instance. For RSV the tool waits for the underlying ConfigureBackup job to reach a terminal state and returns the final job status; for DPP the tool waits for the protect operation to complete and reads back the backup instance, returning ProtectionStatus (DPP protection is not a job - use 'azurebackup protecteditem get' or 'list' to verify).
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp azurebackup protecteditem protect --subscription <subscription> \
--resource-group <resource-group> \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ public sealed class VaultCreateCommand(ILogger<VaultCreateCommand> logger, IAzur
public override string Description =>
"""
Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault
or 'dpp' for a Backup vault (Data Protection). Returns the created vault details.
or 'dpp' for a Backup vault (Data Protection). For DPP vaults a System-Assigned
Managed Identity is enabled by default so the vault can authenticate to protected
datasources (storage accounts, disks, PG Flex, etc.) - change later with
'azurebackup vault update --identity-type ...' if needed. Returns the created
vault details.
""";
public override string Title => CommandTitle;
public override ToolMetadata Metadata => new()
Expand Down
35 changes: 34 additions & 1 deletion tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,41 @@

namespace Azure.Mcp.Tools.AzureBackup.Models;

/// <summary>
/// Result of an <c>azurebackup protecteditem protect</c> call.
/// </summary>
/// <param name="Status">
/// Final outcome of the protect operation as observed by MCP after polling.
/// For RSV: terminal status of the ConfigureBackup job (e.g. <c>Completed</c>,
/// <c>CompletedWithWarnings</c>, <c>Failed</c>, <c>Cancelled</c>) or <c>InProgress</c>
/// if the job is still running when the polling budget is exhausted.
/// For DPP: <c>Succeeded</c> when the backup instance reaches <c>ProtectionConfigured</c>
/// (or <c>ConfiguringProtection</c> if still finalizing) or <c>Failed</c> on error.
/// </param>
/// <param name="ProtectedItemName">
/// RSV protected item name or DPP backup instance name. Use this with
/// <c>azurebackup protecteditem get</c>.
/// </param>
/// <param name="JobId">
/// RSV ConfigureBackup job id (use with <c>azurebackup job get</c>). Always
/// <c>null</c> for DPP — DPP protection is not surfaced as a job; verify with
/// <c>azurebackup protecteditem get</c> or <c>list</c>.
/// </param>
/// <param name="Message">Human-readable summary of the outcome.</param>
/// <param name="ProtectionStatus">
/// DPP only — actual <c>protectionStatus.status</c> read back from the backup
/// instance after the operation (e.g. <c>ProtectionConfigured</c>,
/// <c>ConfiguringProtection</c>, <c>ProtectionError</c>).
/// </param>
/// <param name="ErrorMessage">
/// Error detail when <see cref="Status"/> is <c>Failed</c>. For RSV this comes
/// from the failed ConfigureBackup job; for DPP it comes from the
/// async <c>operationStatus</c> error envelope.
/// </param>
public sealed record ProtectResult(
string Status,
string? ProtectedItemName,
string? JobId,
string? Message);
string? Message,
string? ProtectionStatus = null,
string? ErrorMessage = null);
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@ public async Task<VaultCreateResult> CreateVaultAsync(
}
};

var vaultData = new DataProtectionBackupVaultData(new AzureLocation(location), new DataProtectionBackupVaultProperties(storageSettings));
var vaultData = new DataProtectionBackupVaultData(new AzureLocation(location), new DataProtectionBackupVaultProperties(storageSettings))
{
// DPP (Backup Vault) requires a Managed Identity to authenticate to protected
// datasources (storage accounts, disks, PG Flex, etc.). Without it every
// 'protecteditem protect' call would fail server-side with VaultMSIUnauthorized.
// Default to SystemAssigned so the vault is usable out of the box; callers can
// change this later via 'vault update --identity-type ...'.
Identity = new Azure.ResourceManager.Models.ManagedServiceIdentity(
Azure.ResourceManager.Models.ManagedServiceIdentityType.SystemAssigned)
};

var result = await collection.CreateOrUpdateAsync(WaitUntil.Completed, vaultName, vaultData, cancellationToken);

Expand Down Expand Up @@ -197,15 +206,55 @@ public async Task<ProtectResult> ProtectItemAsync(
Properties = instanceProperties
};

var result = await collection.CreateOrUpdateAsync(WaitUntil.Started, instanceName, instanceData, cancellationToken);
// DPP protection is asynchronous on the server side and is NOT surfaced as a
// backup job (only on-demand backup, restore, etc. are jobs). MCP must therefore
// wait for the underlying operationStatus to reach a terminal state and then read
// back the BackupInstance to confirm the protection actually configured. Using
// WaitUntil.Completed lets the SDK poll the Azure-AsyncOperation header for us
// and surface the real server-side error (e.g. VaultMSIUnauthorized) as a
// RequestFailedException, instead of silently returning "Accepted".
ArmOperation<DataProtectionBackupInstanceResource> operation;
try
{
operation = await collection.CreateOrUpdateAsync(
WaitUntil.Completed, instanceName, instanceData, cancellationToken);
}
catch (RequestFailedException ex)
{
return new ProtectResult(
"Failed",
instanceName,
JobId: null,
$"Protection failed for backup instance '{instanceName}': {ex.Message}",
ProtectionStatus: null,
ErrorMessage: ex.Message);
}
Comment thread
shrja-ms marked this conversation as resolved.

var jobId = ExtractJobIdFromOperation(result.GetRawResponse());
// Re-read the backup instance to capture the authoritative protection status.
// The LRO can complete while the BI is still in ConfiguringProtection; both
// outcomes are surfaced to the caller via ProtectionStatus. If the re-read
// fails with a transient error, report success (protection did complete) and
// let the caller verify with 'protecteditem get'.
string? protectionStatus = null;
try
{
var instanceResource = armClient.GetDataProtectionBackupInstanceResource(operation.Value.Id);
var bi = await instanceResource.GetAsync(cancellationToken);
protectionStatus = bi.Value.Data.Properties?.ProtectionStatus?.Status?.ToString();
}
catch (RequestFailedException)
{
// Transient re-read failure; protection itself succeeded.
}

return new ProtectResult(
"Accepted",
"Succeeded",
instanceName,
jobId,
jobId != null ? $"Protection initiated. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Protection initiated.");
JobId: null,
$"Protection configured for backup instance '{instanceName}' (status: {protectionStatus ?? "Unknown"}). " +
$"Use 'azurebackup protecteditem get --protected-item {instanceName}' to view details.",
ProtectionStatus: protectionStatus,
ErrorMessage: null);
}

public async Task<ProtectedItemInfo> GetProtectedItemAsync(
Expand Down
Loading
Loading