Skip to content

Commit cc8a886

Browse files
Add validation MCP tools
1 parent b59b093 commit cc8a886

File tree

8 files changed

+244
-34
lines changed

8 files changed

+244
-34
lines changed

src/Bicep.McpServer.UnitTests/BicepToolsTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Immutable;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Threading.Tasks;
67
using Bicep.Core.UnitTests.Assertions;
78
using Bicep.Core.UnitTests.Baselines;
89
using FluentAssertions;
@@ -67,4 +68,18 @@ public void GetBicepBestPractices_returns_best_practices_markdown()
6768
// Update this if the file content changes - it's just here as a sanity check to make sure we're decoding the content correctly
6869
expectedBestPractices.Should().StartWith("# Bicep best-practices");
6970
}
71+
72+
[TestMethod]
73+
public async Task GetBicepFileDiagnostics_returns_diagnostics()
74+
{
75+
var response = await tools.GetBicepFileDiagnostics("""
76+
var foo string = 123
77+
""");
78+
79+
response.Should().Be("""
80+
dummy:///DUMMY(1,5) : Warning no-unused-vars: Variable "foo" is declared but never used. [https://aka.ms/bicep/linter-diagnostics#no-unused-vars]
81+
dummy:///DUMMY(1,18) : Error BCP033: Expected a value of type "string" but the provided value is of type "123". [https://aka.ms/bicep/core-diagnostics#BCP033]
82+
83+
""");
84+
}
7085
}

src/Bicep.McpServer.UnitTests/ServerTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public async Task List_tools_returns_full_list_of_tools()
2525
tools.OrderBy(x => x.Name).Should().SatisfyRespectively(
2626
x => x.Name.Should().Be("get_az_resource_type_schema"),
2727
x => x.Name.Should().Be("get_bicep_best_practices"),
28+
x => x.Name.Should().Be("get_bicep_file_diagnostics"),
29+
x => x.Name.Should().Be("get_bicep_what_if_results"),
2830
x => x.Name.Should().Be("list_az_resource_types_for_provider"));
2931
}
3032
}

