Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -3198,7 +3198,7 @@
},
{
"name": "get_azure_appservice_details",
"description": "Get details about Azure App Service resources including Web Apps and web app deployments.",
"description": "Get details about Azure App Service resources including Web Apps, web app deployments, web app diagnostic detectors, and web app diagnostic diagnoses.",
"toolMetadata": {
"destructive": {
"value": false,
Expand Down Expand Up @@ -3227,7 +3227,9 @@
},
"mappedToolList": [
"appservice_webapp_get",
"appservice_webapp_deployment_get"
"appservice_webapp_deployment_get",
"appservice_webapp_diagnostic_diagnose",
"appservice_webapp_diagnostic_list"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Add tools for web app diagnostics"
46 changes: 46 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,52 @@ azmcp appservice webapp deployment get --subscription "my-subscription" \
--deployment-id "deployment-id"
```

#### Web App Diagnostics

```bash
# List detectors for an App Service Web App
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp appservice webapp diagnostic list --subscription <subscription> \
--resource-group <resource-group> \
--app <app>

# Examples:
# List diagnostic detectors for an App Service Web App
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp appservice webapp diagnostic list --subscription "my-subscription" \
--resource-group "my-resource-group" \
--app "my-web-app"
```

```bash
# Diagnose an App Service Web App with detector
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp appservice webapp diagnostic diagnose --subscription <subscription> \
--resource-group <resource-group> \
--app <app> \
--detector-name <detector-name> \
[--start-time <start-time>] \
[--end-time <end-time>] \
[--interval <interval>]

# Examples:
# Diagnose the Web App with detector
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp appservice webapp diagnostic diagnose --subscription "my-subscription" \
--resource-group "my-resource-group" \
--app "my-web-app" \
--detector-name "detector"

# Diagnose the Web App with detector between start and end time with interval
azmcp appservice webapp diagnostic diagnose --subscription "my-subscription" \
--resource-group "my-resource-group" \
--app "my-web-app" \
--detector-name "detector"
--start-time "2026-01-01T00:00:00Z" \
--end-time "2026-01-01T23:59:59Z" \
--interval "PT1H"
```

### Azure CLI Operations

#### Generate
Expand Down
3 changes: 3 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| appservice_database_add | Connect database <database_name> to my app service <app_name> using connection string <connection_string> in resource group <resource_group> |
| appservice_database_add | Set up database <database_name> for app service <app_name> with connection string <connection_string> under resource group <resource_group> |
| appservice_database_add | Configure database <database_name> for app service <app_name> with the connection string <connection_string> in resource group <resource_group> |
| appservice_webapp_diagnostic_diagnose | Diagnose web app <webapp> in <resource_group> with detector <detector_name> |
| appservice_webapp_diagnostic_diagnose | Diagnose web app <webapp> in <resource_group> with detector <detector_name> between <start_time> and <end_time> with interval <interval> |
| appservice_webapp_diagnostic_list | List the diagnostic detectors for web app <webapp> in <resource_group> |
| appservice_webapp_get | List the web apps in my subscription |
| appservice_webapp_get | Show me the web apps in my <resource_group> resource group |
| appservice_webapp_get | Get the details for web app <webapp> in <resource_group> |
Expand Down
30 changes: 22 additions & 8 deletions tools/Azure.Mcp.Tools.AppService/src/AppServiceSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.Mcp.Tools.AppService.Commands.Database;
using Azure.Mcp.Tools.AppService.Commands.Webapp;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Deployment;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Diagnostic;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Settings;
using Azure.Mcp.Tools.AppService.Services;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -23,6 +24,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<IAppServiceService, AppServiceService>();
services.AddSingleton<DatabaseAddCommand>();
services.AddSingleton<WebappGetCommand>();
services.AddSingleton<DetectorDiagnoseCommand>();
services.AddSingleton<DetectorListCommand>();
services.AddSingleton<AppSettingsGetCommand>();
services.AddSingleton<AppSettingsUpdateCommand>();
services.AddSingleton<DeploymentGetCommand>();
Expand Down Expand Up @@ -50,6 +53,25 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
var webappGet = serviceProvider.GetRequiredService<WebappGetCommand>();
webapp.AddCommand(webappGet.Name, webappGet);

// Add deployment subgroup
var deployment = new CommandGroup("deployment", "Operations for managing Azure App Service web app deployments");
webapp.AddSubGroup(deployment);

// Add deployment commands
var deploymentGet = serviceProvider.GetRequiredService<DeploymentGetCommand>();
deployment.AddCommand(deploymentGet.Name, deploymentGet);

// Add diagnostic subgroup under webapp
var diagnostic = new CommandGroup("diagnostic", "Operations for diagnosing Azure App Service web apps");
webapp.AddSubGroup(diagnostic);

// Add diagnostic commands
var detectorDiagnose = serviceProvider.GetRequiredService<DetectorDiagnoseCommand>();
diagnostic.AddCommand(detectorDiagnose.Name, detectorDiagnose);

var detectorList = serviceProvider.GetRequiredService<DetectorListCommand>();
diagnostic.AddCommand(detectorList.Name, detectorList);

// Add settings subgroup under webapp
var settings = new CommandGroup("settings", "Operations for managing Azure App Service web settings");
webapp.AddSubGroup(settings);
Expand All @@ -61,14 +83,6 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
var appSettingsUpdate = serviceProvider.GetRequiredService<AppSettingsUpdateCommand>();
settings.AddCommand(appSettingsUpdate.Name, appSettingsUpdate);

// Add deployment subgroup
var deployment = new CommandGroup("deployment", "Operations for managing Azure App Service web app deployments");
webapp.AddSubGroup(deployment);

// Add deployment commands
var deploymentGet = serviceProvider.GetRequiredService<DeploymentGetCommand>();
deployment.AddCommand(deploymentGet.Name, deploymentGet);

return appService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using Azure.Mcp.Tools.AppService.Commands.Database;
using Azure.Mcp.Tools.AppService.Commands.Webapp;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Deployment;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Diagnostic;
using Azure.Mcp.Tools.AppService.Commands.Webapp.Settings;
using Azure.Mcp.Tools.AppService.Models;
using Azure.ResourceManager.AppService.Models;

namespace Azure.Mcp.Tools.AppService.Commands;

Expand All @@ -15,6 +17,13 @@ namespace Azure.Mcp.Tools.AppService.Commands;
[JsonSerializable(typeof(DatabaseAddCommand.DatabaseAddResult))]
[JsonSerializable(typeof(DatabaseConnectionInfo))]
[JsonSerializable(typeof(DeploymentGetCommand.DeploymentGetResult))]
[JsonSerializable(typeof(DetectorDiagnoseCommand.DetectorDiagnoseResult))]
[JsonSerializable(typeof(DetectorDetails))]
[JsonSerializable(typeof(DetectorInfo))]
[JsonSerializable(typeof(DetectorListCommand.DetectorListResult))]
[JsonSerializable(typeof(DiagnosticDataset))]
[JsonSerializable(typeof(DiagnosisResults))]
[JsonSerializable(typeof(IList<DiagnosticDataset>))]
[JsonSerializable(typeof(WebappDetails))]
[JsonSerializable(typeof(WebappGetCommand.WebappGetResult))]
public partial class AppServiceJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
namespace Azure.Mcp.Tools.AppService.Commands;

public abstract class BaseAppServiceCommand<
[DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>(bool resourceGroupRequired = false)
[DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>(bool resourceGroupRequired = false, bool appRequired = false)
: SubscriptionCommand<TOptions>
where TOptions : BaseAppServiceOptions, new()
{
Expand All @@ -21,12 +21,16 @@ protected override void RegisterOptions(Command command)
command.Options.Add(resourceGroupRequired
? OptionDefinitions.Common.ResourceGroup.AsRequired()
: OptionDefinitions.Common.ResourceGroup.AsOptional());
command.Options.Add(appRequired
? AppServiceOptionDefinitions.AppServiceName.AsRequired()
: AppServiceOptionDefinitions.AppServiceName.AsOptional());
}

protected override TOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.ResourceGroup ??= parseResult.GetValueOrDefault<string>(OptionDefinitions.Common.ResourceGroup.Name);
options.AppName = parseResult.GetValueOrDefault<string>(AppServiceOptionDefinitions.AppServiceName.Name);
return options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
namespace Azure.Mcp.Tools.AppService.Commands.Database;

public sealed class DatabaseAddCommand(ILogger<DatabaseAddCommand> logger)
: BaseAppServiceCommand<DatabaseAddOptions>(resourceGroupRequired: true)
: BaseAppServiceCommand<DatabaseAddOptions>(resourceGroupRequired: true, appRequired: true)
{
private const string CommandTitle = "Add Database to App Service";
private readonly ILogger<DatabaseAddCommand> _logger = logger;
Expand Down Expand Up @@ -43,7 +43,6 @@ public sealed class DatabaseAddCommand(ILogger<DatabaseAddCommand> logger)
protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(AppServiceOptionDefinitions.AppServiceName);
command.Options.Add(AppServiceOptionDefinitions.DatabaseTypeOption);
command.Options.Add(AppServiceOptionDefinitions.DatabaseServerOption);
command.Options.Add(AppServiceOptionDefinitions.DatabaseNameOption);
Expand All @@ -53,7 +52,6 @@ protected override void RegisterOptions(Command command)
protected override DatabaseAddOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.AppName = parseResult.GetValueOrDefault(AppServiceOptionDefinitions.AppServiceName);
options.DatabaseType = parseResult.GetValueOrDefault(AppServiceOptionDefinitions.DatabaseTypeOption);
options.DatabaseServer = parseResult.GetValueOrDefault(AppServiceOptionDefinitions.DatabaseServerOption);
options.DatabaseName = parseResult.GetValueOrDefault(AppServiceOptionDefinitions.DatabaseNameOption);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace Azure.Mcp.Tools.AppService.Commands.Webapp.Deployment;

public sealed class DeploymentGetCommand(ILogger<DeploymentGetCommand> logger)
: BaseAppServiceCommand<DeploymentGetOptions>(resourceGroupRequired: true)
: BaseAppServiceCommand<DeploymentGetOptions>(resourceGroupRequired: true, appRequired: true)
{
private const string CommandTitle = "Gets Azure App Service Web App Deployment Details";
private readonly ILogger<DeploymentGetCommand> _logger = logger;
Expand Down Expand Up @@ -44,14 +44,12 @@ public sealed class DeploymentGetCommand(ILogger<DeploymentGetCommand> logger)
protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(AppServiceOptionDefinitions.AppServiceName);
command.Options.Add(AppServiceOptionDefinitions.DeploymentIdOption);
}

protected override DeploymentGetOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.AppName = parseResult.GetValueOrDefault<string>(AppServiceOptionDefinitions.AppServiceName.Name);
options.DeploymentId = parseResult.GetValueOrDefault<string>(AppServiceOptionDefinitions.DeploymentIdOption.Name);
return options;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Tools.AppService.Models;
using Azure.Mcp.Tools.AppService.Options;
using Azure.Mcp.Tools.AppService.Options.Webapp.Diagnostic;
using Azure.Mcp.Tools.AppService.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Extensions;
using Microsoft.Mcp.Core.Models.Command;
using Microsoft.Mcp.Core.Models.Option;

namespace Azure.Mcp.Tools.AppService.Commands.Webapp.Diagnostic;

public sealed class DetectorDiagnoseCommand(ILogger<DetectorDiagnoseCommand> logger)
: BaseAppServiceCommand<DetectorDiagnoseOptions>(resourceGroupRequired: true, appRequired: true)
{
private const string CommandTitle = "Diagnose an App Service Web App";
private readonly ILogger<DetectorDiagnoseCommand> _logger = logger;
public override string Id => "a8aa0966-4c0c-4e22-8854-cced583f0fb2";
public override string Name => "diagnose";

public override string Description =>
"""
Diagnoses an App Service Web App with the specified detector, returning the diagnostic results of the detector.
""";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new()
{
Destructive = false,
Idempotent = true,
OpenWorld = false,
ReadOnly = true,
Secret = false,
LocalRequired = false
};

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(AppServiceOptionDefinitions.DetectorName.AsRequired());
command.Options.Add(AppServiceOptionDefinitions.StartTime);
command.Options.Add(AppServiceOptionDefinitions.EndTime);
command.Options.Add(AppServiceOptionDefinitions.Interval);
command.Validators.Add(result =>
{
var startTime = result.GetValueOrDefault<string?>(AppServiceOptionDefinitions.StartTime.Name);
var endTime = result.GetValueOrDefault<string?>(AppServiceOptionDefinitions.EndTime.Name);

bool hasStartTime = !string.IsNullOrEmpty(startTime);
bool hasEndTime = !string.IsNullOrEmpty(endTime);

if (hasStartTime && !DateTimeOffset.TryParse(startTime, out _))
{
result.AddError($"Invalid start time format: {startTime}. Please provide a valid ISO format date time string.");
}

if (hasEndTime && !DateTimeOffset.TryParse(endTime, out _))
{
result.AddError($"Invalid end time format: {endTime}. Please provide a valid ISO format date time string.");
}

if (hasStartTime && hasEndTime
&& DateTimeOffset.TryParse(startTime, out var start)
&& DateTimeOffset.TryParse(endTime, out var end)
&& start > end)
{
result.AddError($"Start time '{startTime}' must be earlier than end time '{endTime}'.");
}
});
}

protected override DetectorDiagnoseOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.DetectorName = parseResult.GetValueOrDefault<string>(AppServiceOptionDefinitions.DetectorName.Name);
if (DateTimeOffset.TryParse(parseResult.GetValueOrDefault<string?>(AppServiceOptionDefinitions.StartTime.Name), out var startTime))
{
options.StartTime = startTime.ToUniversalTime();
}
if (DateTimeOffset.TryParse(parseResult.GetValueOrDefault<string?>(AppServiceOptionDefinitions.EndTime.Name), out var endTime))
{
options.EndTime = endTime.ToUniversalTime();
}
options.Interval = parseResult.GetValueOrDefault<string?>(AppServiceOptionDefinitions.Interval.Name);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
{
// Validate first, then bind
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
{
return context.Response;
}

var options = BindOptions(parseResult);

try
{
context.Activity?.AddTag("subscription", options.Subscription);

var appServiceService = context.GetService<IAppServiceService>();
var diagnoses = await appServiceService.DiagnoseDetectorAsync(
options.Subscription!,
options.ResourceGroup!,
options.AppName!,
options.DetectorName!,
options.StartTime,
options.EndTime,
options.Interval,
options.Tenant,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(new(diagnoses), AppServiceJsonContext.Default.DetectorDiagnoseResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get diagnostic detectors for Web App '{AppName}' in subscription {Subscription} and resource group {ResourceGroup}",
options.AppName, options.Subscription, options.ResourceGroup);
HandleException(context, ex);
}

return context.Response;
}

public record DetectorDiagnoseResult(DiagnosisResults Diagnoses);
}
Loading
Loading