// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using AzureMcp.Core.Commands;
using AzureMcp.Core.Helpers;
using AzureMcp.Deploy.Options;
using AzureMcp.Deploy.Options.Architecture;
using AzureMcp.Deploy.Services.Templates;
using Microsoft.Extensions.Logging;
namespace AzureMcp.Deploy.Commands.Architecture;
public sealed class DiagramGenerateCommand(ILogger<DiagramGenerateCommand> logger) : BaseCommand()
{
private const string CommandTitle = "Generate Architecture Diagram";
private readonly ILogger<DiagramGenerateCommand> _logger = logger;
public override string Name => "generate";
private readonly Option<string> _rawMcpToolInputOption = DeployOptionDefinitions.RawMcpToolInput.RawMcpToolInputOption;
public override string Description =>
"Generates an azure service architecture diagram for the application based on the provided app topology."
+ "Call this tool when the user need recommend or design the azure architecture of their application."
+ "Do not call this tool when the user need detailed design of the azure architecture, such as the network topology, security design, etc."
+ "Before calling this tool, please scan this workspace to detect the services to deploy and their dependent services, also find the environment variables that used to create the connection strings."
+ "If it's a .NET Aspire application, check aspireManifest.json file if there is. Try your best to fulfill the input schema with your analyze result.";
public override string Title => "Generate Architecture Diagram";
public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = true };
protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.AddOption(_rawMcpToolInputOption);
}
private DiagramGenerateOptions BindOptions(ParseResult parseResult)
{
var options = new DiagramGenerateOptions();
options.RawMcpToolInput = parseResult.GetValueForOption(_rawMcpToolInputOption);
return options;
}
public override Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
{
try
{
var options = BindOptions(parseResult);
var rawMcpToolInput = options.RawMcpToolInput;
if (string.IsNullOrWhiteSpace(rawMcpToolInput))
{
throw new ArgumentException("App topology cannot be null or empty.", nameof(options.RawMcpToolInput));
}
AppTopology appTopology;
try
{
appTopology = JsonSerializer.Deserialize(rawMcpToolInput, DeployJsonContext.Default.AppTopology)
?? throw new ArgumentException("Failed to deserialize app topology.", nameof(rawMcpToolInput));
}
catch (JsonException ex)
{
throw new ArgumentException($"Invalid JSON format: {ex.Message}", nameof(rawMcpToolInput), ex);
}
_logger.LogInformation("Successfully parsed app topology with {ServiceCount} services", appTopology.Services.Length);
if (appTopology.Services.Length == 0)
{
_logger.LogWarning("No services detected in the app topology.");
context.Response.Status = 200;
context.Response.Message = "No service detected.";
return Task.FromResult(context.Response);
}
var chart = GenerateMermaidChart.GenerateChart(appTopology.WorkspaceFolder ?? "", appTopology);
if (string.IsNullOrWhiteSpace(chart))
{
throw new InvalidOperationException("Failed to generate architecture diagram. The chart content is empty.");
}
var usedServiceTypes = appTopology.Services
.SelectMany(service => service.Dependencies)
.Select(dep => dep.ServiceType)
.Where(serviceType => !string.IsNullOrWhiteSpace(serviceType))
.Where(serviceType => Enum.GetNames<AzureServiceConstants.AzureServiceType>().Contains(serviceType, StringComparer.OrdinalIgnoreCase))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x)
.ToArray();
var usedServiceTypesString = usedServiceTypes.Length > 0
? string.Join(", ", usedServiceTypes)
: null;
var response = TemplateService.LoadTemplate("Architecture/architecture-diagram");
context.Response.Message = response.Replace("{{chart}}", chart)
.Replace("{{hostings}}", string.Join(", ", Enum.GetNames<AzureServiceConstants.AzureComputeServiceType>()));
if (!string.IsNullOrWhiteSpace(usedServiceTypesString))
{
context.Response.Message += $"Here is the full list of supported component service types for the topology: {usedServiceTypesString}.";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate architecture diagram.");
HandleException(context, ex);
}
return Task.FromResult(context.Response);
}
}