Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The Azure MCP Server updates automatically by default whenever a new release com

### Bugs Fixed

- Fixed various exceptions being thrown by the AppLens tool. [[#1419](https://github.com/microsoft/mcp/pull/1419)]

### Other Changes

## 2.0.0-beta.10 (2026-01-09)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
options.Tenant,
cancellationToken);

context.Response.Results = ResponseResult.Create(new(result), AppLensJsonContext.Default.ResourceDiagnoseCommandResult);
context.Response.Results = result switch
{
Success<AppLensInsights> success => ResponseResult.Create(new(success.Data, Message: null), AppLensJsonContext.Default.ResourceDiagnoseCommandResult),
Failure<AppLensInsights> failure => ResponseResult.Create(new ResourceDiagnoseCommandResult(null, failure.Message), AppLensJsonContext.Default.ResourceDiagnoseCommandResult),
_ => throw new InvalidOperationException($"Unexpected result type from ${nameof(service.DiagnoseResourceAsync)}.")
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string interpolation syntax is incorrect. It uses $ followed by ${nameof(...)}, which will not perform the interpolation correctly. The correct syntax should use {nameof(...)} inside a string prefixed with $. This will result in an error message that literally contains "${nameof(service.DiagnoseResourceAsync)}" instead of the actual method name "DiagnoseResourceAsync".

Suggested change
_ => throw new InvalidOperationException($"Unexpected result type from ${nameof(service.DiagnoseResourceAsync)}.")
_ => throw new InvalidOperationException($"Unexpected result type from {nameof(service.DiagnoseResourceAsync)}.")

Copilot uses AI. Check for mistakes.
};
Comment on lines +90 to +95
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new failure path introduced in the Result pattern (when DiagnoseResourceAsync returns a Failure result) lacks test coverage. While the tests check for exceptions being thrown, there are no tests that verify the behavior when the service returns a Failure result containing an error message, such as when a resource is not found or when the AppLens session creation fails. Consider adding tests that mock the service to return Failure results and verify that the command properly handles these cases and returns appropriate responses to the user.

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Azure.Mcp.Tools.AppLens.Models;
[JsonSerializable(typeof(ChatMessage))]
[JsonSerializable(typeof(ChatMessageRequestBody))]
[JsonSerializable(typeof(ChatMessageResponseBody))]
[JsonSerializable(typeof(DiagnosticResult))]
[JsonSerializable(typeof(AppLensInsights))]
[JsonSerializable(typeof(ResourceDiagnoseCommandResult))]
[JsonSerializable(typeof(JsonElement[]))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
Expand Down
63 changes: 32 additions & 31 deletions tools/Azure.Mcp.Tools.AppLens/src/Models/AppLensModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,6 @@ namespace Azure.Mcp.Tools.AppLens.Models;
/// <param name="ExpiresIn">The length of time the Token will remain valid.</param>
public record AppLensSession(string SessionId, string ResourceId, string Token, int ExpiresIn);

/// <summary>
/// Represents the result of trying to obtain an AppLens session.
/// </summary>
public abstract record GetAppLensSessionResult;

/// <summary>
/// Represents the successful creation of an AppLens session.
/// </summary>
/// <param name="Session">The new AppLens session.</param>
public sealed record SuccessfulAppLensSessionResult(AppLensSession Session) : GetAppLensSessionResult;

/// <summary>
/// Represents a failure while trying to create an AppLens session.
/// </summary>
/// <param name="Message">A message about the failure suitable for display to the user.</param>
public sealed record FailedAppLensSessionResult(string Message) : GetAppLensSessionResult;

/// <summary>
/// Result of Azure Resource Graph query for resource lookup.
/// </summary>
Expand All @@ -46,24 +29,14 @@ public record AppLensArgQueryResult(
string ResourceKind,
string ResourceName);

/// <summary>
/// Abstract base for resource finding results.
/// </summary>
public abstract record FindResourceIdResult;

/// <summary>
/// Successful resource finding result.
/// Contains the results of successfully finding a resource based on its name.
/// </summary>
/// <param name="ResourceId">The resource ID.</param>
/// <param name="ResourceTypeAndKind">The resource type and kind.</param>
/// <param name="Message">Optional message.</param>
public sealed record FoundResourceResult(string ResourceId, string ResourceTypeAndKind, string? Message) : FindResourceIdResult;

/// <summary>
/// Failed resource finding result.
/// </summary>
/// <param name="Message">Error message.</param>
public sealed record DidNotFindResourceResult(string Message) : FindResourceIdResult;
public sealed record FoundResourceData(string ResourceId, string ResourceTypeAndKind, string? Message);

/// <summary>
/// Options controlling the behavior of the AppLens service.
Expand Down Expand Up @@ -205,7 +178,7 @@ public static ChatMessageResponseBody FromJson(string json)
/// <param name="Solutions">List of proposed solutions.</param>
/// <param name="ResourceId">The resource ID that was diagnosed.</param>
/// <param name="ResourceType">The type of resource that was diagnosed.</param>
public record DiagnosticResult(
public record AppLensInsights(
List<string> Insights,
List<string> Solutions,
string ResourceId,
Expand All @@ -215,4 +188,32 @@ public record DiagnosticResult(
/// Command result for resource diagnose operation.
/// </summary>
/// <param name="Result">The diagnostic result.</param>
public record ResourceDiagnoseCommandResult(DiagnosticResult Result);
public record ResourceDiagnoseCommandResult(AppLensInsights? Result, string? Message);

/// <summary>
/// Represents the result of an operation, which can be either a success containing a value or a failure containing an
/// error message.
/// </summary>
/// <remarks>Use the static methods <see cref="Success{T}(T)"/> and <see cref="Failure{T}(string)"/> to create
/// instances representing successful or failed outcomes. This type is commonly used to encapsulate the outcome of
/// operations that may fail, providing a way to handle errors without exceptions.</remarks>
/// <typeparam name="T">The type of the value returned in the case of a successful result.</typeparam>
public abstract record Result<T>()
{
public static Success<T> Success(T data) => new(data);
public static Failure<T> Failure(string message) => new(message);
}

/// <summary>
/// Represents a successful result that contains a value of the specified type.
/// </summary>
/// <typeparam name="T">The type of the value returned in the successful result.</typeparam>
/// <param name="Data">The value associated with the successful result.</param>
public sealed record Success<T>(T Data) : Result<T>;

/// <summary>
/// Represents a failed result with a message explaining the problem.
/// </summary>
/// <typeparam name="T">The type of the value returned in a successful result.</typeparam>
/// <param name="Message">A message containing more details on the failure.</param>
public sealed record Failure<T>(string Message) : Result<T>;
45 changes: 23 additions & 22 deletions tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class AppLensService(IHttpClientService httpClientService, ISubscriptionS
private const string ConversationalDiagnosticsSignalREndpoint = "https://diagnosticschat.azure.com/chatHub";

/// <inheritdoc />
public async Task<DiagnosticResult> DiagnoseResourceAsync(
public async Task<Result<AppLensInsights>> DiagnoseResourceAsync(
string question,
string resource,
string subscription,
Expand All @@ -39,34 +39,35 @@ public async Task<DiagnosticResult> DiagnoseResourceAsync(
// Step 1: Get the resource ID
var findResult = await FindResourceIdAsync(resource, subscriptionResource.Data.SubscriptionId, resourceGroup, resourceType, cancellationToken);

if (findResult is DidNotFindResourceResult notFound)
if (findResult is Failure<FoundResourceData> notFoundResult)
{
throw new InvalidOperationException(notFound.Message);
return Result<AppLensInsights>.Failure(notFoundResult.Message);
}

var foundResource = (FoundResourceResult)findResult;
var successfulResult = (Success<FoundResourceData>)findResult;
var resourceData = successfulResult.Data;

// Step 2: Get AppLens session
var session = await GetAppLensSessionAsync(foundResource.ResourceId, tenantId, cancellationToken);
var getSessionResult = await GetAppLensSessionAsync(resourceData.ResourceId, tenantId, cancellationToken);

if (session is FailedAppLensSessionResult failed)
if (getSessionResult is Failure<AppLensSession> failed)
{
throw new InvalidOperationException(failed.Message);
return Result<AppLensInsights>.Failure(failed.Message);
}

var successfulSession = (SuccessfulAppLensSessionResult)session;
var successResult = (Success<AppLensSession>)getSessionResult;

// Step 3: Ask AppLens the diagnostic question
var insights = await CollectInsightsAsync(successfulSession.Session, question, cancellationToken);
var insights = await CollectInsightsAsync(successResult.Data, question, cancellationToken);

return new DiagnosticResult(
return Result<AppLensInsights>.Success(new AppLensInsights(
insights.Insights,
insights.Solutions,
foundResource.ResourceId,
foundResource.ResourceTypeAndKind);
resourceData.ResourceId,
resourceData.ResourceTypeAndKind));
}

private Task<FindResourceIdResult> FindResourceIdAsync(
private Task<Result<FoundResourceData>> FindResourceIdAsync(
string resource,
string? subscription,
string? resourceGroup,
Expand All @@ -77,15 +78,15 @@ private Task<FindResourceIdResult> FindResourceIdAsync(

if (string.IsNullOrEmpty(subscription) || string.IsNullOrEmpty(resourceGroup))
{
return Task.FromResult<FindResourceIdResult>(new DidNotFindResourceResult($"Subscription ID and Resource Group are required to locate resource '{resource}'. Please provide both --subscription-name-or-id and --resource-group parameters."));
return Task.FromResult<Result<FoundResourceData>>(Result<FoundResourceData>.Failure($"Subscription ID and Resource Group are required to locate resource '{resource}'. Please provide both --subscription-name-or-id and --resource-group parameters."));
}

var resourceId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/{resourceType}/{resource}";

return Task.FromResult<FindResourceIdResult>(new FoundResourceResult(resourceId, resourceType ?? "Unknown", null));
return Task.FromResult<Result<FoundResourceData>>(Result<FoundResourceData>.Success(new FoundResourceData(resourceId, resourceType ?? "Unknown", null)));
}

private async Task<GetAppLensSessionResult> GetAppLensSessionAsync(string resourceId, string? tenantId = null, CancellationToken cancellationToken = default)
private async Task<Result<AppLensSession>> GetAppLensSessionAsync(string resourceId, string? tenantId = null, CancellationToken cancellationToken = default)
{
try
{
Expand All @@ -109,21 +110,21 @@ private async Task<GetAppLensSessionResult> GetAppLensSessionAsync(string resour
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new FailedAppLensSessionResult("The specified resource could not be found or does not support diagnostics.");
return Result<AppLensSession>.Failure("The specified resource could not be found or does not support diagnostics.");
}
return new FailedAppLensSessionResult($"Failed to create diagnostics session for resource {resourceId}, http response code: {response.StatusCode}");
return Result<AppLensSession>.Failure($"Failed to create diagnostics session for resource {resourceId}, http response code: {response.StatusCode}");
}

var content = await response.Content.ReadAsStringAsync(cancellationToken);
AppLensSession? appLensSession;
appLensSession = ParseGetTokenResponse(content);

return new SuccessfulAppLensSessionResult(
return Result<AppLensSession>.Success(
appLensSession with { ResourceId = resourceId });
}
catch (Exception ex)
{
return new FailedAppLensSessionResult($"Failed to create AppLens session: {ex.Message}");
return Result<AppLensSession>.Failure($"Failed to create AppLens session: {ex.Message}");
}
}

Expand Down Expand Up @@ -262,7 +263,7 @@ public async IAsyncEnumerable<ChatMessageResponseBody> AskAppLensAsync(
/// <param name="session">The AppLens session.</param>
/// <param name="question">The diagnostic question.</param>
/// <returns>A task containing diagnostic insights and solutions.</returns>
private async Task<DiagnosticResult> CollectInsightsAsync(AppLensSession session, string question, CancellationToken cancellationToken)
private async Task<AppLensInsights> CollectInsightsAsync(AppLensSession session, string question, CancellationToken cancellationToken)
{
var insights = new List<string>();
var solutions = new List<string>();
Expand All @@ -280,7 +281,7 @@ private async Task<DiagnosticResult> CollectInsightsAsync(AppLensSession session
}
}

return new DiagnosticResult(insights, solutions, session.ResourceId, "Resource");
return new AppLensInsights(insights, solutions, session.ResourceId, "Resource");
}

private static AppLensSession ParseGetTokenResponse(string rawResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface IAppLensService
/// <param name="resourceGroup">The resource group of the Azure resource to diagnose.</param>
/// <param name="resourceType">The resource type of the Azure resource to diagnose.</param>
/// <returns>A diagnostic result containing insights and solutions.</returns>
Task<DiagnosticResult> DiagnoseResourceAsync(
Task<Result<AppLensInsights>> DiagnoseResourceAsync(
string question,
string resource,
string subscription,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public ResourceDiagnoseCommandTests()
public async Task ExecuteAsync_ReturnsDiagnosticResult_WhenValidParametersProvided()
{
// Arrange
var expectedResult = new DiagnosticResult(
var expectedResult = new Success<AppLensInsights>(new AppLensInsights(
new List<string> { "Insight 1", "Insight 2" },
new List<string> { "Solution 1", "Solution 2" },
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.Web/sites/myapp",
"Microsoft.Web/sites"
);
));

_appLensService.DiagnoseResourceAsync(
"Why is my app slow?",
Expand Down Expand Up @@ -262,12 +262,12 @@ public async Task ExecuteAsync_Returns503_WhenServiceIsUnavailable()
public async Task ExecuteAsync_HandlesEmptyDiagnosticResult()
{
// Arrange
var expectedResult = new DiagnosticResult(
var expectedResult = new Success<AppLensInsights>(new AppLensInsights(
new List<string>(),
new List<string>(),
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.Web/sites/myapp",
"Microsoft.Web/sites"
);
));

_appLensService.DiagnoseResourceAsync(
"Why is my app slow?",
Expand Down Expand Up @@ -342,12 +342,12 @@ public async Task ExecuteAsync_Returns400_WhenRequiredParameterIsEmpty(
public async Task ExecuteAsync_LogsInformationOnSuccess()
{
// Arrange
var expectedResult = new DiagnosticResult(
var expectedResult = new Success<AppLensInsights>(new AppLensInsights(
new List<string> { "Insight 1" },
new List<string> { "Solution 1" },
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.Web/sites/myapp",
"Microsoft.Web/sites"
);
));

_appLensService.DiagnoseResourceAsync(
"Why is my app slow?",
Expand Down
Loading