From 1960132e36d2b62eccc7fac5144fb861369882a5 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Tue, 29 Jul 2025 17:09:39 -0500 Subject: [PATCH 01/29] added cloud architect tool --- AzureMcp.sln | 39 ++ .../AzureMcp.CloudArchitect/AssemblyInfo.cs | 8 + .../AzureMcp.CloudArchitect.csproj | 17 + .../CloudArchitectJsonContext.cs | 17 + .../CloudArchitectSetup.cs | 30 ++ .../Commands/BaseCloudArchitectCommand.cs | 15 + .../Commands/Design/DesignCommand.cs | 136 ++++++ .../AzureMcp.CloudArchitect/GlobalUsings.cs | 11 + .../Models/ArchitectureModels.cs | 34 ++ .../Options/BaseCloudArchitectOptions.cs | 10 + .../CloudArchitectOptionDefinitions.cs | 44 ++ .../Options/Design/DesignOptions.cs | 21 + .../Services/CloudArchitectService.cs | 409 ++++++++++++++++++ .../Services/ICloudArchitectService.cs | 17 + .../AzureMcp.CloudArchitect.UnitTests.csproj | 19 + .../Design/DesignCommandTests.cs | 192 ++++++++ .../GlobalUsings.cs | 13 + core/src/AzureMcp.Cli/Program.cs | 1 + 18 files changed, 1033 insertions(+) create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/GlobalUsings.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs create mode 100644 areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/AzureMcp.CloudArchitect.UnitTests.csproj create mode 100644 areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs create mode 100644 areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs diff --git a/AzureMcp.sln b/AzureMcp.sln index 646299944..004c996c4 100644 --- a/AzureMcp.sln +++ b/AzureMcp.sln @@ -283,6 +283,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.Core.UnitTests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.Tests", "core\tests\AzureMcp.Tests\AzureMcp.Tests.csproj", "{527FE0F6-40AE-4E71-A483-0F0A2368F2A7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cloudarchitect", "cloudarchitect", "{3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C239126A-8A2E-168D-6172-7C58CE7AEB0A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.CloudArchitect", "areas\cloudarchitect\src\AzureMcp.CloudArchitect\AzureMcp.CloudArchitect.csproj", "{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{90811FCA-7295-E394-37C4-E1FD75D058A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.CloudArchitect.UnitTests", "areas\cloudarchitect\tests\AzureMcp.CloudArchitect.UnitTests\AzureMcp.CloudArchitect.UnitTests.csproj", "{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1097,6 +1107,30 @@ Global {527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x64.Build.0 = Release|Any CPU {527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x86.ActiveCfg = Release|Any CPU {527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x86.Build.0 = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x64.ActiveCfg = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x64.Build.0 = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x86.ActiveCfg = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x86.Build.0 = Debug|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|Any CPU.Build.0 = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x64.ActiveCfg = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x64.Build.0 = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x86.ActiveCfg = Release|Any CPU + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x86.Build.0 = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x64.Build.0 = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x86.Build.0 = Debug|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|Any CPU.Build.0 = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x64.ActiveCfg = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x64.Build.0 = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x86.ActiveCfg = Release|Any CPU + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1240,5 +1274,10 @@ Global {2A3CD1B4-38A3-46A1-AEDC-2C2AC47CB8F1} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA} {1AE3FC50-8E8C-4637-AAB1-A871D5FB4535} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA} {527FE0F6-40AE-4E71-A483-0F0A2368F2A7} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA} + {3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32} = {87783708-79E3-AD60-C783-1D52BE7DE4BB} + {C239126A-8A2E-168D-6172-7C58CE7AEB0A} = {3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32} + {6250B2F1-D4C1-4B4D-B15E-F65CEDB05439} = {C239126A-8A2E-168D-6172-7C58CE7AEB0A} + {90811FCA-7295-E394-37C4-E1FD75D058A2} = {3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32} + {FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559} = {90811FCA-7295-E394-37C4-E1FD75D058A2} EndGlobalSection EndGlobal diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs new file mode 100644 index 000000000..b02f7efec --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Azure/azure-mcp")] +[assembly: InternalsVisibleTo("AzureMcp.CloudArchitect.UnitTests")] diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj new file mode 100644 index 000000000..58f224351 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj @@ -0,0 +1,17 @@ + + + true + + + + + + + + + + + + + + diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs new file mode 100644 index 000000000..40144f745 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Commands.Design; +using AzureMcp.CloudArchitect.Models; + +namespace AzureMcp.CloudArchitect; + +[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(DesignCommandResult))] +[JsonSerializable(typeof(ArchitectureDesign))] +[JsonSerializable(typeof(ArchitectureRecommendation))] +[JsonSerializable(typeof(SecurityConsideration))] +[JsonSerializable(typeof(CostOptimization))] +public partial class CloudArchitectJsonContext : JsonSerializerContext +{ +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs new file mode 100644 index 000000000..e4af908e2 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.Core.Areas; +using AzureMcp.Core.Commands; +using AzureMcp.CloudArchitect.Commands.Design; +using AzureMcp.CloudArchitect.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AzureMcp.CloudArchitect; + +public class CloudArchitectSetup : IAreaSetup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + } + + public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory) + { + // Create CloudArchitect command group + var cloudArchitect = new CommandGroup("cloudarchitect", "Cloud Architecture operations - Commands for generating Azure architecture designs and recommendations based on requirements."); + rootGroup.AddSubGroup(cloudArchitect); + + // Register CloudArchitect commands + cloudArchitect.AddCommand("design", new DesignCommand( + loggerFactory.CreateLogger())); + } +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs new file mode 100644 index 000000000..5ba9958e5 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using AzureMcp.Core.Commands; +using AzureMcp.CloudArchitect.Options; + +namespace AzureMcp.CloudArchitect.Commands; + +public abstract class BaseCloudArchitectCommand< + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] T> + : GlobalCommand + where T : BaseCloudArchitectOptions, new() +{ +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs new file mode 100644 index 000000000..a1bdac667 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Options; +using AzureMcp.CloudArchitect.Options.Design; +using AzureMcp.CloudArchitect.Services; +using AzureMcp.Core.Commands; +using AzureMcp.Core.Models; +using Microsoft.Extensions.Logging; + +namespace AzureMcp.CloudArchitect.Commands.Design; + +public sealed class DesignCommand(ILogger logger) : BaseCloudArchitectCommand +{ + private const string CommandTitle = "Generate Cloud Architecture Design"; + private readonly ILogger _logger = logger; + + // Define options from OptionDefinitions + private readonly Option _requirementsOption = CloudArchitectOptionDefinitions.RequirementsOption; + private readonly Option _workloadTypeOption = CloudArchitectOptionDefinitions.WorkloadTypeOption; + private readonly Option _scaleRequirementsOption = CloudArchitectOptionDefinitions.ScaleRequirementsOption; + private readonly Option _complianceRequirementsOption = CloudArchitectOptionDefinitions.ComplianceRequirementsOption; + + public override string Name => "design"; + + public override string Description => + """ + Generate a comprehensive Azure cloud architecture design based on your requirements. + This command analyzes your application requirements and provides detailed recommendations for: + - Azure service selection and configuration + - Security and compliance considerations + - Cost optimization strategies + - Deployment and monitoring approaches + - Implementation next steps + + Returns a structured architecture design with components, security considerations, and cost optimizations. + + Required options: + - --requirements: Detailed description of your application or system requirements + + Optional options: + - --workload-type: Type of workload (web-application, api-backend, data-processing, microservices, iot, machine-learning) + - --scale-requirements: Expected scale and performance requirements + - --compliance-requirements: Compliance and security requirements (GDPR, HIPAA, SOC2, etc.) + + Examples: + azmcp cloudarchitect design --requirements "E-commerce platform with user authentication, product catalog, and payment processing" + azmcp cloudarchitect design --requirements "Real-time data processing pipeline for IoT sensors" --workload-type "data-processing" --scale-requirements "1M events per day, global distribution" + azmcp cloudarchitect design --requirements "Healthcare patient portal" --compliance-requirements "HIPAA" --scale-requirements "10K concurrent users" + """; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + ReadOnly = true + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.AddOption(_requirementsOption); + command.AddOption(_workloadTypeOption); + command.AddOption(_scaleRequirementsOption); + command.AddOption(_complianceRequirementsOption); + } + + protected override DesignOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Requirements = parseResult.GetValueForOption(_requirementsOption); + options.WorkloadType = parseResult.GetValueForOption(_workloadTypeOption); + options.ScaleRequirements = parseResult.GetValueForOption(_scaleRequirementsOption); + options.ComplianceRequirements = parseResult.GetValueForOption(_complianceRequirementsOption); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) + { + var options = BindOptions(parseResult); + + try + { + // Required validation step + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + // Get the cloud architect service from DI + var service = context.GetService(); + + // Generate architecture design + var design = await service.GenerateArchitectureDesign( + options.Requirements!, + options.WorkloadType, + options.ScaleRequirements, + options.ComplianceRequirements, + options.RetryPolicy); + + // Set results + context.Response.Results = ResponseResult.Create( + new DesignCommandResult(design), + CloudArchitectJsonContext.Default.DesignCommandResult); + } + catch (Exception ex) + { + // Log error with all relevant context + _logger.LogError(ex, + "Error generating architecture design. Requirements: {Requirements}, WorkloadType: {WorkloadType}, Options: {@Options}", + options.Requirements, options.WorkloadType, options); + HandleException(context, ex); + } + + return context.Response; + } + + // Implementation-specific error handling + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => $"Invalid requirements provided. {argEx.Message}", + InvalidOperationException opEx => $"Unable to generate architecture design. {opEx.Message}", + _ => base.GetErrorMessage(ex) + }; + + protected override int GetStatusCode(Exception ex) => ex switch + { + ArgumentException => 400, + InvalidOperationException => 422, + _ => base.GetStatusCode(ex) + }; +} + +// Strongly-typed result record +public record DesignCommandResult(AzureMcp.CloudArchitect.Models.ArchitectureDesign Design); diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/GlobalUsings.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/GlobalUsings.cs new file mode 100644 index 000000000..923301984 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/GlobalUsings.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.CommandLine; +global using System.CommandLine.Parsing; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using AzureMcp.Core.Extensions; +global using AzureMcp.Core.Models; +global using AzureMcp.Core.Models.Command; +global using ModelContextProtocol.Server; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs new file mode 100644 index 000000000..a61699ffa --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AzureMcp.CloudArchitect.Models; + +public record ArchitectureRecommendation( + string Component, + string Service, + string Reasoning, + string Configuration, + List Alternatives +); + +public record SecurityConsideration( + string Area, + string Recommendation, + string Implementation +); + +public record CostOptimization( + string Service, + string Recommendation, + string EstimatedSavings +); + +public record ArchitectureDesign( + string OverallArchitecture, + List Components, + List Security, + List CostOptimizations, + string DeploymentConsiderations, + string MonitoringStrategy, + List NextSteps +); diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs new file mode 100644 index 000000000..4fc93b9b8 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.Core.Options; + +namespace AzureMcp.CloudArchitect.Options; + +public class BaseCloudArchitectOptions : GlobalOptions +{ +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs new file mode 100644 index 000000000..944b80ada --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AzureMcp.CloudArchitect.Options; + +public static class CloudArchitectOptionDefinitions +{ + public const string Requirements = "requirements"; + public const string WorkloadType = "workload-type"; + public const string ScaleRequirements = "scale-requirements"; + public const string ComplianceRequirements = "compliance-requirements"; + + public static readonly Option RequirementsOption = new( + $"--{Requirements}", + "Detailed description of the application or system requirements, including functionality, expected users, and key features." + ) + { + IsRequired = true + }; + + public static readonly Option WorkloadTypeOption = new( + $"--{WorkloadType}", + "The type of workload (e.g., web-application, data-processing, microservices, iot, machine-learning, api-backend)." + ) + { + IsRequired = false + }; + + public static readonly Option ScaleRequirementsOption = new( + $"--{ScaleRequirements}", + "Expected scale and performance requirements (e.g., number of users, data volume, requests per second, geographic distribution)." + ) + { + IsRequired = false + }; + + public static readonly Option ComplianceRequirementsOption = new( + $"--{ComplianceRequirements}", + "Compliance and security requirements (e.g., GDPR, HIPAA, SOC2, data residency requirements)." + ) + { + IsRequired = false + }; +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs new file mode 100644 index 000000000..8466a2453 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options.Design; + +public class DesignOptions : BaseCloudArchitectOptions +{ + [JsonPropertyName(CloudArchitectOptionDefinitions.Requirements)] + public string? Requirements { get; set; } + + [JsonPropertyName(CloudArchitectOptionDefinitions.WorkloadType)] + public string? WorkloadType { get; set; } + + [JsonPropertyName(CloudArchitectOptionDefinitions.ScaleRequirements)] + public string? ScaleRequirements { get; set; } + + [JsonPropertyName(CloudArchitectOptionDefinitions.ComplianceRequirements)] + public string? ComplianceRequirements { get; set; } +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs new file mode 100644 index 000000000..8fcfca756 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs @@ -0,0 +1,409 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Models; +using AzureMcp.Core.Options; +using Microsoft.Extensions.Logging; + +namespace AzureMcp.CloudArchitect.Services; + +public class CloudArchitectService(ILogger logger) : ICloudArchitectService +{ + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public async Task GenerateArchitectureDesign( + string requirements, + string? workloadType = null, + string? scaleRequirements = null, + string? complianceRequirements = null, + RetryPolicyOptions? retryPolicy = null) + { + _logger.LogInformation("Generating architecture design for requirements: {Requirements}", requirements); + + try + { + // Analyze requirements and generate architecture recommendations + var components = await GenerateComponentRecommendations(requirements, workloadType, scaleRequirements); + var security = await GenerateSecurityConsiderations(requirements, complianceRequirements); + var costOptimizations = await GenerateCostOptimizations(components); + var overallArchitecture = await GenerateOverallArchitecture(requirements, workloadType, components); + var deploymentConsiderations = await GenerateDeploymentConsiderations(components); + var monitoringStrategy = await GenerateMonitoringStrategy(components); + var nextSteps = await GenerateNextSteps(components); + + return new ArchitectureDesign( + overallArchitecture, + components, + security, + costOptimizations, + deploymentConsiderations, + monitoringStrategy, + nextSteps + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating architecture design for requirements: {Requirements}", requirements); + throw; + } + } + + private async Task> GenerateComponentRecommendations( + string requirements, + string? workloadType, + string? scaleRequirements) + { + await Task.Delay(100); // Simulate processing time + + var components = new List(); + + // Determine workload type if not specified + var inferredWorkloadType = workloadType ?? InferWorkloadType(requirements); + + // Generate recommendations based on workload type and requirements + switch (inferredWorkloadType.ToLowerInvariant()) + { + case "web-application": + components.AddRange(GetWebApplicationComponents(requirements, scaleRequirements)); + break; + case "api-backend": + components.AddRange(GetApiBackendComponents(requirements, scaleRequirements)); + break; + case "data-processing": + components.AddRange(GetDataProcessingComponents(requirements, scaleRequirements)); + break; + case "microservices": + components.AddRange(GetMicroservicesComponents(requirements, scaleRequirements)); + break; + case "iot": + components.AddRange(GetIoTComponents(requirements, scaleRequirements)); + break; + case "machine-learning": + components.AddRange(GetMLComponents(requirements, scaleRequirements)); + break; + default: + components.AddRange(GetGeneralPurposeComponents(requirements, scaleRequirements)); + break; + } + + return components; + } + + private string InferWorkloadType(string requirements) + { + var lowerRequirements = requirements.ToLowerInvariant(); + + if (lowerRequirements.Contains("web") || lowerRequirements.Contains("frontend") || lowerRequirements.Contains("ui")) + return "web-application"; + if (lowerRequirements.Contains("api") || lowerRequirements.Contains("rest") || lowerRequirements.Contains("backend")) + return "api-backend"; + if (lowerRequirements.Contains("data") || lowerRequirements.Contains("etl") || lowerRequirements.Contains("processing")) + return "data-processing"; + if (lowerRequirements.Contains("microservice") || lowerRequirements.Contains("distributed")) + return "microservices"; + if (lowerRequirements.Contains("iot") || lowerRequirements.Contains("sensor") || lowerRequirements.Contains("device")) + return "iot"; + if (lowerRequirements.Contains("machine learning") || lowerRequirements.Contains("ai") || lowerRequirements.Contains("ml")) + return "machine-learning"; + + return "general-purpose"; + } + + private List GetWebApplicationComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("Frontend Hosting", "Azure Static Web Apps", + "Optimized for static content with global CDN distribution and built-in CI/CD", + "Configure custom domain, SSL certificates, and API integration", + new List { "Azure App Service", "Azure Storage Static Website", "Azure Front Door + Storage" }), + new("Backend API", "Azure App Service", + "Managed platform with built-in scaling, monitoring, and deployment slots", + "Use App Service Plan with auto-scaling rules based on CPU and memory", + new List { "Azure Container Apps", "Azure Functions", "Azure Kubernetes Service" }), + new("Database", "Azure SQL Database", + "Fully managed SQL database with automatic scaling and backup", + "Use General Purpose tier with automatic tuning enabled", + new List { "Azure Cosmos DB", "Azure Database for PostgreSQL", "Azure Database for MySQL" }), + new("Authentication", "Azure Active Directory B2C", + "Enterprise-grade identity management with social login integration", + "Configure user flows, custom policies, and API permissions", + new List { "Azure Active Directory", "Azure AD External Identities", "Third-party auth providers" }), + new("CDN & Caching", "Azure Front Door", + "Global load balancing with intelligent routing and caching", + "Configure caching rules, compression, and WAF protection", + new List { "Azure CDN", "Azure Application Gateway", "CloudFlare" }) + }; + } + + private List GetApiBackendComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("API Gateway", "Azure API Management", + "Centralized API management with security, throttling, and analytics", + "Configure rate limiting, authentication policies, and developer portal", + new List { "Azure Application Gateway", "Azure Front Door", "Open source solutions" }), + new("Compute", "Azure Container Apps", + "Serverless containers with automatic scaling and simplified management", + "Configure container resources, scaling rules, and health probes", + new List { "Azure App Service", "Azure Kubernetes Service", "Azure Functions" }), + new("Database", "Azure Cosmos DB", + "Multi-model database with global distribution for high-performance APIs", + "Use SQL API with consistent indexing and automatic scaling", + new List { "Azure SQL Database", "Azure Database for PostgreSQL", "Redis Cache" }), + new("Monitoring", "Azure Application Insights", + "Application performance monitoring with distributed tracing", + "Configure custom telemetry, alerts, and performance counters", + new List { "Azure Monitor", "Azure Log Analytics", "Third-party APM tools" }) + }; + } + + private List GetDataProcessingComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("Data Ingestion", "Azure Event Hubs", + "High-throughput data streaming platform for real-time ingestion", + "Configure partitioning, retention policies, and consumer groups", + new List { "Azure Service Bus", "Azure Storage Queue", "Apache Kafka on HDInsight" }), + new("Data Processing", "Azure Databricks", + "Apache Spark-based analytics platform with collaborative notebooks", + "Use cluster policies, auto-scaling, and delta lake for optimized performance", + new List { "Azure Synapse Analytics", "Azure HDInsight", "Azure Data Factory" }), + new("Data Storage", "Azure Data Lake Storage Gen2", + "Hierarchical storage optimized for big data analytics workloads", + "Enable hierarchical namespace and configure access tiers", + new List { "Azure Blob Storage", "Azure SQL Data Warehouse", "Azure Cosmos DB" }), + new("Orchestration", "Azure Data Factory", + "Cloud-based data integration service for ETL/ELT pipelines", + "Create pipelines with monitoring, alerting, and error handling", + new List { "Azure Logic Apps", "Azure Functions", "Apache Airflow" }) + }; + } + + private List GetMicroservicesComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("Container Orchestration", "Azure Kubernetes Service", + "Managed Kubernetes for container orchestration with enterprise security", + "Configure node pools, network policies, and Azure AD integration", + new List { "Azure Container Apps", "Azure Service Fabric", "Azure Container Instances" }), + new("Service Mesh", "Istio on AKS", + "Service mesh for traffic management, security, and observability", + "Configure ingress gateway, traffic policies, and distributed tracing", + new List { "Linkerd", "Consul Connect", "Azure Service Fabric Mesh" }), + new("API Gateway", "Azure API Management", + "Centralized gateway for microservices with rate limiting and security", + "Configure backend pools, transformation policies, and developer portal", + new List { "Kong", "Ambassador", "Istio Ingress Gateway" }), + new("Database per Service", "Azure Cosmos DB", + "Multi-model database supporting different data models per microservice", + "Use appropriate APIs (SQL, MongoDB, Cassandra) based on service needs", + new List { "Azure SQL Database", "Azure Database for PostgreSQL", "Azure Redis Cache" }) + }; + } + + private List GetIoTComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("IoT Platform", "Azure IoT Hub", + "Managed service for bi-directional communication with IoT devices", + "Configure device provisioning, routing rules, and security certificates", + new List { "Azure IoT Central", "Azure Event Hubs", "Third-party IoT platforms" }), + new("Device Management", "Azure IoT Device Provisioning Service", + "Zero-touch device provisioning and lifecycle management", + "Configure enrollment groups, attestation methods, and custom allocation policies", + new List { "Manual provisioning", "Azure IoT Central device management", "Third-party device management" }), + new("Stream Processing", "Azure Stream Analytics", + "Real-time analytics on streaming data from IoT devices", + "Create queries for filtering, aggregation, and anomaly detection", + new List { "Azure Functions", "Azure Event Hubs with custom processing", "Apache Storm" }), + new("Time Series Storage", "Azure Time Series Insights", + "Analytics service for time-series data with interactive exploration", + "Configure data retention, access policies, and reference data", + new List { "Azure Cosmos DB", "Azure Data Explorer", "InfluxDB" }) + }; + } + + private List GetMLComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("ML Platform", "Azure Machine Learning", + "End-to-end ML lifecycle management with automated ML and MLOps", + "Configure compute instances, data stores, and model deployment endpoints", + new List { "Azure Databricks", "Azure Cognitive Services", "Open source ML frameworks" }), + new("Data Storage", "Azure Data Lake Storage Gen2", + "Scalable storage for training data with hierarchical organization", + "Enable versioning, access control, and lifecycle management", + new List { "Azure Blob Storage", "Azure SQL Database", "Azure Cosmos DB" }), + new("Model Serving", "Azure Container Instances", + "Serverless containers for deploying ML models as web services", + "Configure auto-scaling, health probes, and A/B testing", + new List { "Azure Kubernetes Service", "Azure App Service", "Azure Functions" }), + new("Feature Store", "Azure ML Feature Store", + "Centralized repository for ML features with versioning and lineage", + "Configure feature sets, transformations, and access permissions", + new List { "Azure Cosmos DB", "Azure SQL Database", "Custom feature store solution" }) + }; + } + + private List GetGeneralPurposeComponents(string requirements, string? scaleRequirements) + { + return new List + { + new("Compute", "Azure App Service", + "Managed application hosting with built-in scaling and monitoring", + "Configure auto-scaling rules, deployment slots, and custom domains", + new List { "Azure Virtual Machines", "Azure Container Apps", "Azure Functions" }), + new("Database", "Azure SQL Database", + "Managed relational database with automatic tuning and backup", + "Use appropriate service tier based on performance requirements", + new List { "Azure Cosmos DB", "Azure Database for PostgreSQL", "Azure Database for MySQL" }), + new("Storage", "Azure Blob Storage", + "Object storage for unstructured data with multiple access tiers", + "Configure appropriate access tier and lifecycle management", + new List { "Azure Files", "Azure Data Lake Storage", "Azure Managed Disks" }), + new("Security", "Azure Key Vault", + "Centralized secrets management with HSM-backed key protection", + "Configure access policies, key rotation, and audit logging", + new List { "Azure Managed Identity", "Azure Active Directory", "Third-party secret managers" }) + }; + } + + private async Task> GenerateSecurityConsiderations(string requirements, string? complianceRequirements) + { + await Task.Delay(50); // Simulate processing time + + var security = new List + { + new("Identity & Access", + "Implement Azure Active Directory with multi-factor authentication and conditional access policies", + "Configure Azure AD, enable MFA, set up conditional access rules based on risk levels"), + new("Network Security", + "Use Azure Virtual Networks with Network Security Groups and Azure Firewall for network isolation", + "Configure VNets, NSGs, and firewall rules to restrict traffic flow between components"), + new("Data Encryption", + "Enable encryption at rest and in transit using Azure-managed keys with option for customer-managed keys", + "Configure TLS 1.2+ for all communications and enable transparent data encryption"), + new("Secrets Management", + "Store all secrets, keys, and certificates in Azure Key Vault with proper access controls", + "Use managed identities where possible and implement key rotation policies"), + new("Monitoring & Auditing", + "Implement comprehensive logging with Azure Monitor and enable Azure Security Center", + "Configure diagnostic settings, set up security alerts, and enable audit logging") + }; + + // Add compliance-specific considerations + if (!string.IsNullOrEmpty(complianceRequirements)) + { + var compliance = complianceRequirements.ToLowerInvariant(); + + if (compliance.Contains("gdpr")) + { + security.Add(new("GDPR Compliance", + "Implement data privacy controls with right to erasure and data portability", + "Configure data retention policies, implement data anonymization, and enable audit trails")); + } + + if (compliance.Contains("hipaa")) + { + security.Add(new("HIPAA Compliance", + "Implement healthcare data protection with encryption and access logging", + "Enable BAA agreements, configure dedicated compute, and implement comprehensive audit logging")); + } + + if (compliance.Contains("soc")) + { + security.Add(new("SOC2 Compliance", + "Implement security controls for availability, confidentiality, and processing integrity", + "Configure security monitoring, access controls, and change management processes")); + } + } + + return security; + } + + private async Task> GenerateCostOptimizations(List components) + { + await Task.Delay(50); // Simulate processing time + + var optimizations = new List + { + new("Compute Resources", + "Use Azure Reserved Instances for predictable workloads and Azure Spot VMs for fault-tolerant workloads", + "Up to 72% savings on compute costs"), + new("Storage", + "Implement Azure Blob Storage lifecycle management to automatically move data to cooler tiers", + "Up to 50% savings on storage costs"), + new("Monitoring", + "Configure appropriate data retention policies and sampling rates for Application Insights", + "Up to 30% savings on monitoring costs"), + new("Database", + "Use serverless compute tier for databases with unpredictable usage patterns", + "Pay only for actual usage, potential 25-40% savings"), + new("Auto-scaling", + "Implement auto-scaling policies to automatically scale down during low-usage periods", + "Average 20-35% savings on compute costs") + }; + + return optimizations; + } + + private async Task GenerateOverallArchitecture(string requirements, string? workloadType, List components) + { + await Task.Delay(50); // Simulate processing time + + var architecture = $"Based on the requirements for a {workloadType ?? "general-purpose"} solution, " + + "the recommended architecture follows cloud-native principles with emphasis on scalability, security, and maintainability. " + + $"The architecture consists of {components.Count} main components working together to deliver the required functionality. " + + "The design prioritizes managed services to reduce operational overhead while maintaining flexibility for future enhancements. " + + "All components are designed with high availability and disaster recovery in mind, utilizing Azure's global infrastructure."; + + return architecture; + } + + private async Task GenerateDeploymentConsiderations(List components) + { + await Task.Delay(50); // Simulate processing time + + return "Deploy using Infrastructure as Code (IaC) with Azure Resource Manager templates or Bicep. " + + "Implement a CI/CD pipeline using Azure DevOps or GitHub Actions for automated deployments. " + + "Use deployment slots for web applications to enable zero-downtime deployments. " + + "Configure environment-specific parameters and secrets management. " + + "Implement proper testing stages including unit tests, integration tests, and load testing before production deployment."; + } + + private async Task GenerateMonitoringStrategy(List components) + { + await Task.Delay(50); // Simulate processing time + + return "Implement comprehensive monitoring using Azure Monitor and Application Insights for application performance monitoring. " + + "Configure custom dashboards and alerts for key performance indicators and business metrics. " + + "Use Azure Log Analytics for centralized log management and correlation. " + + "Implement distributed tracing for microservices architectures. " + + "Set up automated anomaly detection and proactive alerting for critical system metrics."; + } + + private async Task> GenerateNextSteps(List components) + { + await Task.Delay(50); // Simulate processing time + + return new List + { + "Create a detailed technical specification document based on this architecture design", + "Develop Infrastructure as Code (IaC) templates for automated deployment", + "Set up development and staging environments for testing", + "Implement a proof of concept for the most critical components", + "Conduct a detailed cost analysis and optimization review", + "Plan the migration strategy if moving from existing systems", + "Establish monitoring and alerting baselines", + "Create operational runbooks and disaster recovery procedures", + "Schedule architecture review sessions with stakeholders", + "Plan for security testing and compliance validation" + }; + } +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs new file mode 100644 index 000000000..baccae5a8 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Models; +using AzureMcp.Core.Options; + +namespace AzureMcp.CloudArchitect.Services; + +public interface ICloudArchitectService +{ + Task GenerateArchitectureDesign( + string requirements, + string? workloadType = null, + string? scaleRequirements = null, + string? complianceRequirements = null, + RetryPolicyOptions? retryPolicy = null); +} diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/AzureMcp.CloudArchitect.UnitTests.csproj b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/AzureMcp.CloudArchitect.UnitTests.csproj new file mode 100644 index 000000000..f6ad6a956 --- /dev/null +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/AzureMcp.CloudArchitect.UnitTests.csproj @@ -0,0 +1,19 @@ + + + true + false + true + + + + + + + + + + + + + + diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs new file mode 100644 index 000000000..3bbc2e85e --- /dev/null +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Commands.Design; +using AzureMcp.Core.Options; + +namespace AzureMcp.CloudArchitect.UnitTests.Design; + +public class DesignCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly ICloudArchitectService _service; + private readonly ILogger _logger; + private readonly DesignCommand _command; + private readonly CommandContext _context; + private readonly Parser _parser; + + public DesignCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_service); + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _parser = new(_command.GetCommand()); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("design", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + Assert.Contains("Generate a comprehensive Azure cloud architecture design", command.Description); + } + + [Theory] + [InlineData("--requirements \"E-commerce platform with user authentication\"", true)] + [InlineData("--requirements \"Data processing pipeline\" --workload-type \"data-processing\"", true)] + [InlineData("--requirements \"Healthcare portal\" --compliance-requirements \"HIPAA\"", true)] + [InlineData("", false)] + [InlineData("--workload-type \"web-application\"", false)] // Missing required requirements + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + var mockDesign = new ArchitectureDesign( + "Test architecture description", + new List + { + new("Test Component", "Azure App Service", "Test reasoning", "Test config", new List { "Alternative" }) + }, + new List + { + new("Test Area", "Test recommendation", "Test implementation") + }, + new List + { + new("Test Service", "Test optimization", "Test savings") + }, + "Test deployment considerations", + "Test monitoring strategy", + new List { "Test next step" } + ); + + _service.GenerateArchitectureDesign( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockDesign); + } + + var parseResult = _parser.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(shouldSucceed ? 200 : 400, response.Status); + if (shouldSucceed) + { + Assert.NotNull(response.Results); + Assert.Equal("Success", response.Message); + + // Verify service was called + await _service.Received(1).GenerateArchitectureDesign( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + else + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + _service.GenerateArchitectureDesign( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException(new InvalidOperationException("Test error"))); + + var parseResult = _parser.Parse(["--requirements", "test requirements"]); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(422, response.Status); + Assert.Contains("Unable to generate architecture design", response.Message); + Assert.Contains("troubleshooting", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesArgumentExceptions() + { + // Arrange + _service.GenerateArchitectureDesign( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException(new ArgumentException("Invalid requirements"))); + + var parseResult = _parser.Parse(["--requirements", "invalid requirements"]); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Invalid requirements provided", response.Message); + Assert.Contains("troubleshooting", response.Message); + } + + [Theory] + [InlineData("--requirements \"E-commerce platform\" --workload-type \"web-application\"")] + [InlineData("--requirements \"Data pipeline\" --scale-requirements \"1M records/day\"")] + [InlineData("--requirements \"Healthcare app\" --compliance-requirements \"HIPAA\"")] + public async Task ExecuteAsync_PassesCorrectParametersToService(string args) + { + // Arrange + var mockDesign = new ArchitectureDesign( + "Test architecture", + new List(), + new List(), + new List(), + "Test deployment", + "Test monitoring", + new List() + ); + + _service.GenerateArchitectureDesign( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockDesign); + + var parseResult = _parser.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + + // Verify the service was called with correct parameters + await _service.Received(1).GenerateArchitectureDesign( + Arg.Is(r => !string.IsNullOrEmpty(r)), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } +} diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs new file mode 100644 index 000000000..3aee2958a --- /dev/null +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.CommandLine; +global using System.CommandLine.Parsing; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using NSubstitute; +global using Xunit; +global using AzureMcp.Core.Models; +global using AzureMcp.Core.Models.Command; +global using AzureMcp.CloudArchitect.Models; +global using AzureMcp.CloudArchitect.Services; diff --git a/core/src/AzureMcp.Cli/Program.cs b/core/src/AzureMcp.Cli/Program.cs index 9225975f0..60223e83d 100644 --- a/core/src/AzureMcp.Cli/Program.cs +++ b/core/src/AzureMcp.Cli/Program.cs @@ -66,6 +66,7 @@ private static IAreaSetup[] RegisterAreas() new AzureMcp.AppConfig.AppConfigSetup(), new AzureMcp.Authorization.AuthorizationSetup(), new AzureMcp.AzureIsv.AzureIsvSetup(), + new AzureMcp.CloudArchitect.CloudArchitectSetup(), new AzureMcp.Cosmos.CosmosSetup(), new AzureMcp.Foundry.FoundrySetup(), new AzureMcp.Grafana.GrafanaSetup(), From abdb78bdcdaa45d2531709b2cb511460f68cfc64 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 30 Jul 2025 09:36:08 -0500 Subject: [PATCH 02/29] updates --- .../Commands/CloudArchitectJsonContext.cs | 13 ++ .../Commands/Design/DesignCommand.cs | 115 ++++-------------- .../ArchitectureDesignConfidenceFactors.cs | 21 ++++ .../Options/ArchitectureDesignRequirement.cs | 27 ++++ .../Options/ArchitectureDesignRequirements.cs | 21 ++++ .../Options/ArchitectureDesignTiers.cs | 15 +++ .../Options/ArchitectureDesignToolOptions.cs | 39 ++++++ .../Options/ArchitectureDesignToolState.cs | 30 +++++ .../Options/ArchitectureTier.cs | 20 +++ .../Options/Design/DesignOptions.cs | 16 +-- .../Options/RequirementImportance.cs | 17 +++ .../Resources/azure-architecture-design.tx | 73 +++++++++++ 12 files changed, 307 insertions(+), 100 deletions(-) create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs new file mode 100644 index 000000000..cd7d754fe --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Commands; + +[JsonSerializable(typeof(List))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +internal partial class AzureCloudArchitectJsonContext : JsonSerializerContext +{ + +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index a1bdac667..e092ae36d 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -7,47 +7,28 @@ using AzureMcp.Core.Commands; using AzureMcp.Core.Models; using Microsoft.Extensions.Logging; +using System.CommandLine; +using System.CommandLine.Parsing; namespace AzureMcp.CloudArchitect.Commands.Design; public sealed class DesignCommand(ILogger logger) : BaseCloudArchitectCommand { - private const string CommandTitle = "Generate Cloud Architecture Design"; + private const string CommandTitle = "Design Azure cloud architectures through guided questions"; private readonly ILogger _logger = logger; + private readonly CloudArchitectService _cloudArchitectService = cloudArchitectService; // Define options from OptionDefinitions - private readonly Option _requirementsOption = CloudArchitectOptionDefinitions.RequirementsOption; - private readonly Option _workloadTypeOption = CloudArchitectOptionDefinitions.WorkloadTypeOption; - private readonly Option _scaleRequirementsOption = CloudArchitectOptionDefinitions.ScaleRequirementsOption; - private readonly Option _complianceRequirementsOption = CloudArchitectOptionDefinitions.ComplianceRequirementsOption; + private readonly Option _architectureDesignToolOptions = CloudArchitectOptionDefinitions.ArchitectureDesignToolOptions; + + private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); + + private static string GetArchitectureDesignText() => s_designArchitectureText; public override string Name => "design"; public override string Description => - """ - Generate a comprehensive Azure cloud architecture design based on your requirements. - This command analyzes your application requirements and provides detailed recommendations for: - - Azure service selection and configuration - - Security and compliance considerations - - Cost optimization strategies - - Deployment and monitoring approaches - - Implementation next steps - - Returns a structured architecture design with components, security considerations, and cost optimizations. - - Required options: - - --requirements: Detailed description of your application or system requirements - - Optional options: - - --workload-type: Type of workload (web-application, api-backend, data-processing, microservices, iot, machine-learning) - - --scale-requirements: Expected scale and performance requirements - - --compliance-requirements: Compliance and security requirements (GDPR, HIPAA, SOC2, etc.) - - Examples: - azmcp cloudarchitect design --requirements "E-commerce platform with user authentication, product catalog, and payment processing" - azmcp cloudarchitect design --requirements "Real-time data processing pipeline for IoT sensors" --workload-type "data-processing" --scale-requirements "1M events per day, global distribution" - azmcp cloudarchitect design --requirements "Healthcare patient portal" --compliance-requirements "HIPAA" --scale-requirements "10K concurrent users" - """; + "A tool for designing Azure cloud architectures through guided questions."; public override string Title => CommandTitle; @@ -57,79 +38,35 @@ azmcp cloudarchitect design --requirements "Healthcare patient portal" --complia ReadOnly = true }; + private static string LoadArchitectureDesignText() + { + Assembly assembly = typeof(DesignCommand).Assembly; + string resourceName = EmbeddedResourceHelper.FindEmbeddedResource(assembly, "azure-architecture-design.txt"); + return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); + } + + protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.AddOption(_requirementsOption); - command.AddOption(_workloadTypeOption); - command.AddOption(_scaleRequirementsOption); - command.AddOption(_complianceRequirementsOption); + command.AddOption(_architectureDesignToolOptions); } protected override DesignOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.Requirements = parseResult.GetValueForOption(_requirementsOption); - options.WorkloadType = parseResult.GetValueForOption(_workloadTypeOption); - options.ScaleRequirements = parseResult.GetValueForOption(_scaleRequirementsOption); - options.ComplianceRequirements = parseResult.GetValueForOption(_complianceRequirementsOption); + options.ArchitectDesignTool = parseResult.GetValueForOption(_architectureDesignToolOptions); return options; } - public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) + public override Task ExecuteAsync(CommandContext context, ParseResult parseResult) { - var options = BindOptions(parseResult); - - try - { - // Required validation step - if (!Validate(parseResult.CommandResult, context.Response).IsValid) - { - return context.Response; - } - - // Get the cloud architect service from DI - var service = context.GetService(); - - // Generate architecture design - var design = await service.GenerateArchitectureDesign( - options.Requirements!, - options.WorkloadType, - options.ScaleRequirements, - options.ComplianceRequirements, - options.RetryPolicy); - - // Set results - context.Response.Results = ResponseResult.Create( - new DesignCommandResult(design), - CloudArchitectJsonContext.Default.DesignCommandResult); - } - catch (Exception ex) - { - // Log error with all relevant context - _logger.LogError(ex, - "Error generating architecture design. Requirements: {Requirements}, WorkloadType: {WorkloadType}, Options: {@Options}", - options.Requirements, options.WorkloadType, options); - HandleException(context, ex); - } - - return context.Response; + var designArchitecture = GetArchitectureDesignText(); + context.Response.Status = 200; + context.Response.Results = ResponseResult.Create(new List { designArchitecture }, CloudArchitectJsonContext.Default.ListString); + context.Response.Message = string.Empty; + return Task.FromResult(context.Response); } - - // Implementation-specific error handling - protected override string GetErrorMessage(Exception ex) => ex switch - { - ArgumentException argEx => $"Invalid requirements provided. {argEx.Message}", - InvalidOperationException opEx => $"Unable to generate architecture design. {opEx.Message}", - _ => base.GetErrorMessage(ex) - }; - - protected override int GetStatusCode(Exception ex) => ex switch - { - ArgumentException => 400, - InvalidOperationException => 422, - _ => base.GetStatusCode(ex) - }; } // Strongly-typed result record diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs new file mode 100644 index 000000000..e6fadacfa --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Confidence factors for the architecture design. +/// +public class ArchitectureDesignConfidenceFactors +{ + [JsonPropertyName("explicitRequirementsCoverage")] + public double ExplicitRequirementsCoverage { get; set; } + + [JsonPropertyName("implicitRequirementsCertainty")] + public double ImplicitRequirementsCertainty { get; set; } + + [JsonPropertyName("assumptionRisk")] + public double AssumptionRisk { get; set; } +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs new file mode 100644 index 000000000..6f7b807b7 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Represents a single architecture design requirement. +/// +public class ArchitectureDesignRequirement +{ + [JsonPropertyName("category")] + public string Category { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; + + [JsonPropertyName("importance")] + public RequirementImportance Importance { get; set; } + + [JsonPropertyName("confidence")] + public double Confidence { get; set; } +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs new file mode 100644 index 000000000..aaf5c24e4 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Contains all requirements for the architecture design. +/// +public class ArchitectureDesignRequirements +{ + [JsonPropertyName("explicit")] + public List Explicit { get; set; } = new(); + + [JsonPropertyName("implicit")] + public List Implicit { get; set; } = new(); + + [JsonPropertyName("assumed")] + public List Assumed { get; set; } = new(); +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs new file mode 100644 index 000000000..4e6223b7c --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Represents the different architecture tiers. +/// +public class ArchitectureDesignTiers +{ + [JsonPropertyName("additionalProperties")] + public List AdditionalProperties { get; set; } = new(); +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs new file mode 100644 index 000000000..6f76b21a5 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// The set of parameters that the architecture design tool takes as input. +/// +public class ArchitectureDesignToolOptions : BaseCloudArchitectOptions +{ + [JsonPropertyName("question")] + public string Question { get; set; } = string.Empty; + + [JsonPropertyName("questionNumber")] + public int QuestionNumber { get; set; } + + [JsonPropertyName("totalQuestions")] + public int TotalQuestions { get; set; } + + [JsonPropertyName("answer")] + public string? Answer { get; set; } + + [JsonPropertyName("nextQuestionNeeded")] + public bool NextQuestionNeeded { get; set; } + + [JsonPropertyName("confidenceScore")] + public double? ConfidenceScore { get; set; } + + [JsonPropertyName("architectureComponent")] + public string? ArchitectureComponent { get; set; } + + [JsonPropertyName("architectureTier")] + public ArchitectureTier? ArchitectureTier { get; set; } + + [JsonPropertyName("state")] + public ArchitectureDesignToolState State { get; set; } = new(); +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs new file mode 100644 index 000000000..0c63dc682 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// The state object for the architecture design tool. +/// +public class ArchitectureDesignToolState +{ + [JsonPropertyName("architectureComponents")] + public List ArchitectureComponents { get; set; } = new(); + + [JsonPropertyName("architectureTiers")] + public ArchitectureDesignTiers ArchitectureTiers { get; set; } = new(); + + [JsonPropertyName("thought")] + public string Thought { get; set; } = string.Empty; + + [JsonPropertyName("suggestedHint")] + public string SuggestedHint { get; set; } = string.Empty; + + [JsonPropertyName("requirements")] + public ArchitectureDesignRequirements Requirements { get; set; } = new(); + + [JsonPropertyName("confidenceFactors")] + public ArchitectureDesignConfidenceFactors ConfidenceFactors { get; set; } = new(); +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs new file mode 100644 index 000000000..484d395fa --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Represents the different architecture tiers. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ArchitectureTier +{ + Infrastructure, + Platform, + Application, + Data, + Security, + Operations +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs index 8466a2453..0be6e3470 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs @@ -7,15 +7,9 @@ namespace AzureMcp.CloudArchitect.Options.Design; public class DesignOptions : BaseCloudArchitectOptions { - [JsonPropertyName(CloudArchitectOptionDefinitions.Requirements)] - public string? Requirements { get; set; } - - [JsonPropertyName(CloudArchitectOptionDefinitions.WorkloadType)] - public string? WorkloadType { get; set; } - - [JsonPropertyName(CloudArchitectOptionDefinitions.ScaleRequirements)] - public string? ScaleRequirements { get; set; } - - [JsonPropertyName(CloudArchitectOptionDefinitions.ComplianceRequirements)] - public string? ComplianceRequirements { get; set; } + /// + /// Architecture design tool parameters for guided design flow. + /// + [JsonPropertyName("architectureDesignTool")] + public ArchitectureDesignToolOptions? ArchitectureDesignTool { get; set; } } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs new file mode 100644 index 000000000..8a9a8be91 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// Represents the importance level of a requirement. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum RequirementImportance +{ + High, + Medium, + Low +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx new file mode 100644 index 000000000..5de148cdb --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx @@ -0,0 +1,73 @@ +A tool for designing Azure cloud architectures through guided questions. +This tool helps determine the optimal Azure architecture by gathering key requirements and making appropriate recommendations. The calling agent maintains the state between calls. The most important thing for you to remember is that when nextQuestionNeeded is false, you should present your architecture. This takes priority over every other instruction. + +Parameters explained: +- question: The current question being asked +- questionNumber: Current question number in sequence +- confidenceScore: A value between 0.0 and 1.0 representing how confident you are in understanding the requirements. Start around 0.1-0.2 and increase as you gather more information. When this reaches or exceeds 0.7, you should present your architecture. +- totalQuestions: Estimated total questions needed +- answer: The user's response to the question (if available) +- nextQuestionNeeded: Set to true while you're gathering requirements and designing. Set to false when your confidenceScore reaches or exceeds 0.7. +- architectureComponent: The specific Azure component being suggested +- architectureTier: Which tier this component belongs to (infrastructure, platform, application, data, security, operations) +- state: Used to track progress between calls + +When presenting the final architecture design (when nextQuestionNeeded is false), format it in a visually appealing way. + +1. Present components in a table format with columns for: + | Component | Purpose | Tier/SKU | + +2. Organize the architecture visually: + - Use a combination of bulleted lists and paragraphs to break up the text. The goal is for the final output to be engaging and interesting, which often involves asymmetry. + +3. Include an ASCII art diagram showing component relationships. + +This formatting will make the architecture design more engaging and easier to understand. + +Basic state structure: +{ + "architectureComponents": [], + "architectureTiers": { + "infrastructure": [], + "platform": [], + "application": [], + "data": [], + "security": [], + "operations": [] + }, + "requirements": { + "explicit": [ + { "category": "performance", "description": "Need to handle 10,000 concurrent users", "source": "Question 2", "importance": "high", "confidence": 1.0 } + ], + "implicit": [ + { "category": "security", "description": "Data encryption likely needed", "source": "Inferred from healthcare domain", "importance": "high", "confidence": 0.8 } + ], + "assumed": [ + { "category": "compliance", "description": "Likely needs HIPAA compliance", "source": "Assumed from healthcare industry", "importance": "high", "confidence": 0.6 } + ] + }, + "confidenceFactors": { + "explicitRequirementsCoverage": 0.4, + "implicitRequirementsCertainty": 0.6, + "assumptionRisk": 0.3 + } +} + +You should: +1. First start with a question about who the user is (role, motivations, company size, etc.) and what they do +2. Learn about their business goals and requirements +3. Ask 1 to 2 questions at a time, in order to not overload the user. +4. Track your confidence level in understanding requirements using the confidenceScore parameter +5. After each user response, update the requirements in the state object: + - Add explicit requirements directly stated by the user + - Add implicit requirements you can reasonably infer + - Add assumed requirements where you lack information but need to make progress + - Update confidence factors based on the quality and completeness of requirements +6. Ask follow-up questions to clarify technical needs, especially to confirm assumed requirements +7. Identify specific requirements and technical constraints from user responses +8. Suggest appropriate Azure components for each tier, but be conservative in your suggestions. Don't suggest components that are not necessary for the architecture. +9. Ensure you cover all architecture tiers. +10. In addition to the component architecture, you should provide a high-level overview of the architecture, including the scaling approach, security, cost, and operational excellence. Provide actionable advice for the user to follow up on. Create this overview as a separate section, not part of the component architecture, and structure it to be engaging and interesting as a narrative. +11. Follow Azure Well-Architected Framework principles (reliability, security, cost, operational excellence, performance efficiency) +12. Keep track of components you've suggested using the state object +13. Calculate your overall confidence score from the three confidence factors in the state \ No newline at end of file From 5828a1e84e0de64db1158c87794b3f2d41c76d71 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 30 Jul 2025 17:34:50 -0500 Subject: [PATCH 03/29] updates --- .../Commands/Design/DesignCommand.cs | 34 ++++++-- .../CloudArchitectOptionDefinitions.cs | 80 +++++++++++++++---- .../Options/Design/DesignOptions.cs | 15 ---- 3 files changed, 89 insertions(+), 40 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index e092ae36d..6996eabe9 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -2,24 +2,30 @@ // Licensed under the MIT License. using AzureMcp.CloudArchitect.Options; -using AzureMcp.CloudArchitect.Options.Design; using AzureMcp.CloudArchitect.Services; using AzureMcp.Core.Commands; +using AzureMcp.Core.Helpers; using AzureMcp.Core.Models; using Microsoft.Extensions.Logging; using System.CommandLine; using System.CommandLine.Parsing; +using System.Reflection; namespace AzureMcp.CloudArchitect.Commands.Design; -public sealed class DesignCommand(ILogger logger) : BaseCloudArchitectCommand +public sealed class DesignCommand(ILogger logger) : BaseCloudArchitectCommand { private const string CommandTitle = "Design Azure cloud architectures through guided questions"; private readonly ILogger _logger = logger; - private readonly CloudArchitectService _cloudArchitectService = cloudArchitectService; - // Define options from OptionDefinitions - private readonly Option _architectureDesignToolOptions = CloudArchitectOptionDefinitions.ArchitectureDesignToolOptions; + private readonly Option _questionOption = CloudArchitectOptionDefinitions.Question; + private readonly Option _questionNumberOption = CloudArchitectOptionDefinitions.QuestionNumber; + private readonly Option _questionTotalQuestions = CloudArchitectOptionDefinitions.TotalQuestions; + private readonly Option _answerOption = CloudArchitectOptionDefinitions.Answer; + private readonly Option _nextQuestionNeededOption = CloudArchitectOptionDefinitions.NextQuestionNeeded; + private readonly Option _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore; + private readonly Option _architectureComponentOption = CloudArchitectOptionDefinitions.ArchitectureComponent; + private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); @@ -49,13 +55,25 @@ private static string LoadArchitectureDesignText() protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.AddOption(_architectureDesignToolOptions); + command.AddOption(_questionOption); + command.AddOption(_questionNumberOption); + command.AddOption(_questionTotalQuestions); + command.AddOption(_answerOption); + command.AddOption(_nextQuestionNeededOption); + command.AddOption(_confidenceScoreOption); + command.AddOption(_architectureComponentOption); } - protected override DesignOptions BindOptions(ParseResult parseResult) + protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.ArchitectDesignTool = parseResult.GetValueForOption(_architectureDesignToolOptions); + options.Question = parseResult.GetValueForOption(_questionOption); + options.QuestionNumber = parseResult.GetValueForOption(_questionNumberOption); + options.TotalQuestions = parseResult.GetValueForOption(_questionTotalQuestions); + options.Answer = parseResult.GetValueForOption(_answerOption); + options.NextQuestionNeeded = parseResult.GetValueForOption(_nextQuestionNeededOption); + options.ConfidenceScore = parseResult.GetValueForOption(_confidenceScoreOption); + options.ArchitectureComponent = parseResult.GetValueForOption(_architectureComponentOption); return options; } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs index 944b80ada..1cde3fee0 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs @@ -1,42 +1,88 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.CommandLine; + namespace AzureMcp.CloudArchitect.Options; public static class CloudArchitectOptionDefinitions { - public const string Requirements = "requirements"; - public const string WorkloadType = "workload-type"; - public const string ScaleRequirements = "scale-requirements"; - public const string ComplianceRequirements = "compliance-requirements"; + public const string QuestionName = "question"; + public const string QuestionNumberName = "question-number"; + public const string TotalQuestionsName = "total-questions"; + public const string AnswerName = "answer"; + public const string NextQuestionNeededName = "next-question-needed"; + public const string ConfidenceScoreName = "confidence-score"; + public const string ArchitectureComponentName = "architecture-component"; + public const string ArchitectureTierName = "architecture-tier"; + + public static readonly Option Question = new( + $"--{QuestionName}", + "The question to ask during the architecture design process." + ) + { + IsRequired = false + }; + + public static readonly Option QuestionNumber = new( + $"--{QuestionNumberName}", + "The current question number in the design process." + ) + { + IsRequired = false + }; - public static readonly Option RequirementsOption = new( - $"--{Requirements}", - "Detailed description of the application or system requirements, including functionality, expected users, and key features." + public static readonly Option TotalQuestions = new( + $"--{TotalQuestionsName}", + "The total number of questions in the design process." ) { - IsRequired = true + IsRequired = false + }; + + public static readonly Option Answer = new( + $"--{AnswerName}", + "The answer to the current question in the design process." + ) + { + IsRequired = false + }; + + public static readonly Option NextQuestionNeeded = new( + $"--{NextQuestionNeededName}", + "Whether the next question is needed in the design process." + ) + { + IsRequired = false + }; + + public static readonly Option ConfidenceScore = new( + $"--{ConfidenceScoreName}", + "The confidence score for the current architecture design." + ) + { + IsRequired = false }; - public static readonly Option WorkloadTypeOption = new( - $"--{WorkloadType}", - "The type of workload (e.g., web-application, data-processing, microservices, iot, machine-learning, api-backend)." + public static readonly Option ArchitectureComponent = new( + $"--{ArchitectureComponentName}", + "The architecture component being designed." ) { IsRequired = false }; - public static readonly Option ScaleRequirementsOption = new( - $"--{ScaleRequirements}", - "Expected scale and performance requirements (e.g., number of users, data volume, requests per second, geographic distribution)." + public static readonly Option ArchitectureTier = new( + $"--{ArchitectureTierName}", + "The architecture tier being designed (e.g., presentation, business, data)." ) { IsRequired = false }; - public static readonly Option ComplianceRequirementsOption = new( - $"--{ComplianceRequirements}", - "Compliance and security requirements (e.g., GDPR, HIPAA, SOC2, data residency requirements)." + public static readonly Option ArchitectureDesignTool = new( + "--architecture-design-tool", + "The complete architecture design tool options for guided design flow." ) { IsRequired = false diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs deleted file mode 100644 index 0be6e3470..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/Design/DesignOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace AzureMcp.CloudArchitect.Options.Design; - -public class DesignOptions : BaseCloudArchitectOptions -{ - /// - /// Architecture design tool parameters for guided design flow. - /// - [JsonPropertyName("architectureDesignTool")] - public ArchitectureDesignToolOptions? ArchitectureDesignTool { get; set; } -} From 2fbccb0e5e00f7532bbe3829a33cc3ef1394af31 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 30 Jul 2025 17:38:10 -0500 Subject: [PATCH 04/29] updates --- .../AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 6996eabe9..7c8361bdd 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -67,7 +67,7 @@ protected override void RegisterOptions(Command command) protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.Question = parseResult.GetValueForOption(_questionOption); + options.Question = parseResult.GetValueForOption(_questionOption) ?? string.Empty; options.QuestionNumber = parseResult.GetValueForOption(_questionNumberOption); options.TotalQuestions = parseResult.GetValueForOption(_questionTotalQuestions); options.Answer = parseResult.GetValueForOption(_answerOption); From 7520ccace0f6ec4b5ff9e4ba4d778ef346022014 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Mon, 4 Aug 2025 11:36:21 -0500 Subject: [PATCH 05/29] updates --- ...azure-architecture-design.tx => azure-architecture-design.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/{azure-architecture-design.tx => azure-architecture-design.txt} (100%) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.txt similarity index 100% rename from areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.tx rename to areas/cloudarchitect/src/AzureMcp.CloudArchitect/Resources/azure-architecture-design.txt From 05e15bdf9ddeb8b7778c699f1e24770af00e07e1 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 6 Aug 2025 13:57:24 -0500 Subject: [PATCH 06/29] update Tests --- .../Design/DesignCommandTests.cs | 234 +++++++++--------- 1 file changed, 111 insertions(+), 123 deletions(-) diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 3bbc2e85e..927b987b4 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -2,14 +2,23 @@ // Licensed under the MIT License. using AzureMcp.CloudArchitect.Commands.Design; -using AzureMcp.Core.Options; +using AzureMcp.CloudArchitect.Options; +using AzureMcp.CloudArchitect; +using AzureMcp.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Text; +using System.Text.Json; +using Xunit; namespace AzureMcp.CloudArchitect.UnitTests.Design; public class DesignCommandTests { private readonly IServiceProvider _serviceProvider; - private readonly ICloudArchitectService _service; private readonly ILogger _logger; private readonly DesignCommand _command; private readonly CommandContext _context; @@ -17,10 +26,9 @@ public class DesignCommandTests public DesignCommandTests() { - _service = Substitute.For(); _logger = Substitute.For>(); - var collection = new ServiceCollection().AddSingleton(_service); + var collection = new ServiceCollection(); _serviceProvider = collection.BuildServiceProvider(); _command = new(_logger); _context = new(_serviceProvider); @@ -34,159 +42,139 @@ public void Constructor_InitializesCommandCorrectly() Assert.Equal("design", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); - Assert.Contains("Generate a comprehensive Azure cloud architecture design", command.Description); + Assert.Contains("A tool for designing Azure cloud architectures through guided questions", command.Description); + } + + [Fact] + public void Command_HasCorrectOptions() + { + var command = _command.GetCommand(); + + // Check that the command has the expected options + var optionNames = command.Options.Select(o => o.Name).ToList(); + + Assert.Contains("question", optionNames); + Assert.Contains("question-number", optionNames); + Assert.Contains("total-questions", optionNames); + Assert.Contains("answer", optionNames); + Assert.Contains("next-question-needed", optionNames); + Assert.Contains("confidence-score", optionNames); + Assert.Contains("architecture-component", optionNames); } [Theory] - [InlineData("--requirements \"E-commerce platform with user authentication\"", true)] - [InlineData("--requirements \"Data processing pipeline\" --workload-type \"data-processing\"", true)] - [InlineData("--requirements \"Healthcare portal\" --compliance-requirements \"HIPAA\"", true)] - [InlineData("", false)] - [InlineData("--workload-type \"web-application\"", false)] // Missing required requirements - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + [InlineData("")] + [InlineData("--question \"What is your application type?\"")] + [InlineData("--question-number 1")] + [InlineData("--total-questions 5")] + [InlineData("--answer \"Web application\"")] + [InlineData("--next-question-needed true")] + [InlineData("--confidence-score 0.8")] + [InlineData("--architecture-component \"Frontend\"")] + [InlineData("--question \"App type?\" --question-number 1 --total-questions 5")] + public async Task ExecuteAsync_ReturnsArchitectureDesignText(string args) { // Arrange - if (shouldSucceed) - { - var mockDesign = new ArchitectureDesign( - "Test architecture description", - new List - { - new("Test Component", "Azure App Service", "Test reasoning", "Test config", new List { "Alternative" }) - }, - new List - { - new("Test Area", "Test recommendation", "Test implementation") - }, - new List - { - new("Test Service", "Test optimization", "Test savings") - }, - "Test deployment considerations", - "Test monitoring strategy", - new List { "Test next step" } - ); - - _service.GenerateArchitectureDesign( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(mockDesign); - } - var parseResult = _parser.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries)); // Act var response = await _command.ExecuteAsync(_context, parseResult); // Assert - Assert.Equal(shouldSucceed ? 200 : 400, response.Status); - if (shouldSucceed) - { - Assert.NotNull(response.Results); - Assert.Equal("Success", response.Message); - - // Verify service was called - await _service.Received(1).GenerateArchitectureDesign( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); - } - else - { - Assert.Contains("required", response.Message.ToLower()); - } + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify that results contain the architecture design text by serializing it + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + response.Results.Write(writer); + writer.Flush(); + + var serializedResult = Encoding.UTF8.GetString(stream.ToArray()); + var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); + + Assert.NotNull(resultList); + Assert.Single(resultList); + Assert.NotEmpty(resultList[0]); + + // Verify it contains some expected architecture-related content + var architectureText = resultList[0]; + Assert.Contains("architecture", architectureText.ToLower()); } [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() + public async Task ExecuteAsync_ConsistentResults() { // Arrange - _service.GenerateArchitectureDesign( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(Task.FromException(new InvalidOperationException("Test error"))); - - var parseResult = _parser.Parse(["--requirements", "test requirements"]); + var parseResult1 = _parser.Parse(["--question", "test question 1"]); + var parseResult2 = _parser.Parse(["--question", "test question 2"]); // Act - var response = await _command.ExecuteAsync(_context, parseResult); + var response1 = await _command.ExecuteAsync(_context, parseResult1); + var response2 = await _command.ExecuteAsync(_context, parseResult2); - // Assert - Assert.Equal(422, response.Status); - Assert.Contains("Unable to generate architecture design", response.Message); - Assert.Contains("troubleshooting", response.Message); + // Assert - Both calls should return the same architecture design text + Assert.Equal(200, response1.Status); + Assert.Equal(200, response2.Status); + + // Serialize both results to compare them + string serializedResult1 = SerializeResponseResult(response1.Results!); + string serializedResult2 = SerializeResponseResult(response2.Results!); + + Assert.Equal(serializedResult1, serializedResult2); } [Fact] - public async Task ExecuteAsync_HandlesArgumentExceptions() + public async Task ExecuteAsync_WithAllOptionsSet() { // Arrange - _service.GenerateArchitectureDesign( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(Task.FromException(new ArgumentException("Invalid requirements"))); + var args = new[] + { + "--question", "What is your application type?", + "--question-number", "1", + "--total-questions", "5", + "--answer", "Web application", + "--next-question-needed", "true", + "--confidence-score", "0.8", + "--architecture-component", "Frontend" + }; - var parseResult = _parser.Parse(["--requirements", "invalid requirements"]); + var parseResult = _parser.Parse(args); // Act var response = await _command.ExecuteAsync(_context, parseResult); // Assert - Assert.Equal(400, response.Status); - Assert.Contains("Invalid requirements provided", response.Message); - Assert.Contains("troubleshooting", response.Message); + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify the command executed successfully regardless of the input options + string serializedResult = SerializeResponseResult(response.Results); + var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); + Assert.NotNull(resultList); + Assert.Single(resultList); + Assert.NotEmpty(resultList[0]); } - [Theory] - [InlineData("--requirements \"E-commerce platform\" --workload-type \"web-application\"")] - [InlineData("--requirements \"Data pipeline\" --scale-requirements \"1M records/day\"")] - [InlineData("--requirements \"Healthcare app\" --compliance-requirements \"HIPAA\"")] - public async Task ExecuteAsync_PassesCorrectParametersToService(string args) + [Fact] + public void Metadata_IsConfiguredCorrectly() { - // Arrange - var mockDesign = new ArchitectureDesign( - "Test architecture", - new List(), - new List(), - new List(), - "Test deployment", - "Test monitoring", - new List() - ); - - _service.GenerateArchitectureDesign( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(mockDesign); - - var parseResult = _parser.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries)); - - // Act - var response = await _command.ExecuteAsync(_context, parseResult); + // Arrange & Act + var metadata = _command.Metadata; // Assert - Assert.Equal(200, response.Status); - - // Verify the service was called with correct parameters - await _service.Received(1).GenerateArchitectureDesign( - Arg.Is(r => !string.IsNullOrEmpty(r)), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Assert.False(metadata.Destructive); + Assert.True(metadata.ReadOnly); + } + + private static string SerializeResponseResult(ResponseResult responseResult) + { + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + responseResult.Write(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); } } From 629ece775340562e44a725ab6f91d65e7234f6ac Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 6 Aug 2025 21:01:10 -0500 Subject: [PATCH 07/29] update documentation --- README.md | 4 ++++ docs/azmcp-commands.md | 21 +++++++++++++++++++++ e2eTests/e2eTestPrompts.md | 6 ++++++ 3 files changed, 31 insertions(+) diff --git a/README.md b/README.md index 3ffe40bae..d8d16a477 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,10 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some * Get the Bicep schema for specific Azure resource types +### 🏗️ Cloud Architect + +* Design Azure cloud architectures through guided questions + Agents and models can discover and learn best practices and usage guidelines for the `azd` MCP tool. For more information, see [AZD Best Practices](https://github.com/Azure/azure-mcp/tree/main/areas/extension/src/AzureMcp.Extension/Resources/azd-best-practices.txt). diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index d2b3c420b..3c50452d2 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -844,6 +844,26 @@ azmcp workbooks update --workbook-id \ azmcp bicepschema get --resource-type \ ``` +### Cloud Architect + +```bash +# Design Azure cloud architectures through guided questions +azmcp cloudarchitect design [--question ] \ + [--question-number ] \ + [--total-questions ] \ + [--answer ] \ + [--next-question-needed ] \ + [--confidence-score ] \ + [--architecture-component ] + +# Example: +# Start an interactive architecture design session +azmcp cloudarchitect design --question "What type of application are you building?" \ + --question-number 1 \ + --total-questions 5 \ + --confidence-score 0.1 +``` + ## Response Format All responses follow a consistent JSON format: @@ -864,3 +884,4 @@ The CLI returns structured JSON responses for errors, including: - Service availability issues - Authentication errors + diff --git a/e2eTests/e2eTestPrompts.md b/e2eTests/e2eTestPrompts.md index 30a52a3ce..b8edb84b5 100644 --- a/e2eTests/e2eTestPrompts.md +++ b/e2eTests/e2eTestPrompts.md @@ -345,3 +345,9 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | Tool Name | Test Prompt | |:----------|:----------| | azmcp-bicepschema-get | How can I use Bicep to create an Azure OpenAI service? | + +## Cloud Architect + +| Tool Name | Test Prompt | +|:----------|:----------| +| azmcp-cloudarchitect | Please help me design an architecture for a large-scale file upload, storage, and retrieval service | From 900bdeabff7cb0f4e642e6f7dc3f86bcfc321ec1 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 6 Aug 2025 21:35:10 -0500 Subject: [PATCH 08/29] update cspell --- .vscode/cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 9eef8e810..dde5143ad 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -196,6 +196,7 @@ "bestpractices", "bicepschema", "breathability", + "cloudarchitecture", "codesign", "CODEOWNERS", "containerapps", From 951765ec333f0b75d93fba17b56b8cb630a804e9 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 6 Aug 2025 21:38:55 -0500 Subject: [PATCH 09/29] updates --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9217345..f684c9b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The Azure MCP Server updates automatically by default whenever a new release com - Read `AZURE_SUBSCRIPTION_ID` from the environment if the subscription is not provided. [[#533](https://github.com/Azure/azure-mcp/pull/533)] +- Added new command for designing Azure Cloud Architecture through guided questions. [[#890]https://github.com/Azure/azure-mcp/pull/890] + ### Breaking Changes ### Bugs Fixed From d8331a235e6bd619c732ccebcf7c6f55b9a822a0 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Thu, 7 Aug 2025 13:23:58 -0500 Subject: [PATCH 10/29] added more prompt examples --- .../AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs | 1 - e2eTests/e2eTestPrompts.md | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 7c8361bdd..cd68ae335 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -51,7 +51,6 @@ private static string LoadArchitectureDesignText() return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); } - protected override void RegisterOptions(Command command) { base.RegisterOptions(command); diff --git a/e2eTests/e2eTestPrompts.md b/e2eTests/e2eTestPrompts.md index d86417d68..9903d921d 100644 --- a/e2eTests/e2eTestPrompts.md +++ b/e2eTests/e2eTestPrompts.md @@ -338,4 +338,6 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | Tool Name | Test Prompt | |:----------|:----------| -| azmcp-cloudarchitect | Please help me design an architecture for a large-scale file upload, storage, and retrieval service | +| azmcp-cloudarchitect-design | Please help me design an architecture for a large-scale file upload, storage, and retrieval service | +| azmcp-cloudarchitect-design | Help me create a cloud service that will serve as ATM for users | +| azmcp-cloudarchitect-design | I want to design a cloud app for ordering groceries | From f924482bce98ab6424933d442577ddc4c1ef0c5f Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Thu, 7 Aug 2025 17:51:19 -0500 Subject: [PATCH 11/29] removed unnecessary service classes --- .../CloudArchitectSetup.cs | 2 - .../Commands/Design/DesignCommand.cs | 1 - .../Services/CloudArchitectService.cs | 409 ------------------ .../Services/ICloudArchitectService.cs | 17 - .../GlobalUsings.cs | 1 - 5 files changed, 430 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs index e4af908e2..a436cef69 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs @@ -4,7 +4,6 @@ using AzureMcp.Core.Areas; using AzureMcp.Core.Commands; using AzureMcp.CloudArchitect.Commands.Design; -using AzureMcp.CloudArchitect.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -14,7 +13,6 @@ public class CloudArchitectSetup : IAreaSetup { public void ConfigureServices(IServiceCollection services) { - services.AddSingleton(); } public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index cd68ae335..e9e611ecf 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using AzureMcp.CloudArchitect.Options; -using AzureMcp.CloudArchitect.Services; using AzureMcp.Core.Commands; using AzureMcp.Core.Helpers; using AzureMcp.Core.Models; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs deleted file mode 100644 index 8fcfca756..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/CloudArchitectService.cs +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AzureMcp.CloudArchitect.Models; -using AzureMcp.Core.Options; -using Microsoft.Extensions.Logging; - -namespace AzureMcp.CloudArchitect.Services; - -public class CloudArchitectService(ILogger logger) : ICloudArchitectService -{ - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - public async Task GenerateArchitectureDesign( - string requirements, - string? workloadType = null, - string? scaleRequirements = null, - string? complianceRequirements = null, - RetryPolicyOptions? retryPolicy = null) - { - _logger.LogInformation("Generating architecture design for requirements: {Requirements}", requirements); - - try - { - // Analyze requirements and generate architecture recommendations - var components = await GenerateComponentRecommendations(requirements, workloadType, scaleRequirements); - var security = await GenerateSecurityConsiderations(requirements, complianceRequirements); - var costOptimizations = await GenerateCostOptimizations(components); - var overallArchitecture = await GenerateOverallArchitecture(requirements, workloadType, components); - var deploymentConsiderations = await GenerateDeploymentConsiderations(components); - var monitoringStrategy = await GenerateMonitoringStrategy(components); - var nextSteps = await GenerateNextSteps(components); - - return new ArchitectureDesign( - overallArchitecture, - components, - security, - costOptimizations, - deploymentConsiderations, - monitoringStrategy, - nextSteps - ); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating architecture design for requirements: {Requirements}", requirements); - throw; - } - } - - private async Task> GenerateComponentRecommendations( - string requirements, - string? workloadType, - string? scaleRequirements) - { - await Task.Delay(100); // Simulate processing time - - var components = new List(); - - // Determine workload type if not specified - var inferredWorkloadType = workloadType ?? InferWorkloadType(requirements); - - // Generate recommendations based on workload type and requirements - switch (inferredWorkloadType.ToLowerInvariant()) - { - case "web-application": - components.AddRange(GetWebApplicationComponents(requirements, scaleRequirements)); - break; - case "api-backend": - components.AddRange(GetApiBackendComponents(requirements, scaleRequirements)); - break; - case "data-processing": - components.AddRange(GetDataProcessingComponents(requirements, scaleRequirements)); - break; - case "microservices": - components.AddRange(GetMicroservicesComponents(requirements, scaleRequirements)); - break; - case "iot": - components.AddRange(GetIoTComponents(requirements, scaleRequirements)); - break; - case "machine-learning": - components.AddRange(GetMLComponents(requirements, scaleRequirements)); - break; - default: - components.AddRange(GetGeneralPurposeComponents(requirements, scaleRequirements)); - break; - } - - return components; - } - - private string InferWorkloadType(string requirements) - { - var lowerRequirements = requirements.ToLowerInvariant(); - - if (lowerRequirements.Contains("web") || lowerRequirements.Contains("frontend") || lowerRequirements.Contains("ui")) - return "web-application"; - if (lowerRequirements.Contains("api") || lowerRequirements.Contains("rest") || lowerRequirements.Contains("backend")) - return "api-backend"; - if (lowerRequirements.Contains("data") || lowerRequirements.Contains("etl") || lowerRequirements.Contains("processing")) - return "data-processing"; - if (lowerRequirements.Contains("microservice") || lowerRequirements.Contains("distributed")) - return "microservices"; - if (lowerRequirements.Contains("iot") || lowerRequirements.Contains("sensor") || lowerRequirements.Contains("device")) - return "iot"; - if (lowerRequirements.Contains("machine learning") || lowerRequirements.Contains("ai") || lowerRequirements.Contains("ml")) - return "machine-learning"; - - return "general-purpose"; - } - - private List GetWebApplicationComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("Frontend Hosting", "Azure Static Web Apps", - "Optimized for static content with global CDN distribution and built-in CI/CD", - "Configure custom domain, SSL certificates, and API integration", - new List { "Azure App Service", "Azure Storage Static Website", "Azure Front Door + Storage" }), - new("Backend API", "Azure App Service", - "Managed platform with built-in scaling, monitoring, and deployment slots", - "Use App Service Plan with auto-scaling rules based on CPU and memory", - new List { "Azure Container Apps", "Azure Functions", "Azure Kubernetes Service" }), - new("Database", "Azure SQL Database", - "Fully managed SQL database with automatic scaling and backup", - "Use General Purpose tier with automatic tuning enabled", - new List { "Azure Cosmos DB", "Azure Database for PostgreSQL", "Azure Database for MySQL" }), - new("Authentication", "Azure Active Directory B2C", - "Enterprise-grade identity management with social login integration", - "Configure user flows, custom policies, and API permissions", - new List { "Azure Active Directory", "Azure AD External Identities", "Third-party auth providers" }), - new("CDN & Caching", "Azure Front Door", - "Global load balancing with intelligent routing and caching", - "Configure caching rules, compression, and WAF protection", - new List { "Azure CDN", "Azure Application Gateway", "CloudFlare" }) - }; - } - - private List GetApiBackendComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("API Gateway", "Azure API Management", - "Centralized API management with security, throttling, and analytics", - "Configure rate limiting, authentication policies, and developer portal", - new List { "Azure Application Gateway", "Azure Front Door", "Open source solutions" }), - new("Compute", "Azure Container Apps", - "Serverless containers with automatic scaling and simplified management", - "Configure container resources, scaling rules, and health probes", - new List { "Azure App Service", "Azure Kubernetes Service", "Azure Functions" }), - new("Database", "Azure Cosmos DB", - "Multi-model database with global distribution for high-performance APIs", - "Use SQL API with consistent indexing and automatic scaling", - new List { "Azure SQL Database", "Azure Database for PostgreSQL", "Redis Cache" }), - new("Monitoring", "Azure Application Insights", - "Application performance monitoring with distributed tracing", - "Configure custom telemetry, alerts, and performance counters", - new List { "Azure Monitor", "Azure Log Analytics", "Third-party APM tools" }) - }; - } - - private List GetDataProcessingComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("Data Ingestion", "Azure Event Hubs", - "High-throughput data streaming platform for real-time ingestion", - "Configure partitioning, retention policies, and consumer groups", - new List { "Azure Service Bus", "Azure Storage Queue", "Apache Kafka on HDInsight" }), - new("Data Processing", "Azure Databricks", - "Apache Spark-based analytics platform with collaborative notebooks", - "Use cluster policies, auto-scaling, and delta lake for optimized performance", - new List { "Azure Synapse Analytics", "Azure HDInsight", "Azure Data Factory" }), - new("Data Storage", "Azure Data Lake Storage Gen2", - "Hierarchical storage optimized for big data analytics workloads", - "Enable hierarchical namespace and configure access tiers", - new List { "Azure Blob Storage", "Azure SQL Data Warehouse", "Azure Cosmos DB" }), - new("Orchestration", "Azure Data Factory", - "Cloud-based data integration service for ETL/ELT pipelines", - "Create pipelines with monitoring, alerting, and error handling", - new List { "Azure Logic Apps", "Azure Functions", "Apache Airflow" }) - }; - } - - private List GetMicroservicesComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("Container Orchestration", "Azure Kubernetes Service", - "Managed Kubernetes for container orchestration with enterprise security", - "Configure node pools, network policies, and Azure AD integration", - new List { "Azure Container Apps", "Azure Service Fabric", "Azure Container Instances" }), - new("Service Mesh", "Istio on AKS", - "Service mesh for traffic management, security, and observability", - "Configure ingress gateway, traffic policies, and distributed tracing", - new List { "Linkerd", "Consul Connect", "Azure Service Fabric Mesh" }), - new("API Gateway", "Azure API Management", - "Centralized gateway for microservices with rate limiting and security", - "Configure backend pools, transformation policies, and developer portal", - new List { "Kong", "Ambassador", "Istio Ingress Gateway" }), - new("Database per Service", "Azure Cosmos DB", - "Multi-model database supporting different data models per microservice", - "Use appropriate APIs (SQL, MongoDB, Cassandra) based on service needs", - new List { "Azure SQL Database", "Azure Database for PostgreSQL", "Azure Redis Cache" }) - }; - } - - private List GetIoTComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("IoT Platform", "Azure IoT Hub", - "Managed service for bi-directional communication with IoT devices", - "Configure device provisioning, routing rules, and security certificates", - new List { "Azure IoT Central", "Azure Event Hubs", "Third-party IoT platforms" }), - new("Device Management", "Azure IoT Device Provisioning Service", - "Zero-touch device provisioning and lifecycle management", - "Configure enrollment groups, attestation methods, and custom allocation policies", - new List { "Manual provisioning", "Azure IoT Central device management", "Third-party device management" }), - new("Stream Processing", "Azure Stream Analytics", - "Real-time analytics on streaming data from IoT devices", - "Create queries for filtering, aggregation, and anomaly detection", - new List { "Azure Functions", "Azure Event Hubs with custom processing", "Apache Storm" }), - new("Time Series Storage", "Azure Time Series Insights", - "Analytics service for time-series data with interactive exploration", - "Configure data retention, access policies, and reference data", - new List { "Azure Cosmos DB", "Azure Data Explorer", "InfluxDB" }) - }; - } - - private List GetMLComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("ML Platform", "Azure Machine Learning", - "End-to-end ML lifecycle management with automated ML and MLOps", - "Configure compute instances, data stores, and model deployment endpoints", - new List { "Azure Databricks", "Azure Cognitive Services", "Open source ML frameworks" }), - new("Data Storage", "Azure Data Lake Storage Gen2", - "Scalable storage for training data with hierarchical organization", - "Enable versioning, access control, and lifecycle management", - new List { "Azure Blob Storage", "Azure SQL Database", "Azure Cosmos DB" }), - new("Model Serving", "Azure Container Instances", - "Serverless containers for deploying ML models as web services", - "Configure auto-scaling, health probes, and A/B testing", - new List { "Azure Kubernetes Service", "Azure App Service", "Azure Functions" }), - new("Feature Store", "Azure ML Feature Store", - "Centralized repository for ML features with versioning and lineage", - "Configure feature sets, transformations, and access permissions", - new List { "Azure Cosmos DB", "Azure SQL Database", "Custom feature store solution" }) - }; - } - - private List GetGeneralPurposeComponents(string requirements, string? scaleRequirements) - { - return new List - { - new("Compute", "Azure App Service", - "Managed application hosting with built-in scaling and monitoring", - "Configure auto-scaling rules, deployment slots, and custom domains", - new List { "Azure Virtual Machines", "Azure Container Apps", "Azure Functions" }), - new("Database", "Azure SQL Database", - "Managed relational database with automatic tuning and backup", - "Use appropriate service tier based on performance requirements", - new List { "Azure Cosmos DB", "Azure Database for PostgreSQL", "Azure Database for MySQL" }), - new("Storage", "Azure Blob Storage", - "Object storage for unstructured data with multiple access tiers", - "Configure appropriate access tier and lifecycle management", - new List { "Azure Files", "Azure Data Lake Storage", "Azure Managed Disks" }), - new("Security", "Azure Key Vault", - "Centralized secrets management with HSM-backed key protection", - "Configure access policies, key rotation, and audit logging", - new List { "Azure Managed Identity", "Azure Active Directory", "Third-party secret managers" }) - }; - } - - private async Task> GenerateSecurityConsiderations(string requirements, string? complianceRequirements) - { - await Task.Delay(50); // Simulate processing time - - var security = new List - { - new("Identity & Access", - "Implement Azure Active Directory with multi-factor authentication and conditional access policies", - "Configure Azure AD, enable MFA, set up conditional access rules based on risk levels"), - new("Network Security", - "Use Azure Virtual Networks with Network Security Groups and Azure Firewall for network isolation", - "Configure VNets, NSGs, and firewall rules to restrict traffic flow between components"), - new("Data Encryption", - "Enable encryption at rest and in transit using Azure-managed keys with option for customer-managed keys", - "Configure TLS 1.2+ for all communications and enable transparent data encryption"), - new("Secrets Management", - "Store all secrets, keys, and certificates in Azure Key Vault with proper access controls", - "Use managed identities where possible and implement key rotation policies"), - new("Monitoring & Auditing", - "Implement comprehensive logging with Azure Monitor and enable Azure Security Center", - "Configure diagnostic settings, set up security alerts, and enable audit logging") - }; - - // Add compliance-specific considerations - if (!string.IsNullOrEmpty(complianceRequirements)) - { - var compliance = complianceRequirements.ToLowerInvariant(); - - if (compliance.Contains("gdpr")) - { - security.Add(new("GDPR Compliance", - "Implement data privacy controls with right to erasure and data portability", - "Configure data retention policies, implement data anonymization, and enable audit trails")); - } - - if (compliance.Contains("hipaa")) - { - security.Add(new("HIPAA Compliance", - "Implement healthcare data protection with encryption and access logging", - "Enable BAA agreements, configure dedicated compute, and implement comprehensive audit logging")); - } - - if (compliance.Contains("soc")) - { - security.Add(new("SOC2 Compliance", - "Implement security controls for availability, confidentiality, and processing integrity", - "Configure security monitoring, access controls, and change management processes")); - } - } - - return security; - } - - private async Task> GenerateCostOptimizations(List components) - { - await Task.Delay(50); // Simulate processing time - - var optimizations = new List - { - new("Compute Resources", - "Use Azure Reserved Instances for predictable workloads and Azure Spot VMs for fault-tolerant workloads", - "Up to 72% savings on compute costs"), - new("Storage", - "Implement Azure Blob Storage lifecycle management to automatically move data to cooler tiers", - "Up to 50% savings on storage costs"), - new("Monitoring", - "Configure appropriate data retention policies and sampling rates for Application Insights", - "Up to 30% savings on monitoring costs"), - new("Database", - "Use serverless compute tier for databases with unpredictable usage patterns", - "Pay only for actual usage, potential 25-40% savings"), - new("Auto-scaling", - "Implement auto-scaling policies to automatically scale down during low-usage periods", - "Average 20-35% savings on compute costs") - }; - - return optimizations; - } - - private async Task GenerateOverallArchitecture(string requirements, string? workloadType, List components) - { - await Task.Delay(50); // Simulate processing time - - var architecture = $"Based on the requirements for a {workloadType ?? "general-purpose"} solution, " + - "the recommended architecture follows cloud-native principles with emphasis on scalability, security, and maintainability. " + - $"The architecture consists of {components.Count} main components working together to deliver the required functionality. " + - "The design prioritizes managed services to reduce operational overhead while maintaining flexibility for future enhancements. " + - "All components are designed with high availability and disaster recovery in mind, utilizing Azure's global infrastructure."; - - return architecture; - } - - private async Task GenerateDeploymentConsiderations(List components) - { - await Task.Delay(50); // Simulate processing time - - return "Deploy using Infrastructure as Code (IaC) with Azure Resource Manager templates or Bicep. " + - "Implement a CI/CD pipeline using Azure DevOps or GitHub Actions for automated deployments. " + - "Use deployment slots for web applications to enable zero-downtime deployments. " + - "Configure environment-specific parameters and secrets management. " + - "Implement proper testing stages including unit tests, integration tests, and load testing before production deployment."; - } - - private async Task GenerateMonitoringStrategy(List components) - { - await Task.Delay(50); // Simulate processing time - - return "Implement comprehensive monitoring using Azure Monitor and Application Insights for application performance monitoring. " + - "Configure custom dashboards and alerts for key performance indicators and business metrics. " + - "Use Azure Log Analytics for centralized log management and correlation. " + - "Implement distributed tracing for microservices architectures. " + - "Set up automated anomaly detection and proactive alerting for critical system metrics."; - } - - private async Task> GenerateNextSteps(List components) - { - await Task.Delay(50); // Simulate processing time - - return new List - { - "Create a detailed technical specification document based on this architecture design", - "Develop Infrastructure as Code (IaC) templates for automated deployment", - "Set up development and staging environments for testing", - "Implement a proof of concept for the most critical components", - "Conduct a detailed cost analysis and optimization review", - "Plan the migration strategy if moving from existing systems", - "Establish monitoring and alerting baselines", - "Create operational runbooks and disaster recovery procedures", - "Schedule architecture review sessions with stakeholders", - "Plan for security testing and compliance validation" - }; - } -} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs deleted file mode 100644 index baccae5a8..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Services/ICloudArchitectService.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AzureMcp.CloudArchitect.Models; -using AzureMcp.Core.Options; - -namespace AzureMcp.CloudArchitect.Services; - -public interface ICloudArchitectService -{ - Task GenerateArchitectureDesign( - string requirements, - string? workloadType = null, - string? scaleRequirements = null, - string? complianceRequirements = null, - RetryPolicyOptions? retryPolicy = null); -} diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs index 3aee2958a..00ac56de2 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs @@ -10,4 +10,3 @@ global using AzureMcp.Core.Models; global using AzureMcp.Core.Models.Command; global using AzureMcp.CloudArchitect.Models; -global using AzureMcp.CloudArchitect.Services; From 986e3aabb1403e851619366fad2ed009a0527426 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Fri, 8 Aug 2025 15:20:53 -0500 Subject: [PATCH 12/29] fixed formatting, added unit tests for proper escape handling --- .../CloudArchitectSetup.cs | 2 +- .../Commands/BaseCloudArchitectCommand.cs | 2 +- .../Commands/Design/DesignCommand.cs | 6 +- .../Design/DesignCommandTests.cs | 80 +++++++++++++++++-- .../GlobalUsings.cs | 6 +- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs index a436cef69..25aa22760 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectSetup.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using AzureMcp.CloudArchitect.Commands.Design; using AzureMcp.Core.Areas; using AzureMcp.Core.Commands; -using AzureMcp.CloudArchitect.Commands.Design; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs index 5ba9958e5..d696ca71a 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs @@ -2,8 +2,8 @@ // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; -using AzureMcp.Core.Commands; using AzureMcp.CloudArchitect.Options; +using AzureMcp.Core.Commands; namespace AzureMcp.CloudArchitect.Commands; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index e9e611ecf..2aacb3b90 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Reflection; using AzureMcp.CloudArchitect.Options; using AzureMcp.Core.Commands; using AzureMcp.Core.Helpers; using AzureMcp.Core.Models; using Microsoft.Extensions.Logging; -using System.CommandLine; -using System.CommandLine.Parsing; -using System.Reflection; namespace AzureMcp.CloudArchitect.Commands.Design; diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 927b987b4..98b706528 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Text; +using System.Text.Json; +using AzureMcp.CloudArchitect; using AzureMcp.CloudArchitect.Commands.Design; using AzureMcp.CloudArchitect.Options; -using AzureMcp.CloudArchitect; using AzureMcp.Core.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NSubstitute; -using System.CommandLine; -using System.CommandLine.Parsing; -using System.Text; -using System.Text.Json; using Xunit; namespace AzureMcp.CloudArchitect.UnitTests.Design; @@ -158,6 +158,76 @@ public async Task ExecuteAsync_WithAllOptionsSet() Assert.NotEmpty(resultList[0]); } + [Theory] + [InlineData("What's your app type?", "What's your app type?")] + [InlineData("How \"big\" is your app?", "How \"big\" is your app?")] + [InlineData("Is it a \"web app\" or \"mobile app\"?", "Is it a \"web app\" or \"mobile app\"?")] + [InlineData("What's the app's \"main purpose\"?", "What's the app's \"main purpose\"?")] + [InlineData("Use 'single quotes' here", "Use 'single quotes' here")] + [InlineData("Mixed \"quotes\" and 'apostrophes'", "Mixed \"quotes\" and 'apostrophes'")] + public async Task ExecuteAsync_HandlesQuotesAndEscapingProperly(string questionWithQuotes, string expectedQuestion) + { + // Arrange + var args = new[] { "--question", questionWithQuotes }; + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify that the command executed successfully with the quoted input + string serializedResult = SerializeResponseResult(response.Results); + var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); + Assert.NotNull(resultList); + Assert.Single(resultList); + Assert.NotEmpty(resultList[0]); + + // Verify the question was parsed correctly by checking if the command can access the option value + var questionOption = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "question")); + Assert.Equal(expectedQuestion, questionOption); + } + + [Fact] + public async Task ExecuteAsync_HandlesComplexEscapingScenarios() + { + // Arrange - Test multiple options with various escaping scenarios + var complexQuestion = "What is your \"primary\" application 'type' and how \"big\" will it be?"; + var complexAnswer = "It's a \"web application\" with 'high' scalability requirements"; + var complexComponent = "Frontend with \"React\" and 'TypeScript'"; + + var args = new[] + { + "--question", complexQuestion, + "--answer", complexAnswer, + "--architecture-component", complexComponent, + "--question-number", "2", + "--total-questions", "10" + }; + + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify all options were parsed correctly + var questionValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "question")); + var answerValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "answer")); + var componentValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "architecture-component")); + + Assert.Equal(complexQuestion, questionValue); + Assert.Equal(complexAnswer, answerValue); + Assert.Equal(complexComponent, componentValue); + } + [Fact] public void Metadata_IsConfiguredCorrectly() { diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs index 00ac56de2..43f0cfc38 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs @@ -3,10 +3,10 @@ global using System.CommandLine; global using System.CommandLine.Parsing; +global using AzureMcp.CloudArchitect.Models; +global using AzureMcp.Core.Models; +global using AzureMcp.Core.Models.Command; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using NSubstitute; global using Xunit; -global using AzureMcp.Core.Models; -global using AzureMcp.Core.Models.Command; -global using AzureMcp.CloudArchitect.Models; From 9a9dc76972cd70ce67c920c9a2b707f0836ddb26 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Fri, 8 Aug 2025 16:09:49 -0500 Subject: [PATCH 13/29] update cspell value --- .vscode/cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index d40d18205..56fe2782c 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -206,7 +206,7 @@ "bicepschema", "breathability", "Byol", - "cloudarchitecture", + "cloudarchitect", "codesign", "CODEOWNERS", "containerapps", From 64912c64626d7dfec24c4c00fa110213eccabcf8 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Mon, 11 Aug 2025 11:37:10 -0500 Subject: [PATCH 14/29] update description --- .../AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 2aacb3b90..6ca97c251 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -33,7 +33,7 @@ public sealed class DesignCommand(ILogger logger) : BaseCloudArch public override string Name => "design"; public override string Description => - "A tool for designing Azure cloud architectures through guided questions."; + "Design and architect comprehensive Azure cloud solutions for applications and services. This interactive assistant helps create scalable cloud architectures for file upload systems, web applications, APIs, e-commerce platforms, financial services, transaction systems, data processing services, and enterprise solutions. Through guided questions, provides tailored Azure architecture recommendations covering storage, compute, networking, databases, security, and application services to create robust user-facing cloud services and applications."; public override string Title => CommandTitle; From 586f010a167def32dfb64691a821c1ff53216815 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Mon, 11 Aug 2025 12:32:21 -0500 Subject: [PATCH 15/29] updated unit test to use new description --- .../Design/DesignCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 98b706528..20b1d13ec 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -42,7 +42,7 @@ public void Constructor_InitializesCommandCorrectly() Assert.Equal("design", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); - Assert.Contains("A tool for designing Azure cloud architectures through guided questions", command.Description); + Assert.Contains("Design and architect comprehensive Azure cloud solutions for applications and services. This interactive assistant helps create scalable cloud architectures for file upload systems, web applications, APIs, e-commerce platforms, financial services, transaction systems, data processing services, and enterprise solutions. Through guided questions, provides tailored Azure architecture recommendations covering storage, compute, networking, databases, security, and application services to create robust user-facing cloud services and applications.", command.Description); } [Fact] From d9bb62ed5b860ea0ef8faa30d75ce8894799c63d Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Tue, 12 Aug 2025 11:45:01 -0500 Subject: [PATCH 16/29] add e2e prompt for cloud architect tool --- e2eTests/e2eTestPrompts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/e2eTests/e2eTestPrompts.md b/e2eTests/e2eTestPrompts.md index 3c04c438a..53db3d956 100644 --- a/e2eTests/e2eTestPrompts.md +++ b/e2eTests/e2eTestPrompts.md @@ -346,3 +346,4 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp-cloudarchitect-design | Please help me design an architecture for a large-scale file upload, storage, and retrieval service | | azmcp-cloudarchitect-design | Help me create a cloud service that will serve as ATM for users | | azmcp-cloudarchitect-design | I want to design a cloud app for ordering groceries | +| azmcp-cloudarchitect-design | How can I design a cloud service in Azure that will store and present videos for users? | From 1040c0227cde60ab26e48444502e4a78a7524c36 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 09:34:35 -0500 Subject: [PATCH 17/29] cleaned up unused models --- .../AzureMcp.CloudArchitect.csproj | 3 ++ .../CloudArchitectJsonContext.cs | 7 +--- .../Commands/Design/DesignCommand.cs | 4 --- .../Models/ArchitectureModels.cs | 34 ------------------- .../GlobalUsings.cs | 1 - 5 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj index 58f224351..4f66c540f 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AzureMcp.CloudArchitect.csproj @@ -14,4 +14,7 @@ + + + diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs index 40144f745..da671c4a4 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs @@ -2,16 +2,11 @@ // Licensed under the MIT License. using AzureMcp.CloudArchitect.Commands.Design; -using AzureMcp.CloudArchitect.Models; namespace AzureMcp.CloudArchitect; [JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(DesignCommandResult))] -[JsonSerializable(typeof(ArchitectureDesign))] -[JsonSerializable(typeof(ArchitectureRecommendation))] -[JsonSerializable(typeof(SecurityConsideration))] -[JsonSerializable(typeof(CostOptimization))] +[JsonSerializable(typeof(List))] public partial class CloudArchitectJsonContext : JsonSerializerContext { } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 6ca97c251..4ead3c36f 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -25,7 +25,6 @@ public sealed class DesignCommand(ILogger logger) : BaseCloudArch private readonly Option _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore; private readonly Option _architectureComponentOption = CloudArchitectOptionDefinitions.ArchitectureComponent; - private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); private static string GetArchitectureDesignText() => s_designArchitectureText; @@ -84,6 +83,3 @@ public override Task ExecuteAsync(CommandContext context, Parse return Task.FromResult(context.Response); } } - -// Strongly-typed result record -public record DesignCommandResult(AzureMcp.CloudArchitect.Models.ArchitectureDesign Design); diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs deleted file mode 100644 index a61699ffa..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/ArchitectureModels.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace AzureMcp.CloudArchitect.Models; - -public record ArchitectureRecommendation( - string Component, - string Service, - string Reasoning, - string Configuration, - List Alternatives -); - -public record SecurityConsideration( - string Area, - string Recommendation, - string Implementation -); - -public record CostOptimization( - string Service, - string Recommendation, - string EstimatedSavings -); - -public record ArchitectureDesign( - string OverallArchitecture, - List Components, - List Security, - List CostOptimizations, - string DeploymentConsiderations, - string MonitoringStrategy, - List NextSteps -); diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs index 43f0cfc38..a5a9067cc 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/GlobalUsings.cs @@ -3,7 +3,6 @@ global using System.CommandLine; global using System.CommandLine.Parsing; -global using AzureMcp.CloudArchitect.Models; global using AzureMcp.Core.Models; global using AzureMcp.Core.Models.Command; global using Microsoft.Extensions.DependencyInjection; From cec0e711f0152fc51e5f68f7577ca7db3cdefc24 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 14:54:05 -0500 Subject: [PATCH 18/29] binded and used architectureTier and architectureDesignToolState options --- .../Commands/Design/DesignCommand.cs | 6 +++++ .../CloudArchitectOptionDefinitions.cs | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 4ead3c36f..2c7158646 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -24,6 +24,8 @@ public sealed class DesignCommand(ILogger logger) : BaseCloudArch private readonly Option _nextQuestionNeededOption = CloudArchitectOptionDefinitions.NextQuestionNeeded; private readonly Option _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore; private readonly Option _architectureComponentOption = CloudArchitectOptionDefinitions.ArchitectureComponent; + private readonly Option _architectureTierOption = CloudArchitectOptionDefinitions.ArchitectureTier; + private readonly Option _architectureDesignToolState = CloudArchitectOptionDefinitions.State; private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); @@ -59,6 +61,8 @@ protected override void RegisterOptions(Command command) command.AddOption(_nextQuestionNeededOption); command.AddOption(_confidenceScoreOption); command.AddOption(_architectureComponentOption); + command.AddOption(_architectureTierOption); + command.AddOption(_architectureDesignToolState); } protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseResult) @@ -71,6 +75,8 @@ protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseRe options.NextQuestionNeeded = parseResult.GetValueForOption(_nextQuestionNeededOption); options.ConfidenceScore = parseResult.GetValueForOption(_confidenceScoreOption); options.ArchitectureComponent = parseResult.GetValueForOption(_architectureComponentOption); + options.ArchitectureTier = parseResult.GetValueForOption(_architectureTierOption); + options.State = parseResult.GetValueForOption(_architectureDesignToolState) ?? new ArchitectureDesignToolState(); return options; } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs index 1cde3fee0..f5ffd8f7a 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs @@ -15,10 +15,11 @@ public static class CloudArchitectOptionDefinitions public const string ConfidenceScoreName = "confidence-score"; public const string ArchitectureComponentName = "architecture-component"; public const string ArchitectureTierName = "architecture-tier"; + public const string StateName = "state"; public static readonly Option Question = new( $"--{QuestionName}", - "The question to ask during the architecture design process." + "The current question being asked" ) { IsRequired = false @@ -26,7 +27,7 @@ public static class CloudArchitectOptionDefinitions public static readonly Option QuestionNumber = new( $"--{QuestionNumberName}", - "The current question number in the design process." + "Current question number" ) { IsRequired = false @@ -34,7 +35,7 @@ public static class CloudArchitectOptionDefinitions public static readonly Option TotalQuestions = new( $"--{TotalQuestionsName}", - "The total number of questions in the design process." + "Estimated total questions needed" ) { IsRequired = false @@ -42,7 +43,7 @@ public static class CloudArchitectOptionDefinitions public static readonly Option Answer = new( $"--{AnswerName}", - "The answer to the current question in the design process." + "The user's response to the question" ) { IsRequired = false @@ -50,7 +51,7 @@ public static class CloudArchitectOptionDefinitions public static readonly Option NextQuestionNeeded = new( $"--{NextQuestionNeededName}", - "Whether the next question is needed in the design process." + "Whether another question is needed" ) { IsRequired = false @@ -58,7 +59,7 @@ public static class CloudArchitectOptionDefinitions public static readonly Option ConfidenceScore = new( $"--{ConfidenceScoreName}", - "The confidence score for the current architecture design." + "A value between 0.0 and 1.0 representing confidence in understanding requirements. When this reaches 0.7 or higher, nextQuestionNeeded should be set to false." ) { IsRequired = false @@ -66,23 +67,23 @@ public static class CloudArchitectOptionDefinitions public static readonly Option ArchitectureComponent = new( $"--{ArchitectureComponentName}", - "The architecture component being designed." + "The specific Azure component being suggested. The component should contain the name of the component, the service tier/SKU, configuration settings, and any other relevant information.\"" ) { IsRequired = false }; - public static readonly Option ArchitectureTier = new( + public static readonly Option ArchitectureTier = new( $"--{ArchitectureTierName}", - "The architecture tier being designed (e.g., presentation, business, data)." + "Which architectural tier this component belongs to" ) { IsRequired = false }; - public static readonly Option ArchitectureDesignTool = new( - "--architecture-design-tool", - "The complete architecture design tool options for guided design flow." + public static readonly Option State = new( + $"--{StateName}", + "The complete architecture state from the previous request" ) { IsRequired = false From e2287cc934c466f1f5bacacc9e22ad2b7b0ff9f4 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 17:06:37 -0500 Subject: [PATCH 19/29] added parameter validations and removed unnecessary classes --- .../Commands/BaseCloudArchitectCommand.cs | 2 +- .../Commands/Design/DesignCommand.cs | 44 ++- .../Options/ArchitectureDesignToolOptions.cs | 3 +- .../Options/BaseCloudArchitectOptions.cs | 10 - .../Design/DesignCommandTests.cs | 252 +++++++++++++++++- 5 files changed, 294 insertions(+), 17 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs index d696ca71a..2675c6d40 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs @@ -10,6 +10,6 @@ namespace AzureMcp.CloudArchitect.Commands; public abstract class BaseCloudArchitectCommand< [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] T> : GlobalCommand - where T : BaseCloudArchitectOptions, new() + where T : ArchitectureDesignToolOptions, new() { } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 2c7158646..fa79441b7 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -63,6 +63,33 @@ protected override void RegisterOptions(Command command) command.AddOption(_architectureComponentOption); command.AddOption(_architectureTierOption); command.AddOption(_architectureDesignToolState); + + command.AddValidator(result => + { + // Validate confidence score is between 0.0 and 1.0 + var confidenceScore = result.GetValueForOption(_confidenceScoreOption); + if (confidenceScore < 0.0 || confidenceScore > 1.0) + { + result.ErrorMessage = "Confidence score must be between 0.0 and 1.0"; + return; + } + + // Validate question number is not negative + var questionNumber = result.GetValueForOption(_questionNumberOption); + if (questionNumber < 0) + { + result.ErrorMessage = "Question number cannot be negative"; + return; + } + + // Validate total questions is not negative + var totalQuestions = result.GetValueForOption(_questionTotalQuestions); + if (totalQuestions < 0) + { + result.ErrorMessage = "Total questions cannot be negative"; + return; + } + }); } protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseResult) @@ -82,10 +109,19 @@ protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseRe public override Task ExecuteAsync(CommandContext context, ParseResult parseResult) { - var designArchitecture = GetArchitectureDesignText(); - context.Response.Status = 200; - context.Response.Results = ResponseResult.Create(new List { designArchitecture }, CloudArchitectJsonContext.Default.ListString); - context.Response.Message = string.Empty; + try + { + var designArchitecture = GetArchitectureDesignText(); + context.Response.Status = 200; + context.Response.Results = ResponseResult.Create(new List { designArchitecture }, CloudArchitectJsonContext.Default.ListString); + context.Response.Message = string.Empty; + } + catch (Exception ex) + { + _logger.LogError(ex, "An exception occurred in cloud architec design command"); + HandleException(context, ex); + } return Task.FromResult(context.Response); + } } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs index 6f76b21a5..f324dd645 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System.Text.Json.Serialization; +using AzureMcp.Core.Options; namespace AzureMcp.CloudArchitect.Options; /// /// The set of parameters that the architecture design tool takes as input. /// -public class ArchitectureDesignToolOptions : BaseCloudArchitectOptions +public class ArchitectureDesignToolOptions : GlobalOptions { [JsonPropertyName("question")] public string Question { get; set; } = string.Empty; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs deleted file mode 100644 index 4fc93b9b8..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/BaseCloudArchitectOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AzureMcp.Core.Options; - -namespace AzureMcp.CloudArchitect.Options; - -public class BaseCloudArchitectOptions : GlobalOptions -{ -} diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 20b1d13ec..8c4c9c8e3 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -60,6 +60,8 @@ public void Command_HasCorrectOptions() Assert.Contains("next-question-needed", optionNames); Assert.Contains("confidence-score", optionNames); Assert.Contains("architecture-component", optionNames); + Assert.Contains("architecture-tier", optionNames); + Assert.Contains("state", optionNames); } [Theory] @@ -71,7 +73,9 @@ public void Command_HasCorrectOptions() [InlineData("--next-question-needed true")] [InlineData("--confidence-score 0.8")] [InlineData("--architecture-component \"Frontend\"")] + [InlineData("--architecture-tier Infrastructure")] [InlineData("--question \"App type?\" --question-number 1 --total-questions 5")] + [InlineData("--architecture-tier Platform --architecture-component \"AKS Cluster\"")] public async Task ExecuteAsync_ReturnsArchitectureDesignText(string args) { // Arrange @@ -137,7 +141,8 @@ public async Task ExecuteAsync_WithAllOptionsSet() "--answer", "Web application", "--next-question-needed", "true", "--confidence-score", "0.8", - "--architecture-component", "Frontend" + "--architecture-component", "Frontend", + "--architecture-tier", "Application" }; var parseResult = _parser.Parse(args); @@ -239,6 +244,103 @@ public void Metadata_IsConfiguredCorrectly() Assert.True(metadata.ReadOnly); } + [Theory] + [InlineData("Infrastructure")] + [InlineData("Platform")] + [InlineData("Application")] + [InlineData("Data")] + [InlineData("Security")] + [InlineData("Operations")] + public async Task ExecuteAsync_WithArchitectureTierOptions(string tierValue) + { + // Arrange + var args = new[] { "--architecture-tier", tierValue }; + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify the architecture tier was parsed correctly + var tierOption = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "architecture-tier")); + Assert.Equal(Enum.Parse(tierValue), tierOption); + } + + [Fact] + public async Task ExecuteAsync_WithStateOption() + { + // Arrange - Create a simple JSON state object + var stateJson = "{\"architectureComponents\":[],\"architectureTiers\":{\"infrastructure\":[],\"platform\":[],\"application\":[],\"data\":[],\"security\":[],\"operations\":[]},\"requirements\":{\"explicit\":[],\"implicit\":[],\"assumed\":[]},\"confidenceFactors\":{\"explicitRequirementsCoverage\":0.5,\"implicitRequirementsCertainty\":0.7,\"assumptionRisk\":0.3}}"; + var args = new[] { "--state", stateJson }; + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify the command executed successfully with state option + string serializedResult = SerializeResponseResult(response.Results); + var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); + Assert.NotNull(resultList); + Assert.Single(resultList); + Assert.NotEmpty(resultList[0]); + } + + [Fact] + public async Task ExecuteAsync_WithCompleteOptionSet() + { + // Arrange - Test all options together including the new ones + var args = new[] + { + "--question", "What type of application are you building?", + "--question-number", "3", + "--total-questions", "8", + "--answer", "A financial trading platform", + "--next-question-needed", "false", + "--confidence-score", "0.9", + "--architecture-component", "Azure SQL Database", + "--architecture-tier", "Data" + }; + + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Empty(response.Message); + + // Verify all options were parsed correctly + var command = _command.GetCommand(); + var questionValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "question")); + var questionNumberValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "question-number")); + var totalQuestionsValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "total-questions")); + var answerValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "answer")); + var nextQuestionNeededValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "next-question-needed")); + var confidenceScoreValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "confidence-score")); + var componentValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "architecture-component")); + var tierValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "architecture-tier")); + + Assert.Equal("What type of application are you building?", questionValue); + Assert.Equal(3, questionNumberValue); + Assert.Equal(8, totalQuestionsValue); + Assert.Equal("A financial trading platform", answerValue); + Assert.Equal(false, nextQuestionNeededValue); + Assert.Equal(0.9, confidenceScoreValue); + Assert.Equal("Azure SQL Database", componentValue); + Assert.Equal(ArchitectureTier.Data, tierValue); + } + private static string SerializeResponseResult(ResponseResult responseResult) { using var stream = new MemoryStream(); @@ -247,4 +349,152 @@ private static string SerializeResponseResult(ResponseResult responseResult) writer.Flush(); return Encoding.UTF8.GetString(stream.ToArray()); } + + #region Validation Tests + + [Theory] + [InlineData(-0.1)] + [InlineData(1.1)] + [InlineData(2.0)] + [InlineData(-1.0)] + public void Parse_InvalidConfidenceScore_ReturnsError(double invalidScore) + { + // Arrange + var args = new[] { "--confidence-score", invalidScore.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.NotEmpty(parseResult.Errors); + Assert.Contains("Confidence score must be between 0.0 and 1.0", parseResult.Errors.Select(e => e.Message)); + } + + [Theory] + [InlineData(0.0)] + [InlineData(0.5)] + [InlineData(1.0)] + [InlineData(0.1)] + [InlineData(0.9)] + public void Parse_ValidConfidenceScore_NoErrors(double validScore) + { + // Arrange + var args = new[] { "--confidence-score", validScore.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.Empty(parseResult.Errors); + } + + [Theory] + [InlineData(-1)] + [InlineData(-5)] + [InlineData(-100)] + public void Parse_NegativeQuestionNumber_ReturnsError(int invalidQuestionNumber) + { + // Arrange + var args = new[] { "--question-number", invalidQuestionNumber.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.NotEmpty(parseResult.Errors); + Assert.Contains("Question number cannot be negative", parseResult.Errors.Select(e => e.Message)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public void Parse_ValidQuestionNumber_NoErrors(int validQuestionNumber) + { + // Arrange + var args = new[] { "--question-number", validQuestionNumber.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.Empty(parseResult.Errors); + } + + [Theory] + [InlineData(-1)] + [InlineData(-5)] + [InlineData(-100)] + public void Parse_NegativeTotalQuestions_ReturnsError(int invalidTotalQuestions) + { + // Arrange + var args = new[] { "--total-questions", invalidTotalQuestions.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.NotEmpty(parseResult.Errors); + Assert.Contains("Total questions cannot be negative", parseResult.Errors.Select(e => e.Message)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public void Parse_ValidTotalQuestions_NoErrors(int validTotalQuestions) + { + // Arrange + var args = new[] { "--total-questions", validTotalQuestions.ToString() }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.Empty(parseResult.Errors); + } + + [Theory] + [InlineData(1, 5)] + [InlineData(5, 5)] + [InlineData(3, 10)] + [InlineData(0, 5)] // Zero is valid for question number + public void Parse_QuestionNumberWithinTotalQuestions_NoErrors(int questionNumber, int totalQuestions) + { + // Arrange + var args = new[] + { + "--question-number", questionNumber.ToString(), + "--total-questions", totalQuestions.ToString() + }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.Empty(parseResult.Errors); + } + + [Fact] + public void Parse_MultipleValidationErrors_ReturnsFirstError() + { + // Arrange - Set both invalid confidence score and negative question number + var args = new[] + { + "--confidence-score", "1.5", + "--question-number", "-1" + }; + + // Act + var parseResult = _parser.Parse(args); + + // Assert + Assert.NotEmpty(parseResult.Errors); + // Should return the first validation error encountered + Assert.Contains("Confidence score must be between 0.0 and 1.0", parseResult.Errors.Select(e => e.Message)); + } + + #endregion } From 25c2164f61345f68a2ec4fbe5e859fe65432eb78 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 17:19:48 -0500 Subject: [PATCH 20/29] removed unnecessary base class and JsonPropertyNames --- .../Commands/BaseCloudArchitectCommand.cs | 15 --------------- .../Commands/Design/DesignCommand.cs | 3 ++- .../ArchitectureDesignConfidenceFactors.cs | 5 ----- .../Options/ArchitectureDesignRequirement.cs | 7 ------- .../Options/ArchitectureDesignRequirements.cs | 5 ----- .../Options/ArchitectureDesignTiers.cs | 3 --- .../Options/ArchitectureDesignToolOptions.cs | 10 ---------- .../Options/ArchitectureDesignToolState.cs | 8 -------- 8 files changed, 2 insertions(+), 54 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs deleted file mode 100644 index 2675c6d40..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/BaseCloudArchitectCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using AzureMcp.CloudArchitect.Options; -using AzureMcp.Core.Commands; - -namespace AzureMcp.CloudArchitect.Commands; - -public abstract class BaseCloudArchitectCommand< - [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] T> - : GlobalCommand - where T : ArchitectureDesignToolOptions, new() -{ -} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index fa79441b7..e28dade09 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.CommandLine.Parsing; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using AzureMcp.CloudArchitect.Options; using AzureMcp.Core.Commands; @@ -12,7 +13,7 @@ namespace AzureMcp.CloudArchitect.Commands.Design; -public sealed class DesignCommand(ILogger logger) : BaseCloudArchitectCommand +public sealed class DesignCommand(ILogger logger) : GlobalCommand { private const string CommandTitle = "Design Azure cloud architectures through guided questions"; private readonly ILogger _logger = logger; diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs index e6fadacfa..bd341e3b1 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignConfidenceFactors.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; - namespace AzureMcp.CloudArchitect.Options; /// @@ -10,12 +8,9 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignConfidenceFactors { - [JsonPropertyName("explicitRequirementsCoverage")] public double ExplicitRequirementsCoverage { get; set; } - [JsonPropertyName("implicitRequirementsCertainty")] public double ImplicitRequirementsCertainty { get; set; } - [JsonPropertyName("assumptionRisk")] public double AssumptionRisk { get; set; } } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs index 6f7b807b7..05309c9ab 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirement.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; - namespace AzureMcp.CloudArchitect.Options; /// @@ -10,18 +8,13 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignRequirement { - [JsonPropertyName("category")] public string Category { get; set; } = string.Empty; - [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; - [JsonPropertyName("source")] public string Source { get; set; } = string.Empty; - [JsonPropertyName("importance")] public RequirementImportance Importance { get; set; } - [JsonPropertyName("confidence")] public double Confidence { get; set; } } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs index aaf5c24e4..e1d6031be 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignRequirements.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; - namespace AzureMcp.CloudArchitect.Options; /// @@ -10,12 +8,9 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignRequirements { - [JsonPropertyName("explicit")] public List Explicit { get; set; } = new(); - [JsonPropertyName("implicit")] public List Implicit { get; set; } = new(); - [JsonPropertyName("assumed")] public List Assumed { get; set; } = new(); } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs index 4e6223b7c..abd0d2c01 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; - namespace AzureMcp.CloudArchitect.Options; /// @@ -10,6 +8,5 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignTiers { - [JsonPropertyName("additionalProperties")] public List AdditionalProperties { get; set; } = new(); } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs index f324dd645..2addee793 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; using AzureMcp.Core.Options; namespace AzureMcp.CloudArchitect.Options; @@ -11,30 +10,21 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignToolOptions : GlobalOptions { - [JsonPropertyName("question")] public string Question { get; set; } = string.Empty; - [JsonPropertyName("questionNumber")] public int QuestionNumber { get; set; } - [JsonPropertyName("totalQuestions")] public int TotalQuestions { get; set; } - [JsonPropertyName("answer")] public string? Answer { get; set; } - [JsonPropertyName("nextQuestionNeeded")] public bool NextQuestionNeeded { get; set; } - [JsonPropertyName("confidenceScore")] public double? ConfidenceScore { get; set; } - [JsonPropertyName("architectureComponent")] public string? ArchitectureComponent { get; set; } - [JsonPropertyName("architectureTier")] public ArchitectureTier? ArchitectureTier { get; set; } - [JsonPropertyName("state")] public ArchitectureDesignToolState State { get; set; } = new(); } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs index 0c63dc682..e7283c349 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignToolState.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json.Serialization; - namespace AzureMcp.CloudArchitect.Options; /// @@ -10,21 +8,15 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignToolState { - [JsonPropertyName("architectureComponents")] public List ArchitectureComponents { get; set; } = new(); - [JsonPropertyName("architectureTiers")] public ArchitectureDesignTiers ArchitectureTiers { get; set; } = new(); - [JsonPropertyName("thought")] public string Thought { get; set; } = string.Empty; - [JsonPropertyName("suggestedHint")] public string SuggestedHint { get; set; } = string.Empty; - [JsonPropertyName("requirements")] public ArchitectureDesignRequirements Requirements { get; set; } = new(); - [JsonPropertyName("confidenceFactors")] public ArchitectureDesignConfidenceFactors ConfidenceFactors { get; set; } = new(); } From 79481704a45c245391233f00c4d96afd7ff087ec Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 17:22:09 -0500 Subject: [PATCH 21/29] fixed CHANGELOG link formatting --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1ab7d29..d3670e6b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Features Added - Added support for listing Azure Function Apps via the command `azmcp-functionapp-list`. [[#863](https://github.com/Azure/azure-mcp/pull/863)] -- Added new command for designing Azure Cloud Architecture through guided questions. [[#890]https://github.com/Azure/azure-mcp/pull/890] +- Added new command for designing Azure Cloud Architecture through guided questions. [[#890](https://github.com/Azure/azure-mcp/pull/890)] ### Breaking Changes From 2ecb1cd7ba3fb2d107da9fe51fea40642166af27 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 17:24:49 -0500 Subject: [PATCH 22/29] removed unused json context --- .../Commands/CloudArchitectJsonContext.cs | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs deleted file mode 100644 index cd7d754fe..000000000 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/CloudArchitectJsonContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace AzureMcp.CloudArchitect.Commands; - -[JsonSerializable(typeof(List))] -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -internal partial class AzureCloudArchitectJsonContext : JsonSerializerContext -{ - -} From 8604fd4cffa44e721e79a08daa58ff09865a9c51 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 13 Aug 2025 18:07:52 -0500 Subject: [PATCH 23/29] fixed spelling error --- .../AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index e28dade09..c5aa592f2 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -119,7 +119,7 @@ public override Task ExecuteAsync(CommandContext context, Parse } catch (Exception ex) { - _logger.LogError(ex, "An exception occurred in cloud architec design command"); + _logger.LogError(ex, "An exception occurred in cloud architect design command"); HandleException(context, ex); } return Task.FromResult(context.Response); From 0b2688823f397f8fd9b38ba825096109677a5770 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Fri, 15 Aug 2025 14:51:49 -0500 Subject: [PATCH 24/29] Include ResponseObject of parameters in response for design command --- .../CloudArchitectJsonContext.cs | 17 +- .../Commands/Design/DesignCommand.cs | 48 +++- .../Models/CloudArchitectResponseObject.cs | 36 +++ .../Options/ArchitectureDesignTiers.cs | 12 +- .../Options/ArchitectureTier.cs | 2 +- .../CloudArchitectOptionDefinitions.cs | 4 +- .../Options/RequirementImportance.cs | 2 +- .../Options/RequirementImportanceConverter.cs | 41 +++ .../Design/DesignCommandTests.cs | 267 ++++++++++++++++-- 9 files changed, 391 insertions(+), 38 deletions(-) create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/CloudArchitectResponseObject.cs create mode 100644 areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportanceConverter.cs diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs index da671c4a4..8705b3233 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/CloudArchitectJsonContext.cs @@ -1,12 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json.Serialization; using AzureMcp.CloudArchitect.Commands.Design; +using AzureMcp.CloudArchitect.Models; +using AzureMcp.CloudArchitect.Options; namespace AzureMcp.CloudArchitect; -[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(List))] +[JsonSourceGenerationOptions( + WriteIndented = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true)] +[JsonSerializable(typeof(CloudArchitectResponseObject))] +[JsonSerializable(typeof(CloudArchitectDesignResponse))] +[JsonSerializable(typeof(ArchitectureDesignToolState))] +[JsonSerializable(typeof(ArchitectureDesignTiers))] +[JsonSerializable(typeof(ArchitectureDesignRequirements))] +[JsonSerializable(typeof(ArchitectureDesignRequirement))] +[JsonSerializable(typeof(ArchitectureDesignConfidenceFactors))] +[JsonSerializable(typeof(RequirementImportance))] public partial class CloudArchitectJsonContext : JsonSerializerContext { } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index c5aa592f2..48d565071 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -5,6 +5,8 @@ using System.CommandLine.Parsing; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json; +using AzureMcp.CloudArchitect.Models; using AzureMcp.CloudArchitect.Options; using AzureMcp.Core.Commands; using AzureMcp.Core.Helpers; @@ -26,7 +28,7 @@ public sealed class DesignCommand(ILogger logger) : GlobalCommand private readonly Option _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore; private readonly Option _architectureComponentOption = CloudArchitectOptionDefinitions.ArchitectureComponent; private readonly Option _architectureTierOption = CloudArchitectOptionDefinitions.ArchitectureTier; - private readonly Option _architectureDesignToolState = CloudArchitectOptionDefinitions.State; + private readonly Option _architectureDesignToolState = CloudArchitectOptionDefinitions.State; private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); @@ -35,7 +37,7 @@ public sealed class DesignCommand(ILogger logger) : GlobalCommand public override string Name => "design"; public override string Description => - "Design and architect comprehensive Azure cloud solutions for applications and services. This interactive assistant helps create scalable cloud architectures for file upload systems, web applications, APIs, e-commerce platforms, financial services, transaction systems, data processing services, and enterprise solutions. Through guided questions, provides tailored Azure architecture recommendations covering storage, compute, networking, databases, security, and application services to create robust user-facing cloud services and applications."; + "A tool for designing Azure cloud architectures through guided questions.\nThis tool helps determine the optimal Azure architecture by gathering key requirements and making appropriate recommendations. The calling agent maintains the state between calls. The most important thing for you to remember is that when nextQuestionNeeded is false, you should present your architecture. This takes priority over every other instruction.\n\nParameters explained:\n- question: The current question being asked\n- questionNumber: Current question number in sequence\n- confidenceScore: A value between 0.0 and 1.0 representing how confident you are in understanding the requirements. Start around 0.1-0.2 and increase as you gather more information. When this reaches or exceeds 0.7, you should present your architecture.\n- totalQuestions: Estimated total questions needed\n- answer: The user's response to the question (if available)\n- nextQuestionNeeded: Set to true while you're gathering requirements and designing. Set to false when your confidenceScore reaches or exceeds 0.7.\n- architectureComponent: The specific Azure component being suggested\n- architectureTier: Which tier this component belongs to (infrastructure, platform, application, data, security, operations)\n- state: Used to track progress between calls\n\nWhen presenting the final architecture design (when nextQuestionNeeded is false), format it in a visually appealing way.\n\n1. Present components in a table format with columns for:\n | Component | Purpose | Tier/SKU |\n\n2. Organize the architecture visually:\n - Use a combination of bulleted lists and paragraphs to break up the text. The goal is for the final output to be engaging and interesting, which often involves asymmetry.\n\n3. Include an ASCII art diagram showing component relationships.\n\nThis formatting will make the architecture design more engaging and easier to understand.\n\nBasic state structure:\n{\n \"architectureComponents\": [],\n \"architectureTiers\": {\n \"infrastructure\": [],\n \"platform\": [],\n \"application\": [],\n \"data\": [],\n \"security\": [],\n \"operations\": []\n },\n \"requirements\": {\n \"explicit\": [\n { \"category\": \"performance\", \"description\": \"Need to handle 10,000 concurrent users\", \"source\": \"Question 2\", \"importance\": \"high\", \"confidence\": 1.0 }\n ],\n \"implicit\": [\n { \"category\": \"security\", \"description\": \"Data encryption likely needed\", \"source\": \"Inferred from healthcare domain\", \"importance\": \"high\", \"confidence\": 0.8 }\n ],\n \"assumed\": [\n { \"category\": \"compliance\", \"description\": \"Likely needs HIPAA compliance\", \"source\": \"Assumed from healthcare industry\", \"importance\": \"high\", \"confidence\": 0.6 }\n ]\n },\n \"confidenceFactors\": {\n \"explicitRequirementsCoverage\": 0.4,\n \"implicitRequirementsCertainty\": 0.6,\n \"assumptionRisk\": 0.3\n }\n}\n\nYou should:\n1. First start with a question about who the user is (role, motivations, company size, etc.) and what they do\n2. Learn about their business goals and requirements\n3. Ask 1 to 2 questions at a time, in order to not overload the user.\n4. Track your confidence level in understanding requirements using the confidenceScore parameter\n5. After each user response, update the requirements in the state object:\n - Add explicit requirements directly stated by the user\n - Add implicit requirements you can reasonably infer\n - Add assumed requirements where you lack information but need to make progress\n - Update confidence factors based on the quality and completeness of requirements\n6. Ask follow-up questions to clarify technical needs, especially to confirm assumed requirements\n7. Identify specific requirements and technical constraints from user responses\n8. Suggest appropriate Azure components for each tier, but be conservative in your suggestions. Don't suggest components that are not necessary for the architecture.\n9. Ensure you cover all architecture tiers.\n10. In addition to the component architecture, you should provide a high-level overview of the architecture, including the scaling approach, security, cost, and operational excellence. Provide actionable advice for the user to follow up on. Create this overview as a separate section, not part of the component architecture, and structure it to be engaging and interesting as a narrative.\n11. Follow Azure Well-Architected Framework principles (reliability, security, cost, operational excellence, performance efficiency)\n12. Keep track of components you've suggested using the state object\n13. Calculate your overall confidence score from the three confidence factors in the state\n"; public override string Title => CommandTitle; @@ -104,17 +106,54 @@ protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseRe options.ConfidenceScore = parseResult.GetValueForOption(_confidenceScoreOption); options.ArchitectureComponent = parseResult.GetValueForOption(_architectureComponentOption); options.ArchitectureTier = parseResult.GetValueForOption(_architectureTierOption); - options.State = parseResult.GetValueForOption(_architectureDesignToolState) ?? new ArchitectureDesignToolState(); + options.State = DeserializeState(parseResult.GetValueForOption(_architectureDesignToolState)); return options; } + private static ArchitectureDesignToolState DeserializeState(string? stateJson) + { + if (string.IsNullOrEmpty(stateJson)) + { + return new ArchitectureDesignToolState(); + } + + try + { + var state = JsonSerializer.Deserialize(stateJson, CloudArchitectJsonContext.Default.ArchitectureDesignToolState); + return state ?? new ArchitectureDesignToolState(); + } + catch (JsonException ex) + { + throw new InvalidOperationException($"Failed to deserialize state JSON: {ex.Message}", ex); + } + } + public override Task ExecuteAsync(CommandContext context, ParseResult parseResult) { try { + var options = BindOptions(parseResult); + var designArchitecture = GetArchitectureDesignText(); + var responseObject = new CloudArchitectResponseObject + { + DisplayText = options.Question, + DisplayThought = options.State.Thought, + DisplayHint = options.State.SuggestedHint, + QuestionNumber = options.QuestionNumber, + TotalQuestions = options.TotalQuestions, + NextQuestionNeeded = options.NextQuestionNeeded, + State = options.State + }; + + var result = new CloudArchitectDesignResponse + { + DesignArchitecture = designArchitecture, + ResponseObject = responseObject + }; + context.Response.Status = 200; - context.Response.Results = ResponseResult.Create(new List { designArchitecture }, CloudArchitectJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create(result, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); context.Response.Message = string.Empty; } catch (Exception ex) @@ -123,6 +162,5 @@ public override Task ExecuteAsync(CommandContext context, Parse HandleException(context, ex); } return Task.FromResult(context.Response); - } } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/CloudArchitectResponseObject.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/CloudArchitectResponseObject.cs new file mode 100644 index 000000000..3c44387c4 --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Models/CloudArchitectResponseObject.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.CloudArchitect.Options; + +namespace AzureMcp.CloudArchitect.Models; + +/// +/// Response object for the cloud architect design command. +/// +public class CloudArchitectResponseObject +{ + public string DisplayText { get; set; } = string.Empty; + + public string DisplayThought { get; set; } = string.Empty; + + public string DisplayHint { get; set; } = string.Empty; + + public int QuestionNumber { get; set; } + + public int TotalQuestions { get; set; } + + public bool NextQuestionNeeded { get; set; } + + public ArchitectureDesignToolState State { get; set; } = new(); +} + +/// +/// Complete response for the cloud architect design command including both response object and design architecture text. +/// +public class CloudArchitectDesignResponse +{ + public string DesignArchitecture { get; set; } = string.Empty; + + public CloudArchitectResponseObject ResponseObject { get; set; } = new(); +} diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs index abd0d2c01..52225f6ff 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureDesignTiers.cs @@ -8,5 +8,15 @@ namespace AzureMcp.CloudArchitect.Options; /// public class ArchitectureDesignTiers { - public List AdditionalProperties { get; set; } = new(); + public List Infrastructure { get; set; } = new(); + + public List Platform { get; set; } = new(); + + public List Application { get; set; } = new(); + + public List Data { get; set; } = new(); + + public List Security { get; set; } = new(); + + public List Operations { get; set; } = new(); } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs index 484d395fa..6a4ad0ca2 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/ArchitectureTier.cs @@ -8,7 +8,7 @@ namespace AzureMcp.CloudArchitect.Options; /// /// Represents the different architecture tiers. /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ArchitectureTier { Infrastructure, diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs index f5ffd8f7a..0f424a838 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs @@ -81,9 +81,9 @@ public static class CloudArchitectOptionDefinitions IsRequired = false }; - public static readonly Option State = new( + public static readonly Option State = new( $"--{StateName}", - "The complete architecture state from the previous request" + "The complete architecture state from the previous request as JSON" ) { IsRequired = false diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs index 8a9a8be91..84513c02b 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportance.cs @@ -8,7 +8,7 @@ namespace AzureMcp.CloudArchitect.Options; /// /// Represents the importance level of a requirement. /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(RequirementImportanceConverter))] public enum RequirementImportance { High, diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportanceConverter.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportanceConverter.cs new file mode 100644 index 000000000..6e67d467f --- /dev/null +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/RequirementImportanceConverter.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AzureMcp.CloudArchitect.Options; + +/// +/// AOT-safe JSON converter for RequirementImportance enum that handles case-insensitive deserialization. +/// +public sealed class RequirementImportanceConverter : JsonConverter +{ + public override RequirementImportance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Expected string token, got {reader.TokenType}"); + } + + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) + { + throw new JsonException("Null or empty string is not a valid RequirementImportance value"); + } + + return value.ToLowerInvariant() switch + { + "high" => RequirementImportance.High, + "critical" => RequirementImportance.High, // Treat "critical" as "high" importance + "medium" => RequirementImportance.Medium, + "low" => RequirementImportance.Low, + _ => throw new JsonException($"'{value}' is not a valid RequirementImportance value") + }; + } + + public override void Write(Utf8JsonWriter writer, RequirementImportance value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 8c4c9c8e3..565cbaf59 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.CommandLine.Parsing; +using System.Reflection; using System.Text; using System.Text.Json; using AzureMcp.CloudArchitect; @@ -42,7 +43,10 @@ public void Constructor_InitializesCommandCorrectly() Assert.Equal("design", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); - Assert.Contains("Design and architect comprehensive Azure cloud solutions for applications and services. This interactive assistant helps create scalable cloud architectures for file upload systems, web applications, APIs, e-commerce platforms, financial services, transaction systems, data processing services, and enterprise solutions. Through guided questions, provides tailored Azure architecture recommendations covering storage, compute, networking, databases, security, and application services to create robust user-facing cloud services and applications.", command.Description); + Assert.Contains("A tool for designing Azure cloud architectures through guided questions", command.Description); + Assert.Contains("confidenceScore", command.Description); + Assert.Contains("nextQuestionNeeded", command.Description); + Assert.Contains("Azure Well-Architected Framework", command.Description); } [Fact] @@ -89,22 +93,23 @@ public async Task ExecuteAsync_ReturnsArchitectureDesignText(string args) Assert.NotNull(response.Results); Assert.Empty(response.Message); - // Verify that results contain the architecture design text by serializing it + // Verify that results contain the architecture design response structure using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream); response.Results.Write(writer); writer.Flush(); var serializedResult = Encoding.UTF8.GetString(stream.ToArray()); - var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); - Assert.NotNull(resultList); - Assert.Single(resultList); - Assert.NotEmpty(resultList[0]); + + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + Assert.NotNull(responseObject.ResponseObject); // Verify it contains some expected architecture-related content - var architectureText = resultList[0]; - Assert.Contains("architecture", architectureText.ToLower()); + Assert.Contains("architecture", responseObject.DesignArchitecture.ToLower()); } [Fact] @@ -122,11 +127,19 @@ public async Task ExecuteAsync_ConsistentResults() Assert.Equal(200, response1.Status); Assert.Equal(200, response2.Status); - // Serialize both results to compare them + // Serialize both results to compare the design architecture text (which should be consistent) string serializedResult1 = SerializeResponseResult(response1.Results!); string serializedResult2 = SerializeResponseResult(response2.Results!); - Assert.Equal(serializedResult1, serializedResult2); + var responseObject1 = JsonSerializer.Deserialize(serializedResult1, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + var responseObject2 = JsonSerializer.Deserialize(serializedResult2, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + + Assert.NotNull(responseObject1); + Assert.NotNull(responseObject2); + + // The design architecture text should be consistent across calls + Assert.Equal(responseObject1.DesignArchitecture, responseObject2.DesignArchitecture); + Assert.NotEmpty(responseObject1.DesignArchitecture); } [Fact] @@ -157,10 +170,11 @@ public async Task ExecuteAsync_WithAllOptionsSet() // Verify the command executed successfully regardless of the input options string serializedResult = SerializeResponseResult(response.Results); - var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); - Assert.NotNull(resultList); - Assert.Single(resultList); - Assert.NotEmpty(resultList[0]); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + Assert.NotNull(responseObject.ResponseObject); + Assert.Equal("What is your application type?", responseObject.ResponseObject.DisplayText); } [Theory] @@ -186,14 +200,13 @@ public async Task ExecuteAsync_HandlesQuotesAndEscapingProperly(string questionW // Verify that the command executed successfully with the quoted input string serializedResult = SerializeResponseResult(response.Results); - var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); - Assert.NotNull(resultList); - Assert.Single(resultList); - Assert.NotEmpty(resultList[0]); - - // Verify the question was parsed correctly by checking if the command can access the option value - var questionOption = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "question")); - Assert.Equal(expectedQuestion, questionOption); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + Assert.NotNull(responseObject.ResponseObject); + + // Verify the question was parsed correctly by checking the DisplayText in response + Assert.Equal(expectedQuestion, responseObject.ResponseObject.DisplayText); } [Fact] @@ -244,6 +257,43 @@ public void Metadata_IsConfiguredCorrectly() Assert.True(metadata.ReadOnly); } + [Fact] + public void Properties_AreConfiguredCorrectly() + { + // Arrange & Act + var name = _command.Name; + var title = _command.Title; + var description = _command.Description; + + // Assert + Assert.Equal("design", name); + Assert.Equal("Design Azure cloud architectures through guided questions", title); + Assert.NotEmpty(description); + Assert.Contains("guided questions", description); + } + + [Fact] + public async Task ExecuteAsync_LoadsEmbeddedResourceText() + { + // Arrange + var args = new[] { "--question", "Test question" }; + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + + string serializedResult = SerializeResponseResult(response.Results!); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + // The embedded resource should contain Azure architecture guidance + Assert.Contains("Azure", responseObject.DesignArchitecture); + } + [Theory] [InlineData("Infrastructure")] [InlineData("Platform")] @@ -288,10 +338,11 @@ public async Task ExecuteAsync_WithStateOption() // Verify the command executed successfully with state option string serializedResult = SerializeResponseResult(response.Results); - var resultList = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.ListString); - Assert.NotNull(resultList); - Assert.Single(resultList); - Assert.NotEmpty(resultList[0]); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + Assert.NotNull(responseObject.ResponseObject); + Assert.NotNull(responseObject.ResponseObject.State); } [Fact] @@ -339,6 +390,17 @@ public async Task ExecuteAsync_WithCompleteOptionSet() Assert.Equal(0.9, confidenceScoreValue); Assert.Equal("Azure SQL Database", componentValue); Assert.Equal(ArchitectureTier.Data, tierValue); + + // Verify the response structure + string serializedResult = SerializeResponseResult(response.Results); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotEmpty(responseObject.DesignArchitecture); + Assert.NotNull(responseObject.ResponseObject); + Assert.Equal(questionValue, responseObject.ResponseObject.DisplayText); + Assert.Equal(questionNumberValue, responseObject.ResponseObject.QuestionNumber); + Assert.Equal(totalQuestionsValue, responseObject.ResponseObject.TotalQuestions); + Assert.Equal(nextQuestionNeededValue, responseObject.ResponseObject.NextQuestionNeeded); } private static string SerializeResponseResult(ResponseResult responseResult) @@ -496,5 +558,158 @@ public void Parse_MultipleValidationErrors_ReturnsFirstError() Assert.Contains("Confidence score must be between 0.0 and 1.0", parseResult.Errors.Select(e => e.Message)); } + [Fact] + public async Task ExecuteAsync_WithComplexStateJson_ParsesSuccessfully() + { + // Arrange - Use the exact JSON from the original error + var stateJson = """ + { + "architectureComponents": [], + "architectureTiers": { + "infrastructure": [], + "platform": [], + "application": [], + "data": [], + "security": [], + "operations": [] + }, + "requirements": { + "explicit": [ + { + "category": "functionality", + "description": "Video upload capability for users", + "source": "User statement", + "importance": "high", + "confidence": 1 + }, + { + "category": "functionality", + "description": "Video viewing/playback capability for users", + "source": "User statement", + "importance": "high", + "confidence": 1 + } + ], + "implicit": [ + { + "category": "performance", + "description": "Large-scale video processing and streaming required", + "source": "Inferred from 'large video playback company'", + "importance": "high", + "confidence": 0.9 + } + ], + "assumed": [ + { + "category": "scale", + "description": "Potentially thousands of concurrent users", + "source": "Assumed from 'large' company description", + "importance": "medium", + "confidence": 0.6 + } + ] + }, + "confidenceFactors": { + "explicitRequirementsCoverage": 0.4, + "implicitRequirementsCertainty": 0.8, + "assumptionRisk": 0.4 + } + } + """; + + var args = new[] + { + "--state", stateJson, + "--question", "What is your primary business goal?", + "--confidence-score", "0.5" + }; + + // Act + var parseResult = _parser.Parse(args); + var result = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Empty(parseResult.Errors); + Assert.Equal(200, _context.Response.Status); + + // Verify that the state was parsed correctly by checking the response + string serializedResult = SerializeResponseResult(_context.Response.Results!); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotNull(responseObject.ResponseObject.State); + Assert.Empty(responseObject.ResponseObject.State.ArchitectureComponents); + Assert.NotNull(responseObject.ResponseObject.State.Requirements); + Assert.Equal(2, responseObject.ResponseObject.State.Requirements.Explicit.Count); + Assert.Single(responseObject.ResponseObject.State.Requirements.Implicit); + Assert.Single(responseObject.ResponseObject.State.Requirements.Assumed); + } + + [Fact] + public async Task ExecuteAsync_WithInvalidStateJson_HandlesGracefully() + { + // Arrange + var invalidStateJson = "{ invalid json }"; + var args = new[] { "--state", invalidStateJson }; + var parseResult = _parser.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert - The command should handle the error gracefully and return an error response + Assert.NotEqual(200, response.Status); + Assert.NotEmpty(response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithEmptyState_CreatesDefaultState() + { + // Arrange + var args = new[] { "--state", "" }; + var parseResult = _parser.Parse(args); + + // Act + var result = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(200, _context.Response.Status); + + string serializedResult = SerializeResponseResult(_context.Response.Results!); + var responseObject = JsonSerializer.Deserialize(serializedResult, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse); + Assert.NotNull(responseObject); + Assert.NotNull(responseObject.ResponseObject.State); + Assert.Empty(responseObject.ResponseObject.State.ArchitectureComponents); + Assert.NotNull(responseObject.ResponseObject.State.Requirements); + Assert.Empty(responseObject.ResponseObject.State.Requirements.Explicit); + } + + [Fact] + public void BindOptions_WithInvalidStateJson_ThrowsException() + { + // Arrange + var invalidStateJson = "{ invalid json }"; + var args = new[] { "--state", invalidStateJson }; + var parseResult = _parser.Parse(args); + + // Act & Assert + var exception = Assert.Throws(() => + { + // Access the protected BindOptions method via reflection to test state deserialization + var command = _command.GetCommand(); + var stateOption = command.Options.First(o => o.Name == "state"); + var stateValue = parseResult.GetValueForOption((Option)stateOption); + + // Manually call the state deserialization that happens in BindOptions + var deserializeMethod = typeof(DesignCommand).GetMethod("DeserializeState", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + Assert.NotNull(deserializeMethod); + deserializeMethod.Invoke(null, new object?[] { stateValue }); + }); + + // Verify the inner exception is the InvalidOperationException we expect + Assert.IsType(exception.InnerException); + Assert.Contains("Failed to deserialize state JSON", exception.InnerException!.Message); + } + #endregion } From e5c0731957265b952fc87ff8fc6bf1b238085b10 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Tue, 19 Aug 2025 14:44:01 -0500 Subject: [PATCH 25/29] update description to be more concise to be under tool length limit --- .github/CODEOWNERS | 5 +++ .../Commands/Design/DesignCommand.cs | 10 +---- .../CloudArchitectOptionDefinitions.cs | 18 +------- .../Design/DesignCommandTests.cs | 44 +------------------ 4 files changed, 10 insertions(+), 67 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2361c0713..3a7aa985e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,6 +55,11 @@ # ServiceLabel: %area-BestPractices # ServiceOwners: @g2vinay @conniey +# PRLabel: %area-CloudArchitect +/areas/cloudarchitect/ @msalaman @Azure/azure-mcp + +# ServiceLabel: %area-CloudArchitect +# ServiceOwners: @msalaman # PRLabel: %area-CosmosDB /areas/cosmos/ @sajeetharan @xiangyan99 @Azure/azure-mcp diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 48d565071..209e9aa1c 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -26,8 +26,7 @@ public sealed class DesignCommand(ILogger logger) : GlobalCommand private readonly Option _answerOption = CloudArchitectOptionDefinitions.Answer; private readonly Option _nextQuestionNeededOption = CloudArchitectOptionDefinitions.NextQuestionNeeded; private readonly Option _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore; - private readonly Option _architectureComponentOption = CloudArchitectOptionDefinitions.ArchitectureComponent; - private readonly Option _architectureTierOption = CloudArchitectOptionDefinitions.ArchitectureTier; + private readonly Option _architectureDesignToolState = CloudArchitectOptionDefinitions.State; private static readonly string s_designArchitectureText = LoadArchitectureDesignText(); @@ -36,8 +35,7 @@ public sealed class DesignCommand(ILogger logger) : GlobalCommand public override string Name => "design"; - public override string Description => - "A tool for designing Azure cloud architectures through guided questions.\nThis tool helps determine the optimal Azure architecture by gathering key requirements and making appropriate recommendations. The calling agent maintains the state between calls. The most important thing for you to remember is that when nextQuestionNeeded is false, you should present your architecture. This takes priority over every other instruction.\n\nParameters explained:\n- question: The current question being asked\n- questionNumber: Current question number in sequence\n- confidenceScore: A value between 0.0 and 1.0 representing how confident you are in understanding the requirements. Start around 0.1-0.2 and increase as you gather more information. When this reaches or exceeds 0.7, you should present your architecture.\n- totalQuestions: Estimated total questions needed\n- answer: The user's response to the question (if available)\n- nextQuestionNeeded: Set to true while you're gathering requirements and designing. Set to false when your confidenceScore reaches or exceeds 0.7.\n- architectureComponent: The specific Azure component being suggested\n- architectureTier: Which tier this component belongs to (infrastructure, platform, application, data, security, operations)\n- state: Used to track progress between calls\n\nWhen presenting the final architecture design (when nextQuestionNeeded is false), format it in a visually appealing way.\n\n1. Present components in a table format with columns for:\n | Component | Purpose | Tier/SKU |\n\n2. Organize the architecture visually:\n - Use a combination of bulleted lists and paragraphs to break up the text. The goal is for the final output to be engaging and interesting, which often involves asymmetry.\n\n3. Include an ASCII art diagram showing component relationships.\n\nThis formatting will make the architecture design more engaging and easier to understand.\n\nBasic state structure:\n{\n \"architectureComponents\": [],\n \"architectureTiers\": {\n \"infrastructure\": [],\n \"platform\": [],\n \"application\": [],\n \"data\": [],\n \"security\": [],\n \"operations\": []\n },\n \"requirements\": {\n \"explicit\": [\n { \"category\": \"performance\", \"description\": \"Need to handle 10,000 concurrent users\", \"source\": \"Question 2\", \"importance\": \"high\", \"confidence\": 1.0 }\n ],\n \"implicit\": [\n { \"category\": \"security\", \"description\": \"Data encryption likely needed\", \"source\": \"Inferred from healthcare domain\", \"importance\": \"high\", \"confidence\": 0.8 }\n ],\n \"assumed\": [\n { \"category\": \"compliance\", \"description\": \"Likely needs HIPAA compliance\", \"source\": \"Assumed from healthcare industry\", \"importance\": \"high\", \"confidence\": 0.6 }\n ]\n },\n \"confidenceFactors\": {\n \"explicitRequirementsCoverage\": 0.4,\n \"implicitRequirementsCertainty\": 0.6,\n \"assumptionRisk\": 0.3\n }\n}\n\nYou should:\n1. First start with a question about who the user is (role, motivations, company size, etc.) and what they do\n2. Learn about their business goals and requirements\n3. Ask 1 to 2 questions at a time, in order to not overload the user.\n4. Track your confidence level in understanding requirements using the confidenceScore parameter\n5. After each user response, update the requirements in the state object:\n - Add explicit requirements directly stated by the user\n - Add implicit requirements you can reasonably infer\n - Add assumed requirements where you lack information but need to make progress\n - Update confidence factors based on the quality and completeness of requirements\n6. Ask follow-up questions to clarify technical needs, especially to confirm assumed requirements\n7. Identify specific requirements and technical constraints from user responses\n8. Suggest appropriate Azure components for each tier, but be conservative in your suggestions. Don't suggest components that are not necessary for the architecture.\n9. Ensure you cover all architecture tiers.\n10. In addition to the component architecture, you should provide a high-level overview of the architecture, including the scaling approach, security, cost, and operational excellence. Provide actionable advice for the user to follow up on. Create this overview as a separate section, not part of the component architecture, and structure it to be engaging and interesting as a narrative.\n11. Follow Azure Well-Architected Framework principles (reliability, security, cost, operational excellence, performance efficiency)\n12. Keep track of components you've suggested using the state object\n13. Calculate your overall confidence score from the three confidence factors in the state\n"; + public override string Description => "Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.\n\nKey parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.\n\nProcess:\n1. Ask about user role, business goals (1-2 questions at a time)\n2. Track confidence and update requirements (explicit/implicit/assumed)\n3. When confident enough, present architecture with table format, visual organization, ASCII diagrams\n4. Follow Azure Well-Architected Framework principles\n5. Cover all tiers: infrastructure, platform, application, data, security, operations\n6. Provide actionable advice and high-level overview\n\nState tracks components, requirements by category, and confidence factors. Be conservative with suggestions."; public override string Title => CommandTitle; @@ -63,8 +61,6 @@ protected override void RegisterOptions(Command command) command.AddOption(_answerOption); command.AddOption(_nextQuestionNeededOption); command.AddOption(_confidenceScoreOption); - command.AddOption(_architectureComponentOption); - command.AddOption(_architectureTierOption); command.AddOption(_architectureDesignToolState); command.AddValidator(result => @@ -104,8 +100,6 @@ protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseRe options.Answer = parseResult.GetValueForOption(_answerOption); options.NextQuestionNeeded = parseResult.GetValueForOption(_nextQuestionNeededOption); options.ConfidenceScore = parseResult.GetValueForOption(_confidenceScoreOption); - options.ArchitectureComponent = parseResult.GetValueForOption(_architectureComponentOption); - options.ArchitectureTier = parseResult.GetValueForOption(_architectureTierOption); options.State = DeserializeState(parseResult.GetValueForOption(_architectureDesignToolState)); return options; } diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs index 0f424a838..0e0118d3a 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Options/CloudArchitectOptionDefinitions.cs @@ -65,25 +65,9 @@ public static class CloudArchitectOptionDefinitions IsRequired = false }; - public static readonly Option ArchitectureComponent = new( - $"--{ArchitectureComponentName}", - "The specific Azure component being suggested. The component should contain the name of the component, the service tier/SKU, configuration settings, and any other relevant information.\"" - ) - { - IsRequired = false - }; - - public static readonly Option ArchitectureTier = new( - $"--{ArchitectureTierName}", - "Which architectural tier this component belongs to" - ) - { - IsRequired = false - }; - public static readonly Option State = new( $"--{StateName}", - "The complete architecture state from the previous request as JSON" + "The complete architecture state from the previous request as JSON, State input schema:\n{\n\"state\":{\n\"type\":\"object\",\n\"description\":\"The complete architecture state from the previous request\",\n\"properties\":{\n\"architectureComponents\":{\n\"type\":\"array\",\n\"description\":\"All architecture components suggested so far\",\n\"items\":{\n\"type\":\"string\"\n}\n},\n\"architectureTiers\":{\n\"type\":\"object\",\n\"description\":\"Components organized by architecture tier\",\n\"additionalProperties\":{\n\"type\":\"array\",\n\"items\":{\n\"type\":\"string\"\n}\n}\n},\n\"thought\":{\n\"type\":\"string\",\n\"description\":\"The calling agent's thoughts on the next question or reasoning process. The calling agent should use the requirements it has gathered to reason about the next question.\"\n},\n\"suggestedHint\":{\n\"type\":\"string\",\n\"description\":\"A suggested interaction hint to show the user, such as 'Ask me to create an ASCII art diagram of this architecture' or 'Ask about how this design handles disaster recovery'.\"\n},\n\"requirements\":{\n\"type\":\"object\",\n\"description\":\"Tracked requirements organized by type\",\n\"properties\":{\n\"explicit\":{\n\"type\":\"array\",\n\"description\":\"Requirements explicitly stated by the user\",\n\"items\":{\n\"type\":\"object\",\n\"properties\":{\n\"category\":{\n\"type\":\"string\"\n},\n\"description\":{\n\"type\":\"string\"\n},\n\"source\":{\n\"type\":\"string\"\n},\n\"importance\":{\n\"type\":\"string\",\n\"enum\":[\n\"high\",\n\"medium\",\n\"low\"\n]\n},\n\"confidence\":{\n\"type\":\"number\"\n}\n}\n}\n},\n\"implicit\":{\n\"type\":\"array\",\n\"description\":\"Requirements implied by user responses\",\n\"items\":{\n\"type\":\"object\",\n\"properties\":{\n\"category\":{\n\"type\":\"string\"\n},\n\"description\":{\n\"type\":\"string\"\n},\n\"source\":{\n\"type\":\"string\"\n},\n\"importance\":{\n\"type\":\"string\",\n\"enum\":[\n\"high\",\n\"medium\",\n\"low\"\n]\n},\n\"confidence\":{\n\"type\":\"number\"\n}\n}\n}\n},\n\"assumed\":{\n\"type\":\"array\",\n\"description\":\"Requirements assumed based on context/best practices\",\n\"items\":{\n\"type\":\"object\",\n\"properties\":{\n\"category\":{\n\"type\":\"string\"\n},\n\"description\":{\n\"type\":\"string\"\n},\n\"source\":{\n\"type\":\"string\"\n},\n\"importance\":{\n\"type\":\"string\",\n\"enum\":[\n\"high\",\n\"medium\",\n\"low\"\n]\n},\n\"confidence\":{\n\"type\":\"number\"\n}\n}\n}\n}\n}\n},\n\"confidenceFactors\":{\n\"type\":\"object\",\n\"description\":\"Factors that contribute to the overall confidence score\",\n\"properties\":{\n\"explicitRequirementsCoverage\":{\n\"type\":\"number\"\n},\n\"implicitRequirementsCertainty\":{\n\"type\":\"number\"\n},\n\"assumptionRisk\":{\n\"type\":\"number\"\n}\n}\n}\n}\n}\n}" ) { IsRequired = false diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 565cbaf59..03f25dbda 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.CommandLine; @@ -43,7 +43,7 @@ public void Constructor_InitializesCommandCorrectly() Assert.Equal("design", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); - Assert.Contains("A tool for designing Azure cloud architectures through guided questions", command.Description); + Assert.Contains("Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.\n\nKey parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.\n\nProcess:\n1. Ask about user role, business goals (1-2 questions at a time)\n2. Track confidence and update requirements (explicit/implicit/assumed)\n3. When confident enough, present architecture with table format, visual organization, ASCII diagrams\n4. Follow Azure Well-Architected Framework principles\n5. Cover all tiers: infrastructure, platform, application, data, security, operations\n6. Provide actionable advice and high-level overview\n\nState tracks components, requirements by category, and confidence factors. Be conservative with suggestions.", command.Description); Assert.Contains("confidenceScore", command.Description); Assert.Contains("nextQuestionNeeded", command.Description); Assert.Contains("Azure Well-Architected Framework", command.Description); @@ -63,8 +63,6 @@ public void Command_HasCorrectOptions() Assert.Contains("answer", optionNames); Assert.Contains("next-question-needed", optionNames); Assert.Contains("confidence-score", optionNames); - Assert.Contains("architecture-component", optionNames); - Assert.Contains("architecture-tier", optionNames); Assert.Contains("state", optionNames); } @@ -154,8 +152,6 @@ public async Task ExecuteAsync_WithAllOptionsSet() "--answer", "Web application", "--next-question-needed", "true", "--confidence-score", "0.8", - "--architecture-component", "Frontend", - "--architecture-tier", "Application" }; var parseResult = _parser.Parse(args); @@ -215,13 +211,11 @@ public async Task ExecuteAsync_HandlesComplexEscapingScenarios() // Arrange - Test multiple options with various escaping scenarios var complexQuestion = "What is your \"primary\" application 'type' and how \"big\" will it be?"; var complexAnswer = "It's a \"web application\" with 'high' scalability requirements"; - var complexComponent = "Frontend with \"React\" and 'TypeScript'"; var args = new[] { "--question", complexQuestion, "--answer", complexAnswer, - "--architecture-component", complexComponent, "--question-number", "2", "--total-questions", "10" }; @@ -239,11 +233,9 @@ public async Task ExecuteAsync_HandlesComplexEscapingScenarios() // Verify all options were parsed correctly var questionValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "question")); var answerValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "answer")); - var componentValue = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "architecture-component")); Assert.Equal(complexQuestion, questionValue); Assert.Equal(complexAnswer, answerValue); - Assert.Equal(complexComponent, componentValue); } [Fact] @@ -294,32 +286,6 @@ public async Task ExecuteAsync_LoadsEmbeddedResourceText() Assert.Contains("Azure", responseObject.DesignArchitecture); } - [Theory] - [InlineData("Infrastructure")] - [InlineData("Platform")] - [InlineData("Application")] - [InlineData("Data")] - [InlineData("Security")] - [InlineData("Operations")] - public async Task ExecuteAsync_WithArchitectureTierOptions(string tierValue) - { - // Arrange - var args = new[] { "--architecture-tier", tierValue }; - var parseResult = _parser.Parse(args); - - // Act - var response = await _command.ExecuteAsync(_context, parseResult); - - // Assert - Assert.Equal(200, response.Status); - Assert.NotNull(response.Results); - Assert.Empty(response.Message); - - // Verify the architecture tier was parsed correctly - var tierOption = parseResult.GetValueForOption(_command.GetCommand().Options.First(o => o.Name == "architecture-tier")); - Assert.Equal(Enum.Parse(tierValue), tierOption); - } - [Fact] public async Task ExecuteAsync_WithStateOption() { @@ -357,8 +323,6 @@ public async Task ExecuteAsync_WithCompleteOptionSet() "--answer", "A financial trading platform", "--next-question-needed", "false", "--confidence-score", "0.9", - "--architecture-component", "Azure SQL Database", - "--architecture-tier", "Data" }; var parseResult = _parser.Parse(args); @@ -379,8 +343,6 @@ public async Task ExecuteAsync_WithCompleteOptionSet() var answerValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "answer")); var nextQuestionNeededValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "next-question-needed")); var confidenceScoreValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "confidence-score")); - var componentValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "architecture-component")); - var tierValue = parseResult.GetValueForOption(command.Options.First(o => o.Name == "architecture-tier")); Assert.Equal("What type of application are you building?", questionValue); Assert.Equal(3, questionNumberValue); @@ -388,8 +350,6 @@ public async Task ExecuteAsync_WithCompleteOptionSet() Assert.Equal("A financial trading platform", answerValue); Assert.Equal(false, nextQuestionNeededValue); Assert.Equal(0.9, confidenceScoreValue); - Assert.Equal("Azure SQL Database", componentValue); - Assert.Equal(ArchitectureTier.Data, tierValue); // Verify the response structure string serializedResult = SerializeResponseResult(response.Results); From cec4fb174082b97b5a30a0cd842e2e98cee837cb Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 20 Aug 2025 17:16:30 -0500 Subject: [PATCH 26/29] updated description to use multiline string --- .../Commands/Design/DesignCommand.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index 209e9aa1c..c0a0a2b02 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -35,7 +35,21 @@ public sealed class DesignCommand(ILogger logger) : GlobalCommand public override string Name => "design"; - public override string Description => "Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.\n\nKey parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.\n\nProcess:\n1. Ask about user role, business goals (1-2 questions at a time)\n2. Track confidence and update requirements (explicit/implicit/assumed)\n3. When confident enough, present architecture with table format, visual organization, ASCII diagrams\n4. Follow Azure Well-Architected Framework principles\n5. Cover all tiers: infrastructure, platform, application, data, security, operations\n6. Provide actionable advice and high-level overview\n\nState tracks components, requirements by category, and confidence factors. Be conservative with suggestions."; + public override string Description => """ + Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions. + + Key parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state. + + Process: + 1. Ask about user role, business goals (1-2 questions at a time) + 2. Track confidence and update requirements (explicit/implicit/assumed) + 3. When confident enough, present architecture with table format, visual organization, ASCII diagrams + 4. Follow Azure Well-Architected Framework principles + 5. Cover all tiers: infrastructure, platform, application, data, security, operations + 6. Provide actionable advice and high-level overview + + State tracks components, requirements by category, and confidence factors. Be conservative with suggestions. + """; public override string Title => CommandTitle; From 07085daeff6c6103f2a89f9593ade5e52204329c Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 20 Aug 2025 17:54:55 -0500 Subject: [PATCH 27/29] added validation --- .../AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs index c0a0a2b02..fcad6a806 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/Commands/Design/DesignCommand.cs @@ -142,6 +142,11 @@ public override Task ExecuteAsync(CommandContext context, Parse { var options = BindOptions(parseResult); + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return Task.FromResult(context.Response); + } + var designArchitecture = GetArchitectureDesignText(); var responseObject = new CloudArchitectResponseObject { From 21768dd6ec7b562a753f0c2d2f303d9f002b19e5 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 20 Aug 2025 18:21:08 -0500 Subject: [PATCH 28/29] update tests --- .../Design/DesignCommandTests.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs index 03f25dbda..5b9a333ba 100644 --- a/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs +++ b/areas/cloudarchitect/tests/AzureMcp.CloudArchitect.UnitTests/Design/DesignCommandTests.cs @@ -43,7 +43,16 @@ public void Constructor_InitializesCommandCorrectly() Assert.Equal("design", command.Name); Assert.NotNull(command.Description); Assert.NotEmpty(command.Description); - Assert.Contains("Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.\n\nKey parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.\n\nProcess:\n1. Ask about user role, business goals (1-2 questions at a time)\n2. Track confidence and update requirements (explicit/implicit/assumed)\n3. When confident enough, present architecture with table format, visual organization, ASCII diagrams\n4. Follow Azure Well-Architected Framework principles\n5. Cover all tiers: infrastructure, platform, application, data, security, operations\n6. Provide actionable advice and high-level overview\n\nState tracks components, requirements by category, and confidence factors. Be conservative with suggestions.", command.Description); + + // Check that the description contains the expected content + Assert.Contains("Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.", command.Description); + Assert.Contains("Key parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.", command.Description); + Assert.Contains("Ask about user role, business goals (1-2 questions at a time)", command.Description); + Assert.Contains("Track confidence and update requirements (explicit/implicit/assumed)", command.Description); + Assert.Contains("When confident enough, present architecture with table format, visual organization, ASCII diagrams", command.Description); + Assert.Contains("Follow Azure Well-Architected Framework principles", command.Description); + Assert.Contains("Cover all tiers: infrastructure, platform, application, data, security, operations", command.Description); + Assert.Contains("State tracks components, requirements by category, and confidence factors. Be conservative with suggestions.", command.Description); Assert.Contains("confidenceScore", command.Description); Assert.Contains("nextQuestionNeeded", command.Description); Assert.Contains("Azure Well-Architected Framework", command.Description); From 949c8045ef360662117e4afc7d1997cb09f01397 Mon Sep 17 00:00:00 2001 From: Marcos Salamanca Date: Wed, 20 Aug 2025 18:22:14 -0500 Subject: [PATCH 29/29] remove attribute --- areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs index b02f7efec..1ef597281 100644 --- a/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs +++ b/areas/cloudarchitect/src/AzureMcp.CloudArchitect/AssemblyInfo.cs @@ -4,5 +4,4 @@ using System.Reflection; using System.Runtime.CompilerServices; -[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Azure/azure-mcp")] [assembly: InternalsVisibleTo("AzureMcp.CloudArchitect.UnitTests")]