src/Bicep.McpServer.UnitTests/packages.lock.json

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,12 @@
107107
},
108108
"Azure.Core": {
109109
"type": "Transitive",
110-
"resolved": "1.44.1",
111-
"contentHash": "YyznXLQZCregzHvioip07/BkzjuWNXogJEVz9T5W6TwjNr17ax41YGzYMptlo2G10oLCuVPoyva62y0SIRDixg==",
110+
"resolved": "1.46.2",
111+
"contentHash": "HFcvd1besmgBFPIZ7iSFHZOgzGfHTZNTzG8gWYdIP8ZJQySrb+vAdArcmFw7je3kFRMDbbtMoWKNVGj2vvH1sw==",
112112
"dependencies": {
113-
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
114-
"System.ClientModel": "1.1.0",
115-
"System.Diagnostics.DiagnosticSource": "6.0.1",
116-
"System.Memory.Data": "6.0.0",
117-
"System.Numerics.Vectors": "4.5.0",
118-
"System.Text.Encodings.Web": "6.0.0",
119-
"System.Text.Json": "6.0.10",
120-
"System.Threading.Tasks.Extensions": "4.5.4"
113+
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
114+
"System.ClientModel": "1.4.2",
115+
"System.Memory.Data": "6.0.1"
121116
}
122117
},
123118
"Azure.Deployments.Core": {
@@ -453,6 +448,22 @@
453448
"resolved": "9.6.0",
454449
"contentHash": "xGO7rHg3qK8jRdriAxIrsH4voNemCf8GVmgdcPXI5gpZ6lZWqOEM4ZO8yfYxUmg7+URw2AY1h7Uc/H17g7X1Kw=="
455450
},
451+
"Microsoft.Extensions.Azure": {
452+
"type": "Transitive",
453+
"resolved": "1.12.0",
454+
"contentHash": "3sIvazPesPdEn5t2yT+pwVWKm1LyN/LB31Wh52giRNf2ckcKmaxW2KOA5uNZHSaqABhMDPAYGMeGBFQreZ7uUg==",
455+
"dependencies": {
456+
"Azure.Core": "1.46.2",
457+
"Azure.Identity": "1.13.1",
458+
"Microsoft.Extensions.Configuration": "8.0.0",
459+
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
460+
"Microsoft.Extensions.Configuration.Binder": "8.0.2",
461+
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
462+
"Microsoft.Extensions.Logging": "8.0.1",
463+
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
464+
"Microsoft.Extensions.Options": "8.0.2"
465+
}
466+
},
456467
"Microsoft.Extensions.Configuration": {
457468
"type": "Transitive",
458469
"resolved": "9.0.5",
@@ -1198,11 +1209,11 @@
11981209
},
11991210
"System.ClientModel": {
12001211
"type": "Transitive",
1201-
"resolved": "1.1.0",
1202-
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
1212+
"resolved": "1.4.2",
1213+
"contentHash": "goGitN7trB9hoQ01dIpxaSYcruI+lGt/xq471AUv8irFvsIX+4HCqk1pDT/4ZPTLmU6ZUuNzhCb4MJAIwG7+Uw==",
12031214
"dependencies": {
1204-
"System.Memory.Data": "1.0.2",
1205-
"System.Text.Json": "6.0.9"
1215+
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
1216+
"System.Memory.Data": "6.0.1"
12061217
}
12071218
},
12081219
"System.CodeDom": {
@@ -1832,6 +1843,7 @@
18321843
"type": "Project",
18331844
"dependencies": {
18341845
"Azure.Bicep.Core": "[1.0.0, )",
1846+
"Microsoft.Extensions.Azure": "[1.12.0, )",
18351847
"Microsoft.Extensions.Hosting": "[9.0.5, )",
18361848
"ModelContextProtocol": "[0.3.0-preview.2, )"
18371849
}

src/Bicep.McpServer/Bicep.McpServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ItemGroup>
1616
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
1717
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.2" />
18+
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.12.0" />
1819
</ItemGroup>
1920

2021
<ItemGroup>

src/Bicep.McpServer/BicepTools.cs

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Immutable;
45
using System.ComponentModel;
56
using System.Reflection;
7+
using System.Text;
68
using System.Text.Json;
79
using System.Text.Json.Serialization;
10+
using System.Threading.Tasks;
11+
using Azure;
12+
using Azure.ResourceManager;
13+
using Azure.ResourceManager.Resources;
14+
using Azure.ResourceManager.Resources.Models;
15+
using Bicep.Core;
16+
using Bicep.Core.Emit;
17+
using Bicep.Core.Extensions;
18+
using Bicep.Core.SourceGraph;
19+
using Bicep.Core.Text;
820
using Bicep.Core.TypeSystem.Providers.Az;
21+
using Bicep.IO.Abstraction;
922
using Bicep.McpServer.ResourceProperties;
1023
using Bicep.McpServer.ResourceProperties.Entities;
24+
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
1125
using ModelContextProtocol.Server;
26+
using Newtonsoft.Json.Linq;
1227

1328
namespace Bicep.McpServer;
1429

1530
[McpServerToolType]
1631
public sealed class BicepTools(
1732
AzResourceTypeLoader azResourceTypeLoader,
18-
ResourceVisitor resourceVisitor)
33+
ResourceVisitor resourceVisitor,
34+
ISourceFileFactory sourceFileFactory,
35+
BicepCompiler bicepCompiler,
36+
ArmClient armClient)
1937
{
2038
private static Lazy<BinaryData> BestPracticesMarkdownLazy { get; } = new(() =>
2139
BinaryData.FromStream(
@@ -76,4 +94,98 @@ public string GetAzResourceTypeSchema(
7694
This is helpful additional context if you've been asked to generate Bicep code.
7795
""")]
7896
public string GetBicepBestPractices() => BestPracticesMarkdownLazy.Value.ToString();
97+
98+
[McpServerTool(Title = "Get bicep file diagnostics", Destructive = false, Idempotent = true, OpenWorld = false, ReadOnly = true)]
99+
[Description("""
100+
Obtains diagnostics for a Bicep file.
101+
The diagnostics include errors, warnings, and other messages that can help identify issues in the Bicep code.
102+
The diagnostics are returned as a newline-separated list of messages, each containing the file name, line number, character position, severity level, code, and message.
103+
This can be helpful for assessing the accuracy of generated Bicep code an iterating on it to improve quality.
104+
""")]
105+
public async Task<string> GetBicepFileDiagnostics(
106+
[Description("The raw contents of the .bicep file")] string bicepContents)
107+
{
108+
var uri = new Uri("inmemory:///main.bicep");
109+
var bicepFile = sourceFileFactory.CreateBicepFile(uri, bicepContents);
110+
111+
var workspace = new Workspace();
112+
workspace.UpsertSourceFile(bicepFile);
113+
114+
var compilation = await bicepCompiler.CreateCompilation(bicepFile.Uri, workspace, skipRestore: true);
115+
var diagnostics = compilation.GetAllDiagnosticsByBicepFile()[bicepFile];
116+
117+
var sb = new StringBuilder();
118+
foreach (var diagnostic in diagnostics)
119+
{
120+
(var line, var character) = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, diagnostic.Span.Position);
121+
122+
// build a a code description link if the Uri is assigned
123+
var codeDescription = diagnostic.Uri == null ? string.Empty : $" [{diagnostic.Uri.AbsoluteUri}]";
124+
125+
var message = $"{bicepFile.FileHandle.Uri}({line + 1},{character + 1}) : {diagnostic.Level} {diagnostic.Code}: {diagnostic.Message}{codeDescription}";
126+
127+
sb.AppendLine(message);
128+
}
129+
130+
return sb.ToString();
131+
}
132+
133+
[McpServerTool(Title = "Get Bicep what-if results", Destructive = false, Idempotent = true, OpenWorld = true, ReadOnly = true)]
134+
[Description("""
135+
Runs live what-if analysis for a Bicep file.
136+
This tool allows you to see the potential changes that would be made by deploying a Bicep file without actually applying those changes.
137+
It provides a preview of the resources that would be created, updated, or deleted based on the current state of the Azure environment.
138+
This is useful for validating the impact of a Bicep deployment before executing it.
139+
""")]
140+
public async Task<string> GetBicepWhatIfResults(
141+
[Description("The Azure subscription Id, in Guid form. You must use a real value, do not supply a placeholder. You can ask the user or use other tools to obtain this in advance, if you don't already have it in context.")] string subscriptionId,
142+
[Description("The Azure resource group name. You must use a real value, do not supply a placeholder. You can ask the user or use other tools to obtain this in advance, if you don't already have it in context.")] string resourceGroupName,
143+
[Description("The fully-qualified path to a .bicep file on disk")] string pathToBicepFile,
144+
[Description("The deployment parameters, if required. They key of the dictionary is the name of the parameter to supply, and the value is the parameter value to use.")] ImmutableDictionary<string, object>? deploymentParameters = null)
145+
{
146+
deploymentParameters ??= ImmutableDictionary<string, object>.Empty;
147+
var fileUri = IOUri.FromLocalFilePath(pathToBicepFile);
148+
var compilation = await bicepCompiler.CreateCompilation(fileUri.ToUri(), skipRestore: true);
149+
150+
var sb = new StringBuilder();
151+
foreach (var (bicepFile, diagnostics) in compilation.GetAllDiagnosticsByBicepFile())
152+
{
153+
foreach (var diagnostic in diagnostics)
154+
{
155+
(var line, var character) = TextCoordinateConverter.GetPosition(bicepFile.LineStarts, diagnostic.Span.Position);
156+
157+
// build a a code description link if the Uri is assigned
158+
var codeDescription = diagnostic.Uri == null ? string.Empty : $" [{diagnostic.Uri.AbsoluteUri}]";
159+
160+
var message = $"{bicepFile.FileHandle.Uri}({line + 1},{character + 1}) : {diagnostic.Level} {diagnostic.Code}: {diagnostic.Message}{codeDescription}";
161+
162+
sb.AppendLine(message);
163+
}
164+
}
165+
166+
var result = compilation.Emitter.Template();
167+
168+
if (!result.Success || result.Template is null)
169+
{
170+
sb.AppendLine($"Bicep compilation failed. Correct the error diagnostics and try again.");
171+
return sb.ToString();
172+
}
173+
174+
var resourceGroupId = ResourceGroupResource.CreateResourceIdentifier(subscriptionId, resourceGroupName);
175+
var deploymentId = ArmDeploymentResource.CreateResourceIdentifier(resourceGroupId.ToString(), "main");
176+
177+
var deploymentContent = new ArmDeploymentWhatIfContent(new(ArmDeploymentMode.Incremental)
178+
{
179+
Template = BinaryData.FromString(result.Template),
180+
Parameters = BinaryData.FromString(deploymentParameters.ToDictionary(x => x.Key, x => new
181+
{
182+
Value = x.Value
183+
}).ToJson()),
184+
});
185+
186+
var deployment = armClient.GetArmDeploymentResource(deploymentId);
187+
var whatIfResults = await deployment.WhatIfAsync(WaitUntil.Completed, deploymentContent);
188+
189+
return whatIfResults.GetRawResponse().Content.ToString();
190+
}
79191
}
Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,73 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.IO.Abstractions;
45
using Azure.Bicep.Types.Az;
6+
using Azure.Identity;
7+
using Bicep.Core;
8+
using Bicep.Core.Analyzers.Interfaces;
9+
using Bicep.Core.Analyzers.Linter;
10+
using Bicep.Core.Configuration;
11+
using Bicep.Core.Features;
12+
using Bicep.Core.FileSystem;
13+
using Bicep.Core.Registry;
14+
using Bicep.Core.Registry.Auth;
15+
using Bicep.Core.Registry.Catalog.Implementation;
16+
using Bicep.Core.Semantics.Namespaces;
17+
using Bicep.Core.SourceGraph;
18+
using Bicep.Core.TypeSystem.Providers;
519
using Bicep.Core.TypeSystem.Providers.Az;
20+
using Bicep.Core.Utils;
21+
using Bicep.IO.Abstraction;
22+
using Bicep.IO.FileSystem;
623
using Bicep.McpServer.ResourceProperties;
24+
using Microsoft.Extensions.Azure;
725
using Microsoft.Extensions.DependencyInjection;
826
using Microsoft.Extensions.Logging;
927
using Microsoft.Extensions.Logging.Abstractions;
28+
using Environment = Bicep.Core.Utils.Environment;
29+
using LocalFileSystem = System.IO.Abstractions.FileSystem;
1030

1131
namespace Bicep.McpServer;
1232

1333
public static class IServiceCollectionExtensions
1434
{
15-
public static IServiceCollection AddMcpDependencies(this IServiceCollection services) => services
16-
.AddSingleton<ILogger<ResourceVisitor>>(NullLoggerFactory.Instance.CreateLogger<ResourceVisitor>())
17-
.AddSingleton<AzResourceTypeLoader>(provider => new(new AzTypeLoader()))
18-
.AddSingleton<ResourceVisitor>();
35+
public static IServiceCollection AddMcpDependencies(this IServiceCollection services)
36+
{
37+
services
38+
.AddSingleton<ILogger<ResourceVisitor>>(NullLoggerFactory.Instance.CreateLogger<ResourceVisitor>())
39+
.AddSingleton<AzResourceTypeLoader>(provider => new(new AzTypeLoader()))
40+
.AddSingleton<ResourceVisitor>()
41+
.AddBicepCore();
42+
43+
services.AddAzureClients(clientBuilder =>
44+
{
45+
clientBuilder.AddArmClient("00000000-0000-0000-0000-000000000000");
46+
clientBuilder.UseCredential(new DefaultAzureCredential());
47+
});
48+
49+
return services;
50+
}
51+
52+
53+
public static IServiceCollection AddBicepCore(this IServiceCollection services) => services
54+
.AddSingleton<INamespaceProvider, NamespaceProvider>()
55+
.AddSingleton<IResourceTypeProviderFactory, ResourceTypeProviderFactory>()
56+
.AddSingleton<IContainerRegistryClientFactory, ContainerRegistryClientFactory>()
57+
.AddSingleton<ITemplateSpecRepositoryFactory, TemplateSpecRepositoryFactory>()
58+
.AddSingleton<IModuleDispatcher, ModuleDispatcher>()
59+
.AddSingleton<IArtifactRegistryProvider, DefaultArtifactRegistryProvider>()
60+
.AddSingleton<ITokenCredentialFactory, TokenCredentialFactory>()
61+
.AddSingleton<IFileResolver, FileResolver>()
62+
.AddSingleton<IEnvironment, Environment>()
63+
.AddSingleton<IFileSystem, LocalFileSystem>()
64+
.AddSingleton<IFileExplorer, FileSystemFileExplorer>()
65+
.AddSingleton<IAuxiliaryFileCache, AuxiliaryFileCache>()
66+
.AddSingleton<IConfigurationManager, ConfigurationManager>()
67+
.AddSingleton<IBicepAnalyzer, LinterAnalyzer>()
68+
.AddSingleton<IFeatureProviderFactory, FeatureProviderFactory>()
69+
.AddSingleton<ILinterRulesProvider, LinterRulesProvider>()
70+
.AddSingleton<ISourceFileFactory, SourceFileFactory>()
71+
.AddRegistryCatalogServices()
72+
.AddSingleton<BicepCompiler>();
1973
}

src/Bicep.McpServer/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using Microsoft.Extensions.Azure;
45
using Microsoft.Extensions.Hosting;
56
using Microsoft.Extensions.DependencyInjection;
67
using Bicep.McpServer;
8+
using Azure.Identity;
79

810
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
911
builder.Services
@@ -12,4 +14,4 @@
1214
.WithStdioServerTransport()
1315
.WithTools<BicepTools>();
1416

15-
await builder.Build().RunAsync();
17+
await builder.Build().RunAsync();

0 commit comments

Comments
 (0)