From c4d80f9591f354398dc3886acd02d249ddeb7559 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Wed, 3 Jun 2026 11:23:48 +0530 Subject: [PATCH 1/3] Fix AzureBackup vault list classification and DPP policy FormatException NEW-5: When azurebackup_vault_list and azurebackup_governance_find-unprotected have both RSV and DPP backends fail with RequestFailedException, surface a single RequestFailedException (preserving HTTP status and combining both messages) instead of an InvalidOperationException so the failure is correctly classified as an Azure service error rather than an MCP-side bug. NEW-6: Fix azurebackup_policy_get and azurebackup_policy_list (DPP vaults) failing with FormatException from XmlConvert.ToTimeSpan when policies contain ISO 8601 duration values the Azure SDK cannot parse. policy_list now skips unparseable policies; policy_get falls back to listing the vault's policies and matching by name. --- ...vault-list-and-policy-format-exception.yml | 5 ++ .../src/Services/AzureBackupService.cs | 11 +++++ .../src/Services/DppBackupOperations.cs | 46 +++++++++++++++++-- .../Services/AzureBackupServiceTests.cs | 32 ++++++++++--- 4 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 servers/Azure.Mcp.Server/changelog-entries/azurebackup-fix-vault-list-and-policy-format-exception.yml diff --git a/servers/Azure.Mcp.Server/changelog-entries/azurebackup-fix-vault-list-and-policy-format-exception.yml b/servers/Azure.Mcp.Server/changelog-entries/azurebackup-fix-vault-list-and-policy-format-exception.yml new file mode 100644 index 0000000000..8371f04534 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/azurebackup-fix-vault-list-and-policy-format-exception.yml @@ -0,0 +1,5 @@ +changes: + - section: "Bugs Fixed" + description: "Fixed `azurebackup_vault_list` and `azurebackup_governance_find-unprotected` so that when both the RSV and DPP backends fail with `RequestFailedException`, a single `RequestFailedException` is surfaced (preserving the HTTP status and combining both error messages) instead of an `InvalidOperationException`. This ensures the failure is correctly classified as an Azure service error rather than an MCP-side bug." + - section: "Bugs Fixed" + description: "Fixed `azurebackup_policy_get` and `azurebackup_policy_list` (DPP vaults) failing with `FormatException` from `XmlConvert.ToTimeSpan` when policies contain ISO 8601 duration values that the Azure SDK cannot parse (e.g. fractional seconds, week designators in retention rules). `policy_list` now skips policies that the SDK cannot parse and returns the rest; `policy_get` falls back to listing the vault's policies and matching by name. Tracks the Azure SDK gap in `Azure.ResourceManager.DataProtectionBackup`." diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs index 89c6041139..ef7b971785 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -61,6 +61,17 @@ private static Exception BuildBothVaultListingsFailedException( $"RSV error: {rsvInner.GetType().Name}: {rsvInner.Message} " + $"DPP error: {dppInner.GetType().Name}: {dppInner.Message}"; + if (rsvInner is RequestFailedException rsvRfe && dppInner is RequestFailedException dppRfe) + { + // NEW-5 fix: when both inners are RequestFailedException, return a + // RequestFailedException so the command-layer error mapper classifies the + // failure as an Azure service error (with the original HTTP status code) + // rather than as an MCP-side bug. Prefer the first non-zero status. + var status = rsvRfe.Status != 0 ? rsvRfe.Status : dppRfe.Status; + var errorCode = rsvRfe.ErrorCode ?? dppRfe.ErrorCode; + return new RequestFailedException(status, combinedMessage, errorCode, rsvRfe); + } + return new InvalidOperationException(combinedMessage, rsvInner); } diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index 4b13ec6e05..ef15273ce8 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -421,9 +421,23 @@ public async Task GetPolicyAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); - var policy = await policyResource.GetAsync(cancellationToken); - return MapToPolicyInfo(policy.Value.Data); + try + { + var policy = await policyResource.GetAsync(cancellationToken); + return MapToPolicyInfo(policy.Value.Data); + } + catch (FormatException) + { + // The Azure SDK may throw FormatException when deserializing the policy's + // retention/duration fields (XmlConvert.ToTimeSpan limitation in + // DataProtectionBackupAbsoluteDeleteSetting). Fall back to listing all + // policies and matching by name to work around this SDK limitation. + var policies = await ListPoliciesAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + return policies.FirstOrDefault(p => p.Name == policyName) + ?? throw new InvalidOperationException( + $"Policy '{policyName}' not found or cannot be parsed by the Azure SDK due to an unsupported retention/duration field."); + } } public async Task> ListPoliciesAsync( @@ -441,9 +455,33 @@ public async Task> ListPoliciesAsync( var collection = vaultResource.GetDataProtectionBackupPolicies(); var policies = new List(); - await foreach (var policy in collection.GetAllAsync(cancellationToken)) + var enumerator = collection.GetAllAsync(cancellationToken).GetAsyncEnumerator(cancellationToken); + try { - policies.Add(MapToPolicyInfo(policy.Data)); + while (true) + { + try + { + if (!await enumerator.MoveNextAsync()) + { + break; + } + + policies.Add(MapToPolicyInfo(enumerator.Current.Data)); + } + catch (FormatException) + { + // The Azure SDK may throw FormatException when deserializing policies with + // non-standard ISO 8601 retention/duration fields (XmlConvert.ToTimeSpan + // limitation in DataProtectionBackupAbsoluteDeleteSetting). Return the + // policies collected so far rather than failing the entire list. + break; + } + } + } + finally + { + await enumerator.DisposeAsync(); } return policies; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs index e8cdda6020..a3e7837418 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs @@ -228,24 +228,42 @@ public async Task ListVaultsAsync_DppFails_ReturnsRsvOnly() } [Fact] - public async Task ListVaultsAsync_BothFailWithSameStatus_ThrowsInvalidOperationWithBothMessages() + public async Task ListVaultsAsync_BothFailWithRequestFailedException_ThrowsRequestFailedException() { - // NEW-1: when both backends fail with the same HTTP status, the two services can - // carry meaningfully different ErrorCodes / messages, so neither side is dropped - - // both inner messages are folded into the resulting InvalidOperationException's - // message and the RSV exception is preserved as InnerException for diagnostics. + // NEW-1: both inner messages are preserved in the combined message. + // NEW-5: when both inners are RequestFailedException, the wrapper itself is a + // RequestFailedException so the command-layer error mapper classifies the failure + // as an Azure service error (with the original HTTP status code) rather than as + // an MCP-side bug. _rsvOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) .ThrowsAsync(new RequestFailedException(403, "RSV error")); _dppOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) .ThrowsAsync(new RequestFailedException(403, "DPP error")); - var ex = await Assert.ThrowsAsync(() => + var ex = await Assert.ThrowsAsync(() => _service.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, null, null, CancellationToken.None)); + Assert.Equal(403, ex.Status); Assert.Contains("RSV error", ex.Message); Assert.Contains("DPP error", ex.Message); Assert.IsType(ex.InnerException); - Assert.Equal(403, ((RequestFailedException)ex.InnerException!).Status); + } + + [Fact] + public async Task ListVaultsAsync_BothFailWithRequestFailedException_PrefersNonZeroStatus() + { + // NEW-5: when the RSV RequestFailedException carries a 0 status (e.g. transport-level + // failure with no HTTP status), prefer the DPP status so the user still sees a + // useful HTTP code. + _rsvOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) + .ThrowsAsync(new RequestFailedException(0, "RSV transport error")); + _dppOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) + .ThrowsAsync(new RequestFailedException(503, "DPP throttled")); + + var ex = await Assert.ThrowsAsync(() => + _service.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, null, null, CancellationToken.None)); + + Assert.Equal(503, ex.Status); } [Fact] From 2969fcbb41c6dd546a406c1a7026a1f63307cbec Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Wed, 3 Jun 2026 11:39:49 +0530 Subject: [PATCH 2/3] Address PR review: pair RFE status+errorCode from same source; skip-not-break on per-item FormatException 1. AzureBackupService.BuildBothVaultListingsFailedException now picks one source exception (preferring non-zero HTTP status) and takes both Status and ErrorCode from it, so callers never see a mismatched (Status, ErrorCode) pair. 2. DppBackupOperations.ListPoliciesAsync and ListJobsAsync now continue past a single-item FormatException instead of breaking out of the enumerator. A small consecutive-failure cap (3) prevents infinite loops when the SDK enumerator is left in a permanently-broken state by a page-level deserialization failure. This also makes GetPolicyAsync's list-and-match fallback resilient to bad policies positioned before the requested one. Adds a NEW-5 test asserting the (Status, ErrorCode) pair is sourced consistently. --- .../src/Services/AzureBackupService.cs | 9 ++--- .../src/Services/DppBackupOperations.cs | 35 ++++++++++++++----- .../Services/AzureBackupServiceTests.cs | 19 ++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs index ef7b971785..d6245e9f15 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -66,10 +66,11 @@ private static Exception BuildBothVaultListingsFailedException( // NEW-5 fix: when both inners are RequestFailedException, return a // RequestFailedException so the command-layer error mapper classifies the // failure as an Azure service error (with the original HTTP status code) - // rather than as an MCP-side bug. Prefer the first non-zero status. - var status = rsvRfe.Status != 0 ? rsvRfe.Status : dppRfe.Status; - var errorCode = rsvRfe.ErrorCode ?? dppRfe.ErrorCode; - return new RequestFailedException(status, combinedMessage, errorCode, rsvRfe); + // rather than as an MCP-side bug. Pick a single source for the + // (Status, ErrorCode) pair so they are guaranteed to come from the same + // exception - prefer the side that reports a non-zero HTTP status. + var primary = rsvRfe.Status != 0 ? rsvRfe : dppRfe; + return new RequestFailedException(primary.Status, combinedMessage, primary.ErrorCode, primary); } return new InvalidOperationException(combinedMessage, rsvInner); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index ef15273ce8..816b359edc 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -456,6 +456,15 @@ public async Task> ListPoliciesAsync( var policies = new List(); var enumerator = collection.GetAllAsync(cancellationToken).GetAsyncEnumerator(cancellationToken); + // The Azure SDK may throw FormatException when deserializing policies with + // non-standard ISO 8601 retention/duration fields (XmlConvert.ToTimeSpan + // limitation in DataProtectionBackupAbsoluteDeleteSetting). When that happens + // we try to skip past the offending item and continue, so valid policies that + // appear after a bad one are still returned. If the SDK enumerator becomes + // unusable (typical for page-level deserialization failures), MoveNextAsync + // will keep throwing - cap consecutive failures so we cannot loop forever. + const int maxConsecutiveFailures = 3; + var consecutiveFailures = 0; try { while (true) @@ -468,14 +477,14 @@ public async Task> ListPoliciesAsync( } policies.Add(MapToPolicyInfo(enumerator.Current.Data)); + consecutiveFailures = 0; } catch (FormatException) { - // The Azure SDK may throw FormatException when deserializing policies with - // non-standard ISO 8601 retention/duration fields (XmlConvert.ToTimeSpan - // limitation in DataProtectionBackupAbsoluteDeleteSetting). Return the - // policies collected so far rather than failing the entire list. - break; + if (++consecutiveFailures >= maxConsecutiveFailures) + { + break; + } } } } @@ -580,6 +589,13 @@ public async Task> ListJobsAsync( var jobs = new List(); var enumerator = collection.GetAllAsync(cancellationToken).GetAsyncEnumerator(cancellationToken); + // The Azure SDK may throw FormatException when deserializing jobs with + // non-standard ISO 8601 duration fields (XmlConvert.ToTimeSpan limitation). + // Try to skip past the offending item so valid jobs after a bad one are still + // returned; cap consecutive failures so a permanently-broken enumerator + // (typical for page-level deserialization failures) cannot loop forever. + const int maxConsecutiveFailures = 3; + var consecutiveFailures = 0; try { while (true) @@ -592,13 +608,14 @@ public async Task> ListJobsAsync( } jobs.Add(MapToJobInfo(enumerator.Current.Data)); + consecutiveFailures = 0; } catch (FormatException) { - // The Azure SDK may throw FormatException when deserializing jobs with - // non-standard ISO 8601 duration fields (XmlConvert.ToTimeSpan limitation). - // Return the jobs collected so far rather than failing the entire list. - break; + if (++consecutiveFailures >= maxConsecutiveFailures) + { + break; + } } } } diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs index a3e7837418..d2d23aa2be 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.Tests/Services/AzureBackupServiceTests.cs @@ -266,6 +266,25 @@ public async Task ListVaultsAsync_BothFailWithRequestFailedException_PrefersNonZ Assert.Equal(503, ex.Status); } + [Fact] + public async Task ListVaultsAsync_BothFailWithRequestFailedException_StatusAndErrorCodePairedFromSameSource() + { + // NEW-5: Status and ErrorCode must come from the same exception so callers + // never see a mismatched (Status, ErrorCode) pair. Here RSV has Status=0 with + // its own ErrorCode; the wrapper should take both fields from DPP (the source + // of the non-zero Status). + _rsvOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) + .ThrowsAsync(new RequestFailedException(0, "RSV transport error", "RsvOnlyCode", null)); + _dppOps.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, Arg.Any()) + .ThrowsAsync(new RequestFailedException(503, "DPP throttled", "DppThrottled", null)); + + var ex = await Assert.ThrowsAsync(() => + _service.ListVaultsAsync("22222222-2222-2222-2222-222222222222", null, null, null, null, CancellationToken.None)); + + Assert.Equal(503, ex.Status); + Assert.Equal("DppThrottled", ex.ErrorCode); + } + [Fact] public async Task ListVaultsAsync_BothFailWithDifferentExceptions_ThrowsInvalidOperationWithBothMessages() { From 5b654c18b4e61fbdbc24e9463d47a72eb040aebb Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Wed, 3 Jun 2026 17:08:03 +0530 Subject: [PATCH 3/3] azurebackup: add actionable error messages for container-name/RBAC errors; fix KQL classifier - RecoveryPointGetCommand: add BMSUserErrorContainerNameIncorrectFormat and 403 AuthorizationFailed error messages with guidance - ProtectedItemGetCommand: add BMSUserErrorContainerNameIncorrectFormat, BMSUserErrorProtectedItemNameIncorrectFormat, and 403 error messages - KQL classifier: add ValidationError, UnauthorizedAccessException, and CredentialUnavailableException to Customer bucket (reclassifies 26 Unknown hits) - SKILL.md: update error classification decision tree to match --- .../skills/azurebackup-telemetry-report/SKILL.md | 8 +++++++- .../references/kql-queries.md | 9 +++++++++ .../Commands/ProtectedItem/ProtectedItemGetCommand.cs | 6 ++++++ .../Commands/RecoveryPoint/RecoveryPointGetCommand.cs | 4 ++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/SKILL.md b/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/SKILL.md index 1583808879..862374ad58 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/SKILL.md +++ b/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/SKILL.md @@ -164,7 +164,13 @@ Is success == true? Is ExceptionType FormatException / ArgumentNullException / ArgumentException / InvalidOperationException? └─ YES → "MCP Tool Bug" └─ NO → - Is StatusCode 400/403/404? + Is ExceptionType ValidationError? + └─ YES → "Customer (4xx)" (user omitted required options) + Is ExceptionType System.UnauthorizedAccessException? + └─ YES → "Customer (4xx)" (broken credentials / expired token) + Is ExceptionType Azure.Identity.CredentialUnavailableException? + └─ YES → "Customer (4xx)" (auth not configured) + Is StatusCode 400/401/403/404? └─ YES → "Customer (4xx)" └─ NO → Is ExceptionType Azure.RequestFailedException with 5xx? diff --git a/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/references/kql-queries.md b/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/references/kql-queries.md index 8dcf14598c..361edd9a8f 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/references/kql-queries.md +++ b/tools/Azure.Mcp.Tools.AzureBackup/skills/azurebackup-telemetry-report/references/kql-queries.md @@ -25,8 +25,11 @@ getAzureMcpEvents_ToolCalls(ago(7d), now()) | extend ErrorCategory = case( success == true, "Success", ExType in ("System.FormatException", "System.ArgumentNullException", "System.ArgumentException", "System.InvalidOperationException"), "McpToolBug", + ExType == "ValidationError", "Customer", + ExType == "System.UnauthorizedAccessException", "Customer", StatusCode >= 400 and StatusCode < 500, "Customer", ExType == "System.Collections.Generic.KeyNotFoundException", "Customer", + ExType == "Azure.Identity.CredentialUnavailableException", "Customer", ExType == "Azure.RequestFailedException" and StatusCode >= 500, "AzureService", ExType == "System.AggregateException", "AzureService", "Unknown") @@ -63,8 +66,11 @@ getAzureMcpEvents_ToolCalls(TwoWeeksAgo, now()) | extend ErrorCategory = case( success == true, "Success", ExType in ("System.FormatException", "System.ArgumentNullException", "System.ArgumentException", "System.InvalidOperationException"), "McpToolBug", + ExType == "ValidationError", "Customer", + ExType == "System.UnauthorizedAccessException", "Customer", StatusCode >= 400 and StatusCode < 500, "Customer", ExType == "System.Collections.Generic.KeyNotFoundException", "Customer", + ExType == "Azure.Identity.CredentialUnavailableException", "Customer", ExType == "Azure.RequestFailedException" and StatusCode >= 500, "AzureService", ExType == "System.AggregateException", "AzureService", "Unknown") @@ -112,8 +118,11 @@ getAzureMcpEvents_ToolCalls(ago(7d), now()) | extend StatusCode = toint(extract(@"StatusCode.*?(\d+)", 1, ExMsg)) | extend ErrorCategory = case( ExType in ("System.FormatException", "System.ArgumentNullException", "System.ArgumentException", "System.InvalidOperationException"), "McpToolBug", + ExType == "ValidationError", "Customer", + ExType == "System.UnauthorizedAccessException", "Customer", StatusCode >= 400 and StatusCode < 500, "Customer", ExType == "System.Collections.Generic.KeyNotFoundException", "Customer", + ExType == "Azure.Identity.CredentialUnavailableException", "Customer", ExType == "Azure.RequestFailedException" and StatusCode >= 500, "AzureService", ExType == "System.AggregateException", "AzureService", "Unknown") diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs index 5b4dca9bbf..5a8d46d0cd 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs @@ -117,6 +117,12 @@ public override async Task ExecuteAsync(CommandContext context, KeyNotFoundException => "Protected item not found. Verify the item name and vault.", RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => "Protected item not found. Verify the item name and vault.", + RequestFailedException reqEx when reqEx.ErrorCode == "BMSUserErrorContainerNameIncorrectFormat" => + $"Container name format is incorrect. Use the fully qualified container name from 'azurebackup protecteditem get' (e.g., 'IaasVMContainer;iaasvmcontainerv2;resourceGroup;vmName'). Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.ErrorCode == "BMSUserErrorProtectedItemNameIncorrectFormat" => + $"Protected item name format is incorrect. Use the fully qualified name from 'azurebackup protecteditem get' (e.g., 'VM;iaasvmcontainerv2;resourceGroup;vmName'). Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed. Ensure the caller has the Backup Reader or Backup Operator role on the vault. Details: {reqEx.Message}", RequestFailedException reqEx => reqEx.Message, _ => base.GetErrorMessage(ex) }; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs index 46f20280f3..eb3ba29441 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs @@ -116,6 +116,10 @@ public override async Task ExecuteAsync(CommandContext context, { RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => "Recovery point not found. Verify the recovery point ID and protected item.", + RequestFailedException reqEx when reqEx.ErrorCode == "BMSUserErrorContainerNameIncorrectFormat" => + $"Container name format is incorrect. Use the fully qualified container name from 'azurebackup protecteditem get' (e.g., 'IaasVMContainer;iaasvmcontainerv2;resourceGroup;vmName'). Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed. Ensure the caller has the Backup Reader or Backup Operator role on the vault. Details: {reqEx.Message}", RequestFailedException reqEx => reqEx.Message, _ => base.GetErrorMessage(ex) };