From 6e81204fae793ffaa1014e4e8019e20e57c549a0 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:27:16 -0800 Subject: [PATCH 01/12] Fix Service Bus authentication error propagation --- .../src/Commands/Queue/QueueDetailsCommand.cs | 6 ++++++ .../src/Commands/Queue/QueuePeekCommand.cs | 6 ++++++ .../src/Commands/Topic/SubscriptionDetailsCommand.cs | 8 ++++++-- .../src/Commands/Topic/SubscriptionPeekCommand.cs | 6 ++++++ .../src/Commands/Topic/TopicDetailsCommand.cs | 8 +++++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs index 05d6ced6e8..5b10eae91d 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs @@ -95,12 +95,18 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Queue not found. Please check the queue name and try again.", + Azure.Identity.AuthenticationFailedException authEx => + $"Authentication failed: {authEx.Message}", + UnauthorizedAccessException => + "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, + UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; internal record QueueDetailsCommandResult(QueueDetails QueueDetails); diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs index fce7ce5637..5e0f03acfc 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs @@ -100,12 +100,18 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Queue not found. Please check the queue name and try again.", + Azure.Identity.AuthenticationFailedException authEx => + $"Authentication failed: {authEx.Message}", + UnauthorizedAccessException => + "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, + UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index 6c9d7a7f02..ac2bf68586 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -97,13 +97,17 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => - $"Topic or subscription not found. Please check the topic and subscription names and try again.", - _ => base.GetErrorMessage(ex) + $"Topic or subscription not found. Please check the topic and subscription names and try again.", Azure.Identity.AuthenticationFailedException authEx => + $"Authentication failed: {authEx.Message}", + UnauthorizedAccessException => + "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, + UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs index b55f79587b..3106d0b9c3 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs @@ -105,12 +105,18 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Subscription not found. Please check the topic and subscription name and try again.", + Azure.Identity.AuthenticationFailedException authEx => + $"Authentication failed: {authEx.Message}", + UnauthorizedAccessException => + "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, + UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs index 1374b1baf5..5ed069593f 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs @@ -91,13 +91,19 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => - $"Subscription not found. Please check the topic and subscription name and try again.", + $"Topic not found. Please check the topic name and try again.", + Azure.Identity.AuthenticationFailedException authEx => + $"Authentication failed: {authEx.Message}", + UnauthorizedAccessException => + "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, + UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; From 09afb763f230253508c5823ec43cc2ca59592721 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:44:35 -0800 Subject: [PATCH 02/12] Add authentication error handling tests --- .../Queue/QueueDetailsCommandTests.cs | 52 +++++++++++++++ .../Topic/SubscriptionDetailsCommandTests.cs | 64 +++++++++++++++++++ .../Topic/TopicDetailsCommandTests.cs | 52 +++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs index cd7b3a6de4..e58ecb8587 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs @@ -114,6 +114,58 @@ public async Task ExecuteAsync_HandlesQueueNotFound() Assert.Contains("Queue not found", response.Message); } + [Fact] + public async Task ExecuteAsync_HandlesAuthenticationFailure() + { + // Arrange + var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); + + _serviceBusService.GetQueueDetails( + Arg.Is(NamespaceName), + Arg.Is(QueueName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(authException); + + var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--queue", QueueName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Unauthorized, response.Status); + Assert.Contains("Authentication failed", response.Message); + Assert.Contains("wrong issuer", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesUnauthorizedAccess() + { + // Arrange + var unauthorizedException = new UnauthorizedAccessException("Access denied"); + + _serviceBusService.GetQueueDetails( + Arg.Is(NamespaceName), + Arg.Is(QueueName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(unauthorizedException); + + var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--queue", QueueName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Forbidden, response.Status); + Assert.Contains("Access denied", response.Message); + Assert.Contains("credentials and permissions", response.Message); + } + [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs index a884520215..ce918cc66c 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs @@ -126,6 +126,70 @@ public async Task ExecuteAsync_HandlesSubscriptionNotFound() Assert.Contains("not found", response.Message); } + [Fact] + public async Task ExecuteAsync_HandlesAuthenticationFailure() + { + // Arrange + var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); + + _serviceBusService.GetSubscriptionDetails( + Arg.Is(NamespaceName), + Arg.Is(TopicName), + Arg.Is(SubscriptionName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(authException); + + var args = _commandDefinition.Parse([ + "--subscription", SubscriptionId, + "--namespace", NamespaceName, + "--topic", TopicName, + "--subscription-name", SubscriptionName + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Unauthorized, response.Status); + Assert.Contains("Authentication failed", response.Message); + Assert.Contains("wrong issuer", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesUnauthorizedAccess() + { + // Arrange + var unauthorizedException = new UnauthorizedAccessException("Access denied"); + + _serviceBusService.GetSubscriptionDetails( + Arg.Is(NamespaceName), + Arg.Is(TopicName), + Arg.Is(SubscriptionName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(unauthorizedException); + + var args = _commandDefinition.Parse([ + "--subscription", SubscriptionId, + "--namespace", NamespaceName, + "--topic", TopicName, + "--subscription-name", SubscriptionName + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Forbidden, response.Status); + Assert.Contains("Access denied", response.Message); + Assert.Contains("credentials and permissions", response.Message); + } + [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs index 3a096bbc39..bb420aebdb 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs @@ -113,6 +113,58 @@ public async Task ExecuteAsync_HandlesTopicNotFound() Assert.Equal(HttpStatusCode.NotFound, response.Status); } + [Fact] + public async Task ExecuteAsync_HandlesAuthenticationFailure() + { + // Arrange + var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); + + _serviceBusService.GetTopicDetails( + Arg.Is(NamespaceName), + Arg.Is(TopicName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(authException); + + var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--topic", TopicName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Unauthorized, response.Status); + Assert.Contains("Authentication failed", response.Message); + Assert.Contains("wrong issuer", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesUnauthorizedAccess() + { + // Arrange + var unauthorizedException = new UnauthorizedAccessException("Access denied"); + + _serviceBusService.GetTopicDetails( + Arg.Is(NamespaceName), + Arg.Is(TopicName), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ThrowsAsync(unauthorizedException); + + var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--topic", TopicName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Forbidden, response.Status); + Assert.Contains("Access denied", response.Message); + Assert.Contains("credentials and permissions", response.Message); + } + [Fact] public async Task ExecuteAsync_HandlesGenericException() { From facdefcc29f343c732234573e15d8ed8a76523cf Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:20:34 -0800 Subject: [PATCH 03/12] fix format issues --- .../src/Commands/Topic/SubscriptionDetailsCommand.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index ac2bf68586..d3cce2c553 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -96,16 +96,20 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { - ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => - $"Topic or subscription not found. Please check the topic and subscription names and try again.", Azure.Identity.AuthenticationFailedException authEx => + ServiceBusException exception when exception.Reason == + ServiceBusFailureReason.MessagingEntityNotFound => + $"Topic or subscription not found. Please check the topic and subscription names and try again.", + Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", _ => base.GetErrorMessage(ex) + "Access denied. Please check your credentials and permissions.", + _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { - ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + ServiceBusException sbEx when sbEx.Reason == + ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, UnauthorizedAccessException => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) From 9ba0cb867ca656ee00c37255f3ceb17bf182fd71 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:53:55 -0800 Subject: [PATCH 04/12] Replace UnauthorizedAccessException with Azure.RequestFailedException Azure SDKs do not throw UnauthorizedAccessException for authorization failures. They throw Azure.RequestFailedException with Status == 403. This aligns Service Bus error handling with the standard Azure SDK exception pattern used across all other toolsets and provides more specific error messages mentioning RBAC permissions. Changes: - Replace UnauthorizedAccessException with Azure.RequestFailedException (Status 403) - Update error messages to mention RBAC permissions - Update test method names from HandlesUnauthorizedAccess to HandlesAuthorizationFailure - Update test assertions to verify new error messages --- .../src/Commands/Queue/QueueDetailsCommand.cs | 6 +++--- .../src/Commands/Queue/QueuePeekCommand.cs | 6 +++--- .../src/Commands/Topic/SubscriptionDetailsCommand.cs | 6 +++--- .../src/Commands/Topic/SubscriptionPeekCommand.cs | 6 +++--- .../src/Commands/Topic/TopicDetailsCommand.cs | 6 +++--- .../Queue/QueueDetailsCommandTests.cs | 10 +++++----- .../Topic/SubscriptionDetailsCommandTests.cs | 10 +++++----- .../Topic/TopicDetailsCommandTests.cs | 10 +++++----- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs index 5b10eae91d..b100c7517c 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs @@ -97,8 +97,8 @@ public override async Task ExecuteAsync(CommandContext context, $"Queue not found. Please check the queue name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", + Azure.RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -106,7 +106,7 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - UnauthorizedAccessException => HttpStatusCode.Forbidden, + Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; internal record QueueDetailsCommandResult(QueueDetails QueueDetails); diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs index 5e0f03acfc..2feeacb5f7 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs @@ -102,8 +102,8 @@ public override async Task ExecuteAsync(CommandContext context, $"Queue not found. Please check the queue name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", + Azure.RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -111,7 +111,7 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - UnauthorizedAccessException => HttpStatusCode.Forbidden, + Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index d3cce2c553..f3d9fd21be 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -101,8 +101,8 @@ public override async Task ExecuteAsync(CommandContext context, $"Topic or subscription not found. Please check the topic and subscription names and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", + Azure.RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -111,7 +111,7 @@ public override async Task ExecuteAsync(CommandContext context, ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - UnauthorizedAccessException => HttpStatusCode.Forbidden, + Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs index 3106d0b9c3..2e3ca6752c 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs @@ -107,8 +107,8 @@ public override async Task ExecuteAsync(CommandContext context, $"Subscription not found. Please check the topic and subscription name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", + Azure.RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -116,7 +116,7 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - UnauthorizedAccessException => HttpStatusCode.Forbidden, + Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs index 5ed069593f..646190a409 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs @@ -94,8 +94,8 @@ public override async Task ExecuteAsync(CommandContext context, $"Topic not found. Please check the topic name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - UnauthorizedAccessException => - "Access denied. Please check your credentials and permissions.", + Azure.RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -103,7 +103,7 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - UnauthorizedAccessException => HttpStatusCode.Forbidden, + Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs index e58ecb8587..a753674d2a 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs @@ -141,10 +141,10 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() } [Fact] - public async Task ExecuteAsync_HandlesUnauthorizedAccess() + public async Task ExecuteAsync_HandlesAuthorizationFailure() { // Arrange - var unauthorizedException = new UnauthorizedAccessException("Access denied"); + var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); _serviceBusService.GetQueueDetails( Arg.Is(NamespaceName), @@ -152,7 +152,7 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() Arg.Any(), Arg.Any(), Arg.Any() - ).ThrowsAsync(unauthorizedException); + ).ThrowsAsync(forbiddenException); var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--queue", QueueName]); @@ -162,8 +162,8 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() // Assert Assert.NotNull(response); Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Access denied", response.Message); - Assert.Contains("credentials and permissions", response.Message); + Assert.Contains("Authorization failed", response.Message); + Assert.Contains("RBAC permissions", response.Message); } [Fact] diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs index ce918cc66c..9fb4544fd4 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs @@ -159,10 +159,10 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() } [Fact] - public async Task ExecuteAsync_HandlesUnauthorizedAccess() + public async Task ExecuteAsync_HandlesAuthorizationFailure() { // Arrange - var unauthorizedException = new UnauthorizedAccessException("Access denied"); + var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); _serviceBusService.GetSubscriptionDetails( Arg.Is(NamespaceName), @@ -171,7 +171,7 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() Arg.Any(), Arg.Any(), Arg.Any() - ).ThrowsAsync(unauthorizedException); + ).ThrowsAsync(forbiddenException); var args = _commandDefinition.Parse([ "--subscription", SubscriptionId, @@ -186,8 +186,8 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() // Assert Assert.NotNull(response); Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Access denied", response.Message); - Assert.Contains("credentials and permissions", response.Message); + Assert.Contains("Authorization failed", response.Message); + Assert.Contains("RBAC permissions", response.Message); } [Fact] diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs index bb420aebdb..41271a8463 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs @@ -140,10 +140,10 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() } [Fact] - public async Task ExecuteAsync_HandlesUnauthorizedAccess() + public async Task ExecuteAsync_HandlesAuthorizationFailure() { // Arrange - var unauthorizedException = new UnauthorizedAccessException("Access denied"); + var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); _serviceBusService.GetTopicDetails( Arg.Is(NamespaceName), @@ -151,7 +151,7 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() Arg.Any(), Arg.Any(), Arg.Any() - ).ThrowsAsync(unauthorizedException); + ).ThrowsAsync(forbiddenException); var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--topic", TopicName]); @@ -161,8 +161,8 @@ public async Task ExecuteAsync_HandlesUnauthorizedAccess() // Assert Assert.NotNull(response); Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Access denied", response.Message); - Assert.Contains("credentials and permissions", response.Message); + Assert.Contains("Authorization failed", response.Message); + Assert.Contains("RBAC permissions", response.Message); } [Fact] From f682b56d8310b32bf2d7c75cb4f4eeae481fd0bc Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:11:18 -0800 Subject: [PATCH 05/12] Focus on Identity-specific exceptions only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Azure.RequestFailedException handling since it's not thrown by Azure.Identity. Azure.Identity only throws AuthenticationFailedException for token acquisition failures. Service-level authorization errors (403 Forbidden) would come from ServiceBusException, not RequestFailedException. This simplifies error handling to focus only on Identity-specific exceptions: - Azure.Identity.AuthenticationFailedException → 401 Unauthorized - ServiceBusException (entity not found) → 404 NotFound - All other exceptions → handled by base class Changes: - Remove Azure.RequestFailedException handling from all commands - Remove HandlesAuthorizationFailure tests (no longer applicable) - Keep only AuthenticationFailedException tests (3 tests, all passing) --- .../src/Commands/Queue/QueueDetailsCommand.cs | 3 -- .../src/Commands/Queue/QueuePeekCommand.cs | 3 -- .../Topic/SubscriptionDetailsCommand.cs | 3 -- .../Commands/Topic/SubscriptionPeekCommand.cs | 3 -- .../src/Commands/Topic/TopicDetailsCommand.cs | 3 -- .../Queue/QueueDetailsCommandTests.cs | 26 --------------- .../Topic/SubscriptionDetailsCommandTests.cs | 32 ------------------- .../Topic/TopicDetailsCommandTests.cs | 26 --------------- 8 files changed, 99 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs index b100c7517c..73e5589f3a 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs @@ -97,8 +97,6 @@ public override async Task ExecuteAsync(CommandContext context, $"Queue not found. Please check the queue name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - Azure.RequestFailedException reqEx when reqEx.Status == 403 => - $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -106,7 +104,6 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; internal record QueueDetailsCommandResult(QueueDetails QueueDetails); diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs index 2feeacb5f7..7feeabf9af 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs @@ -102,8 +102,6 @@ public override async Task ExecuteAsync(CommandContext context, $"Queue not found. Please check the queue name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - Azure.RequestFailedException reqEx when reqEx.Status == 403 => - $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -111,7 +109,6 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index f3d9fd21be..efe00f5206 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -101,8 +101,6 @@ public override async Task ExecuteAsync(CommandContext context, $"Topic or subscription not found. Please check the topic and subscription names and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - Azure.RequestFailedException reqEx when reqEx.Status == 403 => - $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -111,7 +109,6 @@ public override async Task ExecuteAsync(CommandContext context, ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs index 2e3ca6752c..6132db134b 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs @@ -107,8 +107,6 @@ public override async Task ExecuteAsync(CommandContext context, $"Subscription not found. Please check the topic and subscription name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - Azure.RequestFailedException reqEx when reqEx.Status == 403 => - $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -116,7 +114,6 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs index 646190a409..18c201e4a0 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs @@ -94,8 +94,6 @@ public override async Task ExecuteAsync(CommandContext context, $"Topic not found. Please check the topic name and try again.", Azure.Identity.AuthenticationFailedException authEx => $"Authentication failed: {authEx.Message}", - Azure.RequestFailedException reqEx when reqEx.Status == 403 => - $"Authorization failed. Ensure you have appropriate RBAC permissions on the Service Bus namespace. Details: {reqEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -103,7 +101,6 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, - Azure.RequestFailedException reqEx when reqEx.Status == 403 => HttpStatusCode.Forbidden, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs index a753674d2a..c023b99f29 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs @@ -140,32 +140,6 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.Contains("wrong issuer", response.Message); } - [Fact] - public async Task ExecuteAsync_HandlesAuthorizationFailure() - { - // Arrange - var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); - - _serviceBusService.GetQueueDetails( - Arg.Is(NamespaceName), - Arg.Is(QueueName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(forbiddenException); - - var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--queue", QueueName]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Authorization failed", response.Message); - Assert.Contains("RBAC permissions", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs index 9fb4544fd4..6eef6ba8e3 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs @@ -158,38 +158,6 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.Contains("wrong issuer", response.Message); } - [Fact] - public async Task ExecuteAsync_HandlesAuthorizationFailure() - { - // Arrange - var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); - - _serviceBusService.GetSubscriptionDetails( - Arg.Is(NamespaceName), - Arg.Is(TopicName), - Arg.Is(SubscriptionName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(forbiddenException); - - var args = _commandDefinition.Parse([ - "--subscription", SubscriptionId, - "--namespace", NamespaceName, - "--topic", TopicName, - "--subscription-name", SubscriptionName - ]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Authorization failed", response.Message); - Assert.Contains("RBAC permissions", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs index 41271a8463..c27d7eb193 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs @@ -139,32 +139,6 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.Contains("wrong issuer", response.Message); } - [Fact] - public async Task ExecuteAsync_HandlesAuthorizationFailure() - { - // Arrange - var forbiddenException = new Azure.RequestFailedException(403, "Access denied"); - - _serviceBusService.GetTopicDetails( - Arg.Is(NamespaceName), - Arg.Is(TopicName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(forbiddenException); - - var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--topic", TopicName]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Forbidden, response.Status); - Assert.Contains("Authorization failed", response.Message); - Assert.Contains("RBAC permissions", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { From 4dbe44127a98c90a01bf5beaf5eae5ab6629bc10 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:26:17 -0800 Subject: [PATCH 06/12] Add CredentialUnavailableException handling to GlobalCommand Azure.Identity SDK throws two exception types: 1. AuthenticationFailedException - credentials exist but authentication fails 2. CredentialUnavailableException - no credentials available Previously, only AuthenticationFailedException was handled. This adds CredentialUnavailableException handling to provide clearer error messages when no credential sources are available (e.g., Azure CLI not logged in, VS Code not signed in). Benefits: - Better error messages distinguishing 'no credentials' from 'authentication failed' - More actionable guidance for users (lists all authentication methods) - Consistent with internal credential chaining in CustomChainedCredential.cs Status code: Both map to 401 Unauthorized Applies to: All commands inheriting from GlobalCommand (automatically benefits all Azure toolsets) --- core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs b/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs index c5a20680b5..f89f20d884 100644 --- a/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs +++ b/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs @@ -108,6 +108,15 @@ protected override TOptions BindOptions(ParseResult parseResult) protected override string GetErrorMessage(Exception ex) => ex switch { + CredentialUnavailableException credEx => + $"No Azure credentials are available. Please authenticate using one of the supported methods:\n" + + $" - Azure CLI: Run 'az login'\n" + + $" - Azure PowerShell: Run 'Connect-AzAccount'\n" + + $" - Azure Developer CLI: Run 'azd auth login'\n" + + $" - Visual Studio: Sign in through Azure Service Authentication\n" + + $" - VS Code: Sign in through Azure Account extension\n" + + $"For more information, see: https://aka.ms/azmcp/auth\n" + + $"Details: {credEx.Message}", AuthenticationFailedException authEx => $"Authentication failed. Please run 'az login' to sign in to Azure. Details: {authEx.Message}", RequestFailedException rfEx => HandleRequestFailedException(rfEx), @@ -119,6 +128,7 @@ protected override TOptions BindOptions(ParseResult parseResult) protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { KeyNotFoundException => HttpStatusCode.NotFound, + CredentialUnavailableException => HttpStatusCode.Unauthorized, AuthenticationFailedException => HttpStatusCode.Unauthorized, RequestFailedException rfEx => (HttpStatusCode)rfEx.Status, HttpRequestException => HttpStatusCode.ServiceUnavailable, From 915b5d03919485dfc0865ec98743b80f6d8102cf Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:44:54 -0800 Subject: [PATCH 07/12] Remove redundant Identity exception handling from Service Bus commands Service Bus commands were duplicating AuthenticationFailedException and CredentialUnavailableException handling that already exists in GlobalCommand base class. Since Service Bus commands inherit from SubscriptionCommand -> GlobalCommand, they automatically get Identity exception handling through the fallback case: _ => base.GetErrorMessage(ex). Changes: - Remove AuthenticationFailedException handling from all 5 Service Bus commands - Let exceptions fall through to GlobalCommand for consistent error messages - Update tests to verify GlobalCommand message format ('az login' guidance) - Service Bus commands now only handle ServiceBusException (entity not found) Benefits: - Eliminates code duplication - Consistent error messages across all Azure toolsets - Automatic CredentialUnavailableException support - Single source of truth for Identity errors in GlobalCommand All tests passing (3/3) --- .../src/Commands/Queue/QueueDetailsCommand.cs | 3 --- .../src/Commands/Queue/QueuePeekCommand.cs | 3 --- .../src/Commands/Topic/SubscriptionDetailsCommand.cs | 3 --- .../src/Commands/Topic/SubscriptionPeekCommand.cs | 3 --- .../src/Commands/Topic/TopicDetailsCommand.cs | 3 --- .../Queue/QueueDetailsCommandTests.cs | 1 + .../Topic/SubscriptionDetailsCommandTests.cs | 1 + .../Topic/TopicDetailsCommandTests.cs | 1 + 8 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs index 73e5589f3a..05d6ced6e8 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueueDetailsCommand.cs @@ -95,15 +95,12 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Queue not found. Please check the queue name and try again.", - Azure.Identity.AuthenticationFailedException authEx => - $"Authentication failed: {authEx.Message}", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, - Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, _ => base.GetStatusCode(ex) }; internal record QueueDetailsCommandResult(QueueDetails QueueDetails); diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs index 7feeabf9af..fce7ce5637 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Queue/QueuePeekCommand.cs @@ -100,15 +100,12 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Queue not found. Please check the queue name and try again.", - Azure.Identity.AuthenticationFailedException authEx => - $"Authentication failed: {authEx.Message}", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, - Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index efe00f5206..bb74d379ab 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -99,8 +99,6 @@ public override async Task ExecuteAsync(CommandContext context, ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Topic or subscription not found. Please check the topic and subscription names and try again.", - Azure.Identity.AuthenticationFailedException authEx => - $"Authentication failed: {authEx.Message}", _ => base.GetErrorMessage(ex) }; @@ -108,7 +106,6 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, - Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs index 6132db134b..b55f79587b 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionPeekCommand.cs @@ -105,15 +105,12 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Subscription not found. Please check the topic and subscription name and try again.", - Azure.Identity.AuthenticationFailedException authEx => - $"Authentication failed: {authEx.Message}", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, - Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs index 18c201e4a0..068991b6b8 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs @@ -92,15 +92,12 @@ public override async Task ExecuteAsync(CommandContext context, { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Topic not found. Please check the topic name and try again.", - Azure.Identity.AuthenticationFailedException authEx => - $"Authentication failed: {authEx.Message}", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, - Azure.Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs index c023b99f29..8bb356911a 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs @@ -137,6 +137,7 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.NotNull(response); Assert.Equal(HttpStatusCode.Unauthorized, response.Status); Assert.Contains("Authentication failed", response.Message); + Assert.Contains("az login", response.Message); // Verify GlobalCommand message format Assert.Contains("wrong issuer", response.Message); } diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs index 6eef6ba8e3..28f9468175 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs @@ -155,6 +155,7 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.NotNull(response); Assert.Equal(HttpStatusCode.Unauthorized, response.Status); Assert.Contains("Authentication failed", response.Message); + Assert.Contains("az login", response.Message); // Verify GlobalCommand message format Assert.Contains("wrong issuer", response.Message); } diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs index c27d7eb193..ce5385d66c 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs @@ -136,6 +136,7 @@ public async Task ExecuteAsync_HandlesAuthenticationFailure() Assert.NotNull(response); Assert.Equal(HttpStatusCode.Unauthorized, response.Status); Assert.Contains("Authentication failed", response.Message); + Assert.Contains("az login", response.Message); // Verify GlobalCommand message format Assert.Contains("wrong issuer", response.Message); } From 244e1f134b6bcf614050baea4d00646cbf67afad Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:47:43 -0800 Subject: [PATCH 08/12] Preserve specific error details in parameter validation messages Previously, when commands reported 'Missing Required options: --subscription', the ServerToolLoader and NamespaceToolLoader would replace the specific error with a generic 'The command is missing required parameters' message, making it unclear which parameter was actually missing. This was particularly problematic for subscription parameter errors because: - Subscription is often auto-detected from AZURE_SUBSCRIPTION_ID or Azure CLI - When auto-detection fails (e.g., resource in different tenant), validation fails - Users see 'missing required parameters' but don't know it's --subscription - The MCP schema shows subscription as optional, adding to confusion Changes: - ServerToolLoader: Use actual error text instead of generic message - NamespaceToolLoader: Extract commandResponse.Message for specific details - Both still provide helpful guidance and command spec - Original detailed error now visible to users Example improvement: Before: 'The servicebus_topic_details command is missing required parameters.' After: 'Missing Required options: --subscription' + helpful guidance Fixes the root cause reported in the original bug where users couldn't tell that subscription parameter was needed when resources were in different tenants. --- .../Server/Commands/ToolLoading/NamespaceToolLoader.cs | 8 +++++++- .../Areas/Server/Commands/ToolLoading/ServerToolLoader.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs index 75ca5290f8..c8f74d8603 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs @@ -380,13 +380,19 @@ private async Task InvokeChildToolAsync( var childToolSpecJson = GetChildToolJson(request, namespaceName, command); _logger.LogWarning("Namespace {Namespace} command {Command} requires additional parameters.", namespaceName, command); + + // Extract the specific error message from the response + var errorMessage = string.IsNullOrEmpty(commandResponse.Message) + ? $"The '{command}' command is missing required parameters." + : commandResponse.Message; + var finalResponse = new CallToolResult { Content = [ new TextContentBlock { Text = $""" - The '{command}' command is missing required parameters. + {errorMessage} - Review the following command spec and identify the required arguments from the input schema. - Omit any arguments that are not required or do not apply to your use case. diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/ServerToolLoader.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/ServerToolLoader.cs index 01d8b01c2c..1c8a90aa56 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/ServerToolLoader.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/ServerToolLoader.cs @@ -283,7 +283,7 @@ private async Task InvokeChildToolAsync(RequestContext Date: Wed, 18 Feb 2026 20:46:57 -0800 Subject: [PATCH 09/12] refactor --- core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs | 10 ---------- .../src/Commands/Topic/SubscriptionDetailsCommand.cs | 6 ++---- .../src/Commands/Topic/TopicDetailsCommand.cs | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs b/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs index f89f20d884..c5a20680b5 100644 --- a/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs +++ b/core/Microsoft.Mcp.Core/src/Commands/GlobalCommand.cs @@ -108,15 +108,6 @@ protected override TOptions BindOptions(ParseResult parseResult) protected override string GetErrorMessage(Exception ex) => ex switch { - CredentialUnavailableException credEx => - $"No Azure credentials are available. Please authenticate using one of the supported methods:\n" + - $" - Azure CLI: Run 'az login'\n" + - $" - Azure PowerShell: Run 'Connect-AzAccount'\n" + - $" - Azure Developer CLI: Run 'azd auth login'\n" + - $" - Visual Studio: Sign in through Azure Service Authentication\n" + - $" - VS Code: Sign in through Azure Account extension\n" + - $"For more information, see: https://aka.ms/azmcp/auth\n" + - $"Details: {credEx.Message}", AuthenticationFailedException authEx => $"Authentication failed. Please run 'az login' to sign in to Azure. Details: {authEx.Message}", RequestFailedException rfEx => HandleRequestFailedException(rfEx), @@ -128,7 +119,6 @@ protected override TOptions BindOptions(ParseResult parseResult) protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { KeyNotFoundException => HttpStatusCode.NotFound, - CredentialUnavailableException => HttpStatusCode.Unauthorized, AuthenticationFailedException => HttpStatusCode.Unauthorized, RequestFailedException rfEx => (HttpStatusCode)rfEx.Status, HttpRequestException => HttpStatusCode.ServiceUnavailable, diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs index bb74d379ab..6c9d7a7f02 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/SubscriptionDetailsCommand.cs @@ -96,16 +96,14 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { - ServiceBusException exception when exception.Reason == - ServiceBusFailureReason.MessagingEntityNotFound => + ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => $"Topic or subscription not found. Please check the topic and subscription names and try again.", _ => base.GetErrorMessage(ex) }; protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch { - ServiceBusException sbEx when sbEx.Reason == - ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, + ServiceBusException sbEx when sbEx.Reason == ServiceBusFailureReason.MessagingEntityNotFound => HttpStatusCode.NotFound, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs index 068991b6b8..1374b1baf5 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/src/Commands/Topic/TopicDetailsCommand.cs @@ -91,7 +91,7 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { ServiceBusException exception when exception.Reason == ServiceBusFailureReason.MessagingEntityNotFound => - $"Topic not found. Please check the topic name and try again.", + $"Subscription not found. Please check the topic and subscription name and try again.", _ => base.GetErrorMessage(ex) }; From 6e25b8d5bccb22a04e573f74f96e3498f969aeb5 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:42:02 -0800 Subject: [PATCH 10/12] Add unit tests for error message propagation in ToolLoaders Added tests to verify that specific error messages (like 'Missing Required options: -subscription') are preserved in NamespaceToolLoader and ServerToolLoader instead of being replaced with generic 'missing required parameters' messages. These tests validate the fix in commit 244e1f13 which ensures users see exact error details when commands have validation failures. --- .../ToolLoading/NamespaceToolLoaderTests.cs | 62 +++++++++++++++++++ .../ToolLoading/ServerToolLoaderTests.cs | 43 +++++++++++++ 2 files changed, 105 insertions(+) diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs index 36d7e14bef..809e2e4289 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs @@ -419,6 +419,39 @@ public async Task CallToolHandler_ThreadSafeLazyLoading() }); } + [Fact] + public async Task CallToolHandler_PreservesSpecificErrorMessageForMissingParameters() + { + // Arrange + var loader = new NamespaceToolLoader(_commandFactory, _options, _serviceProvider, _logger); + // Use subscription list command which requires --subscription or AZURE_SUBSCRIPTION_ID + var toolName = "subscription"; + + // Create request without required subscription parameter + var request = CreateCallToolRequest(toolName, new Dictionary + { + ["command"] = "list", + ["parameters"] = new Dictionary() + }); + + // Act + var result = await loader.CallToolHandler(request, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(result); + Assert.True(result.IsError); + + var textContent = result.Content[0] as TextContentBlock; + Assert.NotNull(textContent); + + // Verify the specific error message is preserved (not replaced with generic message) + Assert.Contains("Missing Required options:", textContent.Text); + + // Verify the command spec guidance is still included + Assert.Contains("Review the following command spec", textContent.Text); + Assert.Contains("Command Spec:", textContent.Text); + } + [Fact] public async Task DisposeAsync_ClearsCaches() { @@ -556,6 +589,35 @@ await Assert.ThrowsAsync(async () => await options.Handlers.ElicitationHandler.Invoke(null!, TestContext.Current.CancellationToken)); } + [Fact] + public async Task CallToolHandler_WithMissingRequiredOptions_PreservesSpecificErrorMessage() + { + // Arrange + // Service Bus commands require --subscription parameter + // When calling without it, we should get a specific error message, not a generic one + const string command = "servicebus_namespace_list"; + var args = new Dictionary(); // Missing required --subscription parameter + + var request = CreateCallToolRequest(command, args); + + // Act + var response = await _loader.CallToolHandler(request, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsError); + Assert.Single(response.Content); + + var textContent = response.Content[0] as TextContent; + Assert.NotNull(textContent); + + // Verify the specific error message is preserved (not the generic "missing required parameters") + Assert.Contains("Missing Required options: --subscription", textContent.Text); + + // Verify it still includes the command spec guidance + Assert.Contains("Review the following command spec", textContent.Text); + } + // Helper methods private string GetFirstAvailableNamespace() diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs index 4aa1ac52ff..05e43af515 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs @@ -136,4 +136,47 @@ public async Task ListToolsHandler_WithRealRegistryDiscovery_ReturnsExpectedStru Assert.True(tool.InputSchema.ValueKind != JsonValueKind.Undefined, "InputSchema should be defined"); } } + + [Fact] + public async Task CallToolHandler_PreservesSpecificErrorMessageForMissingParameters() + { + // Arrange - use real RegistryDiscoveryStrategy + var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider(); + var loggerFactory = serviceProvider.GetRequiredService(); + var serviceStartOptions = Microsoft.Extensions.Options.Options.Create(new ServiceStartOptions()); + var toolLoaderOptions = Microsoft.Extensions.Options.Options.Create(new ToolLoaderOptions()); + var discoveryLogger = loggerFactory.CreateLogger(); + var discoveryStrategy = RegistryDiscoveryStrategyHelper.CreateStrategy(serviceStartOptions.Value, discoveryLogger); + var logger = loggerFactory.CreateLogger(); + + var toolLoader = new ServerToolLoader(discoveryStrategy, toolLoaderOptions, logger); + + // Create request for documentation tool with missing required parameters + var request = CreateCallToolRequest("documentation", + new Dictionary + { + { "command", JsonDocument.Parse("\"microsoft_docs_search\"").RootElement }, + // Missing 'question' parameter which is required + { "parameters", JsonDocument.Parse("{}").RootElement } + }); + + // Act + var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Content); + Assert.NotEmpty(result.Content); + + var textContent = result.Content[0] as TextContentBlock; + Assert.NotNull(textContent); + + // Verify the specific error message is preserved (not replaced with generic message) + // Should contain "Missing required options:" or similar specific error, not generic "missing required parameters" + Assert.Contains("Missing", textContent.Text); + + // Verify the command spec guidance is still included + Assert.Contains("Review the following command spec", textContent.Text); + Assert.Contains("Command Spec:", textContent.Text); + } } From 3eb7b24804d7c85e1da886cb335779c8d5d405e4 Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:51:25 -0800 Subject: [PATCH 11/12] Remove Service Bus authentication tests These authentication tests were added to verify GlobalCommand integration but are out of scope for this PR which focuses specifically on error message propagation in ToolLoaders. --- .../Queue/QueueDetailsCommandTests.cs | 27 --------------- .../Topic/SubscriptionDetailsCommandTests.cs | 33 ------------------- .../Topic/TopicDetailsCommandTests.cs | 27 --------------- 3 files changed, 87 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs index 8bb356911a..cd7b3a6de4 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Queue/QueueDetailsCommandTests.cs @@ -114,33 +114,6 @@ public async Task ExecuteAsync_HandlesQueueNotFound() Assert.Contains("Queue not found", response.Message); } - [Fact] - public async Task ExecuteAsync_HandlesAuthenticationFailure() - { - // Arrange - var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); - - _serviceBusService.GetQueueDetails( - Arg.Is(NamespaceName), - Arg.Is(QueueName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(authException); - - var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--queue", QueueName]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Unauthorized, response.Status); - Assert.Contains("Authentication failed", response.Message); - Assert.Contains("az login", response.Message); // Verify GlobalCommand message format - Assert.Contains("wrong issuer", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs index 28f9468175..a884520215 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/SubscriptionDetailsCommandTests.cs @@ -126,39 +126,6 @@ public async Task ExecuteAsync_HandlesSubscriptionNotFound() Assert.Contains("not found", response.Message); } - [Fact] - public async Task ExecuteAsync_HandlesAuthenticationFailure() - { - // Arrange - var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); - - _serviceBusService.GetSubscriptionDetails( - Arg.Is(NamespaceName), - Arg.Is(TopicName), - Arg.Is(SubscriptionName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(authException); - - var args = _commandDefinition.Parse([ - "--subscription", SubscriptionId, - "--namespace", NamespaceName, - "--topic", TopicName, - "--subscription-name", SubscriptionName - ]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Unauthorized, response.Status); - Assert.Contains("Authentication failed", response.Message); - Assert.Contains("az login", response.Message); // Verify GlobalCommand message format - Assert.Contains("wrong issuer", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { diff --git a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs index ce5385d66c..3a096bbc39 100644 --- a/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ServiceBus/tests/Azure.Mcp.Tools.ServiceBus.UnitTests/Topic/TopicDetailsCommandTests.cs @@ -113,33 +113,6 @@ public async Task ExecuteAsync_HandlesTopicNotFound() Assert.Equal(HttpStatusCode.NotFound, response.Status); } - [Fact] - public async Task ExecuteAsync_HandlesAuthenticationFailure() - { - // Arrange - var authException = new Azure.Identity.AuthenticationFailedException("The access token is from the wrong issuer"); - - _serviceBusService.GetTopicDetails( - Arg.Is(NamespaceName), - Arg.Is(TopicName), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).ThrowsAsync(authException); - - var args = _commandDefinition.Parse(["--subscription", SubscriptionId, "--namespace", NamespaceName, "--topic", TopicName]); - - // Act - var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Unauthorized, response.Status); - Assert.Contains("Authentication failed", response.Message); - Assert.Contains("az login", response.Message); // Verify GlobalCommand message format - Assert.Contains("wrong issuer", response.Message); - } - [Fact] public async Task ExecuteAsync_HandlesGenericException() { From b570eb622c13690abf7aa4ce93d2e373f69cb89a Mon Sep 17 00:00:00 2001 From: g2vinay <5430778+g2vinay@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:21:58 -0800 Subject: [PATCH 12/12] Fix whitespace formatting in NamespaceToolLoader files Normalize blank lines to fix formatting validation errors reported by dotnet format. --- .../Commands/ToolLoading/NamespaceToolLoaderTests.cs | 10 +++++----- .../Server/Commands/ToolLoading/NamespaceToolLoader.cs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs index 809e2e4289..9fd5d29c1a 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs @@ -440,13 +440,13 @@ public async Task CallToolHandler_PreservesSpecificErrorMessageForMissingParamet // Assert Assert.NotNull(result); Assert.True(result.IsError); - + var textContent = result.Content[0] as TextContentBlock; Assert.NotNull(textContent); - + // Verify the specific error message is preserved (not replaced with generic message) Assert.Contains("Missing Required options:", textContent.Text); - + // Verify the command spec guidance is still included Assert.Contains("Review the following command spec", textContent.Text); Assert.Contains("Command Spec:", textContent.Text); @@ -610,10 +610,10 @@ public async Task CallToolHandler_WithMissingRequiredOptions_PreservesSpecificEr var textContent = response.Content[0] as TextContent; Assert.NotNull(textContent); - + // Verify the specific error message is preserved (not the generic "missing required parameters") Assert.Contains("Missing Required options: --subscription", textContent.Text); - + // Verify it still includes the command spec guidance Assert.Contains("Review the following command spec", textContent.Text); } diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs index c8f74d8603..130cc2b990 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs @@ -380,12 +380,12 @@ private async Task InvokeChildToolAsync( var childToolSpecJson = GetChildToolJson(request, namespaceName, command); _logger.LogWarning("Namespace {Namespace} command {Command} requires additional parameters.", namespaceName, command); - + // Extract the specific error message from the response - var errorMessage = string.IsNullOrEmpty(commandResponse.Message) + var errorMessage = string.IsNullOrEmpty(commandResponse.Message) ? $"The '{command}' command is missing required parameters." : commandResponse.Message; - + var finalResponse = new CallToolResult { Content =