|
1 | 1 | // Copyright (c) Microsoft Corporation.
|
2 | 2 | // Licensed under the MIT License.
|
3 | 3 |
|
| 4 | +using System.Collections.Immutable; |
4 | 5 | using System.ComponentModel;
|
5 | 6 | using System.Reflection;
|
| 7 | +using System.Text; |
6 | 8 | using System.Text.Json;
|
7 | 9 | 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; |
8 | 20 | using Bicep.Core.TypeSystem.Providers.Az;
|
| 21 | +using Bicep.IO.Abstraction; |
9 | 22 | using Bicep.McpServer.ResourceProperties;
|
10 | 23 | using Bicep.McpServer.ResourceProperties.Entities;
|
| 24 | +using Microsoft.WindowsAzure.ResourceStack.Common.Json; |
11 | 25 | using ModelContextProtocol.Server;
|
| 26 | +using Newtonsoft.Json.Linq; |
12 | 27 |
|
13 | 28 | namespace Bicep.McpServer;
|
14 | 29 |
|
15 | 30 | [McpServerToolType]
|
16 | 31 | public sealed class BicepTools(
|
17 | 32 | AzResourceTypeLoader azResourceTypeLoader,
|
18 |
| - ResourceVisitor resourceVisitor) |
| 33 | + ResourceVisitor resourceVisitor, |
| 34 | + ISourceFileFactory sourceFileFactory, |
| 35 | + BicepCompiler bicepCompiler, |
| 36 | + ArmClient armClient) |
19 | 37 | {
|
20 | 38 | private static Lazy<BinaryData> BestPracticesMarkdownLazy { get; } = new(() =>
|
21 | 39 | BinaryData.FromStream(
|
@@ -76,4 +94,98 @@ public string GetAzResourceTypeSchema(
|
76 | 94 | This is helpful additional context if you've been asked to generate Bicep code.
|
77 | 95 | """)]
|
78 | 96 | 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 | + } |
79 | 191 | }
|
0 commit comments