Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.

Commit f2de3a4

Browse files
authored
Migrate Cloud Architect Tool from @Azure to az mcp server (#890)
* added cloud architect tool * updates * updates * updates * updates * update Tests * update documentation * update cspell * updates * added more prompt examples * removed unnecessary service classes * fixed formatting, added unit tests for proper escape handling * update cspell value * update description * updated unit test to use new description * add e2e prompt for cloud architect tool * cleaned up unused models * binded and used architectureTier and architectureDesignToolState options * added parameter validations and removed unnecessary classes * removed unnecessary base class and JsonPropertyNames * fixed CHANGELOG link formatting * removed unused json context * fixed spelling error * Include ResponseObject of parameters in response for design command * update description to be more concise to be under tool length limit * updated description to use multiline string * added validation * update tests * remove attribute
1 parent eec5591 commit f2de3a4

28 files changed

+1451
-0
lines changed

‎.github/CODEOWNERS‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
# ServiceLabel: %area-BestPractices
5656
# ServiceOwners: @g2vinay @conniey
5757

58+
# PRLabel: %area-CloudArchitect
59+
/areas/cloudarchitect/ @msalaman @Azure/azure-mcp
60+
61+
# ServiceLabel: %area-CloudArchitect
62+
# ServiceOwners: @msalaman
5863

5964
# PRLabel: %area-CosmosDB
6065
/areas/cosmos/ @sajeetharan @xiangyan99 @Azure/azure-mcp

‎AzureMcp.sln‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.Core.UnitTests", "
295295
EndProject
296296
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.Tests", "core\tests\AzureMcp.Tests\AzureMcp.Tests.csproj", "{527FE0F6-40AE-4E71-A483-0F0A2368F2A7}"
297297
EndProject
298+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cloudarchitect", "cloudarchitect", "{3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32}"
299+
EndProject
300+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C239126A-8A2E-168D-6172-7C58CE7AEB0A}"
301+
EndProject
302+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.CloudArchitect", "areas\cloudarchitect\src\AzureMcp.CloudArchitect\AzureMcp.CloudArchitect.csproj", "{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}"
303+
EndProject
304+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{90811FCA-7295-E394-37C4-E1FD75D058A2}"
305+
EndProject
306+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.CloudArchitect.UnitTests", "areas\cloudarchitect\tests\AzureMcp.CloudArchitect.UnitTests\AzureMcp.CloudArchitect.UnitTests.csproj", "{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}"
298307
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureMcp.FunctionApp", "areas\functionapp\src\AzureMcp.FunctionApp\AzureMcp.FunctionApp.csproj", "{E6E10688-A3CD-4C33-8E13-E0E905329272}"
299308
EndProject
300309
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "functionapp", "functionapp", "{3310D97C-93BE-4434-BED7-81EB639B3141}"
@@ -1216,6 +1225,30 @@ Global
12161225
{527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x64.Build.0 = Release|Any CPU
12171226
{527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x86.ActiveCfg = Release|Any CPU
12181227
{527FE0F6-40AE-4E71-A483-0F0A2368F2A7}.Release|x86.Build.0 = Release|Any CPU
1228+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1229+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|Any CPU.Build.0 = Debug|Any CPU
1230+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x64.ActiveCfg = Debug|Any CPU
1231+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x64.Build.0 = Debug|Any CPU
1232+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x86.ActiveCfg = Debug|Any CPU
1233+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Debug|x86.Build.0 = Debug|Any CPU
1234+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|Any CPU.ActiveCfg = Release|Any CPU
1235+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|Any CPU.Build.0 = Release|Any CPU
1236+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x64.ActiveCfg = Release|Any CPU
1237+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x64.Build.0 = Release|Any CPU
1238+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x86.ActiveCfg = Release|Any CPU
1239+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439}.Release|x86.Build.0 = Release|Any CPU
1240+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1241+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|Any CPU.Build.0 = Debug|Any CPU
1242+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x64.ActiveCfg = Debug|Any CPU
1243+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x64.Build.0 = Debug|Any CPU
1244+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x86.ActiveCfg = Debug|Any CPU
1245+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Debug|x86.Build.0 = Debug|Any CPU
1246+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|Any CPU.ActiveCfg = Release|Any CPU
1247+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|Any CPU.Build.0 = Release|Any CPU
1248+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x64.ActiveCfg = Release|Any CPU
1249+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x64.Build.0 = Release|Any CPU
1250+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x86.ActiveCfg = Release|Any CPU
1251+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559}.Release|x86.Build.0 = Release|Any CPU
12191252
{E6E10688-A3CD-4C33-8E13-E0E905329272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12201253
{E6E10688-A3CD-4C33-8E13-E0E905329272}.Debug|Any CPU.Build.0 = Debug|Any CPU
12211254
{E6E10688-A3CD-4C33-8E13-E0E905329272}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1581,6 +1614,11 @@ Global
15811614
{2A3CD1B4-38A3-46A1-AEDC-2C2AC47CB8F1} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA}
15821615
{1AE3FC50-8E8C-4637-AAB1-A871D5FB4535} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA}
15831616
{527FE0F6-40AE-4E71-A483-0F0A2368F2A7} = {8783C0BC-EE27-8E0C-7452-5882FB8E96CA}
1617+
{3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32} = {87783708-79E3-AD60-C783-1D52BE7DE4BB}
1618+
{C239126A-8A2E-168D-6172-7C58CE7AEB0A} = {3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32}
1619+
{6250B2F1-D4C1-4B4D-B15E-F65CEDB05439} = {C239126A-8A2E-168D-6172-7C58CE7AEB0A}
1620+
{90811FCA-7295-E394-37C4-E1FD75D058A2} = {3C677F0E-A3E3-0F75-DBE2-C3DFA8463D32}
1621+
{FD8691F1-BFFA-403E-8E1C-4ED6ABB5B559} = {90811FCA-7295-E394-37C4-E1FD75D058A2}
15841622
{E6E10688-A3CD-4C33-8E13-E0E905329272} = {4A85B59D-2802-46D2-B9D1-CDFE11A37945}
15851623
{3310D97C-93BE-4434-BED7-81EB639B3141} = {87783708-79E3-AD60-C783-1D52BE7DE4BB}
15861624
{4A85B59D-2802-46D2-B9D1-CDFE11A37945} = {3310D97C-93BE-4434-BED7-81EB639B3141}

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The Azure MCP Server updates automatically by default whenever a new release com
3535
- `azmcp-azuremanagedlustre-filesystem-list`: List available Azure Managed Lustre filesystem. [[#1001](https://github.com/Azure/azure-mcp/issues/1001)]
3636
- `azmcp-azuremanagedlustre-filesystem-required-subnet-size`: Returns the number of IP addresses required for a specific SKU and size of Azure Managed Lustre filesystem. [[#1002](https://github.com/Azure/azure-mcp/issues/1002)]
3737

38+
- Added new command for designing Azure Cloud Architecture through guided questions. [[#890](https://github.com/Azure/azure-mcp/pull/890)]
3839
- Added support for the following Azure Deploy and Azure Quota operations: [[#626](https://github.com/Azure/azure-mcp/pull/626)]
3940
- `azmcp_deploy_app_logs_get` - Get logs from Azure applications deployed using azd.
4041
- `azmcp_deploy_iac_rules_get` - Get Infrastructure as Code rules.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
303303

304304
* Get the Bicep schema for specific Azure resource types
305305

306+
### 🏗️ Cloud Architect
307+
308+
* Design Azure cloud architectures through guided questions
309+
306310
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).
307311

308312
</details>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Reflection;
5+
using System.Runtime.CompilerServices;
6+
7+
[assembly: InternalsVisibleTo("AzureMcp.CloudArchitect.UnitTests")]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<IsAotCompatible>true</IsAotCompatible>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
<EmbeddedResource Include="**\Resources\*.txt" />
7+
<EmbeddedResource Include="**\Resources\*.json" />
8+
</ItemGroup>
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\..\..\core\src\AzureMcp.Core\AzureMcp.Core.csproj" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
14+
<PackageReference Include="ModelContextProtocol" />
15+
<PackageReference Include="System.CommandLine" />
16+
</ItemGroup>
17+
<ItemGroup>
18+
<Folder Include="Models\" />
19+
</ItemGroup>
20+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using AzureMcp.CloudArchitect.Commands.Design;
6+
using AzureMcp.CloudArchitect.Models;
7+
using AzureMcp.CloudArchitect.Options;
8+
9+
namespace AzureMcp.CloudArchitect;
10+
11+
[JsonSourceGenerationOptions(
12+
WriteIndented = true,
13+
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
14+
PropertyNameCaseInsensitive = true)]
15+
[JsonSerializable(typeof(CloudArchitectResponseObject))]
16+
[JsonSerializable(typeof(CloudArchitectDesignResponse))]
17+
[JsonSerializable(typeof(ArchitectureDesignToolState))]
18+
[JsonSerializable(typeof(ArchitectureDesignTiers))]
19+
[JsonSerializable(typeof(ArchitectureDesignRequirements))]
20+
[JsonSerializable(typeof(ArchitectureDesignRequirement))]
21+
[JsonSerializable(typeof(ArchitectureDesignConfidenceFactors))]
22+
[JsonSerializable(typeof(RequirementImportance))]
23+
public partial class CloudArchitectJsonContext : JsonSerializerContext
24+
{
25+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using AzureMcp.CloudArchitect.Commands.Design;
5+
using AzureMcp.Core.Areas;
6+
using AzureMcp.Core.Commands;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace AzureMcp.CloudArchitect;
11+
12+
public class CloudArchitectSetup : IAreaSetup
13+
{
14+
public void ConfigureServices(IServiceCollection services)
15+
{
16+
}
17+
18+
public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory)
19+
{
20+
// Create CloudArchitect command group
21+
var cloudArchitect = new CommandGroup("cloudarchitect", "Cloud Architecture operations - Commands for generating Azure architecture designs and recommendations based on requirements.");
22+
rootGroup.AddSubGroup(cloudArchitect);
23+
24+
// Register CloudArchitect commands
25+
cloudArchitect.AddCommand("design", new DesignCommand(
26+
loggerFactory.CreateLogger<DesignCommand>()));
27+
}
28+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.CommandLine;
5+
using System.CommandLine.Parsing;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Reflection;
8+
using System.Text.Json;
9+
using AzureMcp.CloudArchitect.Models;
10+
using AzureMcp.CloudArchitect.Options;
11+
using AzureMcp.Core.Commands;
12+
using AzureMcp.Core.Helpers;
13+
using AzureMcp.Core.Models;
14+
using Microsoft.Extensions.Logging;
15+
16+
namespace AzureMcp.CloudArchitect.Commands.Design;
17+
18+
public sealed class DesignCommand(ILogger<DesignCommand> logger) : GlobalCommand<ArchitectureDesignToolOptions>
19+
{
20+
private const string CommandTitle = "Design Azure cloud architectures through guided questions";
21+
private readonly ILogger<DesignCommand> _logger = logger;
22+
23+
private readonly Option<string> _questionOption = CloudArchitectOptionDefinitions.Question;
24+
private readonly Option<int> _questionNumberOption = CloudArchitectOptionDefinitions.QuestionNumber;
25+
private readonly Option<int> _questionTotalQuestions = CloudArchitectOptionDefinitions.TotalQuestions;
26+
private readonly Option<string> _answerOption = CloudArchitectOptionDefinitions.Answer;
27+
private readonly Option<bool> _nextQuestionNeededOption = CloudArchitectOptionDefinitions.NextQuestionNeeded;
28+
private readonly Option<double> _confidenceScoreOption = CloudArchitectOptionDefinitions.ConfidenceScore;
29+
30+
private readonly Option<string> _architectureDesignToolState = CloudArchitectOptionDefinitions.State;
31+
32+
private static readonly string s_designArchitectureText = LoadArchitectureDesignText();
33+
34+
private static string GetArchitectureDesignText() => s_designArchitectureText;
35+
36+
public override string Name => "design";
37+
38+
public override string Description => """
39+
Azure architecture design tool that gathers requirements through guided questions and recommends optimal solutions.
40+
41+
Key parameters: question, questionNumber, confidenceScore (0.0-1.0, present architecture when ≥0.7), totalQuestions, answer, nextQuestionNeeded, architectureComponent, architectureTier, state.
42+
43+
Process:
44+
1. Ask about user role, business goals (1-2 questions at a time)
45+
2. Track confidence and update requirements (explicit/implicit/assumed)
46+
3. When confident enough, present architecture with table format, visual organization, ASCII diagrams
47+
4. Follow Azure Well-Architected Framework principles
48+
5. Cover all tiers: infrastructure, platform, application, data, security, operations
49+
6. Provide actionable advice and high-level overview
50+
51+
State tracks components, requirements by category, and confidence factors. Be conservative with suggestions.
52+
""";
53+
54+
public override string Title => CommandTitle;
55+
56+
public override ToolMetadata Metadata => new()
57+
{
58+
Destructive = false,
59+
ReadOnly = true
60+
};
61+
62+
private static string LoadArchitectureDesignText()
63+
{
64+
Assembly assembly = typeof(DesignCommand).Assembly;
65+
string resourceName = EmbeddedResourceHelper.FindEmbeddedResource(assembly, "azure-architecture-design.txt");
66+
return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName);
67+
}
68+
69+
protected override void RegisterOptions(Command command)
70+
{
71+
base.RegisterOptions(command);
72+
command.AddOption(_questionOption);
73+
command.AddOption(_questionNumberOption);
74+
command.AddOption(_questionTotalQuestions);
75+
command.AddOption(_answerOption);
76+
command.AddOption(_nextQuestionNeededOption);
77+
command.AddOption(_confidenceScoreOption);
78+
command.AddOption(_architectureDesignToolState);
79+
80+
command.AddValidator(result =>
81+
{
82+
// Validate confidence score is between 0.0 and 1.0
83+
var confidenceScore = result.GetValueForOption(_confidenceScoreOption);
84+
if (confidenceScore < 0.0 || confidenceScore > 1.0)
85+
{
86+
result.ErrorMessage = "Confidence score must be between 0.0 and 1.0";
87+
return;
88+
}
89+
90+
// Validate question number is not negative
91+
var questionNumber = result.GetValueForOption(_questionNumberOption);
92+
if (questionNumber < 0)
93+
{
94+
result.ErrorMessage = "Question number cannot be negative";
95+
return;
96+
}
97+
98+
// Validate total questions is not negative
99+
var totalQuestions = result.GetValueForOption(_questionTotalQuestions);
100+
if (totalQuestions < 0)
101+
{
102+
result.ErrorMessage = "Total questions cannot be negative";
103+
return;
104+
}
105+
});
106+
}
107+
108+
protected override ArchitectureDesignToolOptions BindOptions(ParseResult parseResult)
109+
{
110+
var options = base.BindOptions(parseResult);
111+
options.Question = parseResult.GetValueForOption(_questionOption) ?? string.Empty;
112+
options.QuestionNumber = parseResult.GetValueForOption(_questionNumberOption);
113+
options.TotalQuestions = parseResult.GetValueForOption(_questionTotalQuestions);
114+
options.Answer = parseResult.GetValueForOption(_answerOption);
115+
options.NextQuestionNeeded = parseResult.GetValueForOption(_nextQuestionNeededOption);
116+
options.ConfidenceScore = parseResult.GetValueForOption(_confidenceScoreOption);
117+
options.State = DeserializeState(parseResult.GetValueForOption(_architectureDesignToolState));
118+
return options;
119+
}
120+
121+
private static ArchitectureDesignToolState DeserializeState(string? stateJson)
122+
{
123+
if (string.IsNullOrEmpty(stateJson))
124+
{
125+
return new ArchitectureDesignToolState();
126+
}
127+
128+
try
129+
{
130+
var state = JsonSerializer.Deserialize<ArchitectureDesignToolState>(stateJson, CloudArchitectJsonContext.Default.ArchitectureDesignToolState);
131+
return state ?? new ArchitectureDesignToolState();
132+
}
133+
catch (JsonException ex)
134+
{
135+
throw new InvalidOperationException($"Failed to deserialize state JSON: {ex.Message}", ex);
136+
}
137+
}
138+
139+
public override Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
140+
{
141+
try
142+
{
143+
var options = BindOptions(parseResult);
144+
145+
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
146+
{
147+
return Task.FromResult(context.Response);
148+
}
149+
150+
var designArchitecture = GetArchitectureDesignText();
151+
var responseObject = new CloudArchitectResponseObject
152+
{
153+
DisplayText = options.Question,
154+
DisplayThought = options.State.Thought,
155+
DisplayHint = options.State.SuggestedHint,
156+
QuestionNumber = options.QuestionNumber,
157+
TotalQuestions = options.TotalQuestions,
158+
NextQuestionNeeded = options.NextQuestionNeeded,
159+
State = options.State
160+
};
161+
162+
var result = new CloudArchitectDesignResponse
163+
{
164+
DesignArchitecture = designArchitecture,
165+
ResponseObject = responseObject
166+
};
167+
168+
context.Response.Status = 200;
169+
context.Response.Results = ResponseResult.Create(result, CloudArchitectJsonContext.Default.CloudArchitectDesignResponse);
170+
context.Response.Message = string.Empty;
171+
}
172+
catch (Exception ex)
173+
{
174+
_logger.LogError(ex, "An exception occurred in cloud architect design command");
175+
HandleException(context, ex);
176+
}
177+
return Task.FromResult(context.Response);
178+
}
179+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
global using System.CommandLine;
5+
global using System.CommandLine.Parsing;
6+
global using System.Text.Json;
7+
global using System.Text.Json.Serialization;
8+
global using AzureMcp.Core.Extensions;
9+
global using AzureMcp.Core.Models;
10+
global using AzureMcp.Core.Models.Command;
11+
global using ModelContextProtocol.Server;

0 commit comments

Comments
 (0)