Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
"version": "10.0.203",
"rollForward": "latestPatch"
}
}
}
98 changes: 74 additions & 24 deletions src/Bicep.McpServer.Core/BicepCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using System.Collections.Immutable;
using System.ComponentModel;
using Bicep.Core;
using Bicep.Core.Diagnostics;
using Bicep.Core.Extensions;
using Bicep.Core.PrettyPrintV2;
using Bicep.Core.SourceGraph;
using Bicep.IO.Abstraction;
using ModelContextProtocol.Server;

Expand Down Expand Up @@ -39,40 +41,76 @@ public record GetFileReferencesResult(
[Description("The list of files being referenced by the specified Bicep or Bicep parameters file")]
ImmutableArray<Uri> FileUris);

[McpServerTool(Title = "Get Bicep File Diagnostics", Destructive = false, Idempotent = true, OpenWorld = true, ReadOnly = true, UseStructuredContent = true)]
public record BuildBicepResult(
[Description("Whether the compilation succeeded without errors")]
bool Success,
[Description("The compiled ARM template JSON, or null if compilation failed")]
string? Template,
[Description("The list of diagnostics from compilation")]
ImmutableArray<DiagnosticDefinition> Diagnostics);

public record BuildBicepparamResult(
[Description("Whether the compilation succeeded without errors")]
bool Success,
[Description("The compiled parameters JSON, or null if compilation failed")]
string? Parameters,
[Description("The compiled ARM template JSON, or null if not applicable or compilation failed")]
string? Template,
[Description("The list of diagnostics from compilation")]
ImmutableArray<DiagnosticDefinition> Diagnostics);

[McpServerTool(Title = "Build Bicep", Destructive = false, Idempotent = true, OpenWorld = true, ReadOnly = true, UseStructuredContent = true)]
[Description("""
Analyzes a Bicep file (.bicep) or Bicep parameters file (.bicepparam) and returns all compilation diagnostics including errors, warnings, and informational messages.
Compiles a Bicep file (.bicep) to an ARM template JSON string and returns the result along with any diagnostics.

Use this tool to:
- Validate Bicep syntax and identify compilation errors before deployment
- Check for warnings about deprecated features, security issues, or best practice violations
- Troubleshoot why a Bicep file isn't compiling
- Understand the severity and location of issues in the code

Each diagnostic includes the error code (e.g., BCP033), severity level (Error/Warning/Info), descriptive message, exact position in the file, and a link to documentation for more details.
The file path must be absolute. Diagnostics are returned for the specified file and any modules it references.
- Compile Bicep source code to ARM template JSON
- Check for compilation errors before deployment
- Obtain the ARM template output for inspection or deployment

The compiled ARM template JSON is returned along with any compilation diagnostics (errors, warnings, and informational messages).
The file path must be absolute. If compilation fails due to errors, the Template field will be null.
""")]
public async Task<ImmutableArray<DiagnosticDefinition>> GetBicepFileDiagnostics(
[Description("The path to the .bicep or .bicepparam file")] string filePath)
public async Task<BuildBicepResult> BuildBicep(
[Description("The path to the .bicep file")] string filePath)
{
var fileUri = IOUri.FromFilePath(filePath);
if (!fileUri.HasBicepExtension() && !fileUri.HasBicepParamExtension())
if (!fileUri.HasBicepExtension())
{
throw new ArgumentException("The specified file must have a .bicep or .bicepparam extension.", nameof(filePath));
throw new ArgumentException("The specified file must have a .bicep extension.", nameof(filePath));
}

var compilation = await compiler.CreateCompilation(fileUri);
var result = compilation.Emitter.Template();

return new BuildBicepResult(result.Success, result.Template, GetDiagnostics(result.Diagnostics));
}

[McpServerTool(Title = "Build Bicep Parameters", Destructive = false, Idempotent = true, OpenWorld = true, ReadOnly = true, UseStructuredContent = true)]
[Description("""
Compiles a Bicep parameters file (.bicepparam) to a parameters JSON string and returns the result along with any diagnostics.

Use this tool to:
- Compile Bicep parameters source code to ARM parameters JSON
- Check for compilation errors before deployment
- Obtain the parameters JSON output for inspection or deployment

The compiled parameters JSON and the associated ARM template JSON are returned along with any compilation diagnostics (errors, warnings, and informational messages).
The file path must be absolute. If compilation fails due to errors, the Parameters field will be null.
""")]
public async Task<BuildBicepparamResult> BuildBicepparam(
[Description("The path to the .bicepparam file")] string filePath)
{
var fileUri = IOUri.FromFilePath(filePath);
if (!fileUri.HasBicepParamExtension())
{
throw new ArgumentException("The specified file must have a .bicepparam extension.", nameof(filePath));
}

var compilation = await compiler.CreateCompilation(fileUri);
var result = compilation.Emitter.Parameters();

return [.. compilation
.GetAllDiagnosticsByBicepFile()
.SelectMany(kvp => kvp.Value.Select(x => new DiagnosticDefinition(
kvp.Key.FileHandle.Uri.ToUri(),
x.Code,
x.Level.ToString(),
x.Message,
x.Uri,
x.Span.Position,
x.Span.Length)))];
return new BuildBicepparamResult(result.Success, result.Parameters, result.Template?.Template, GetDiagnostics(result.Diagnostics));
}

[McpServerTool(Title = "Format Bicep File", Destructive = false, Idempotent = true, OpenWorld = true, ReadOnly = true, UseStructuredContent = true)]
Expand Down Expand Up @@ -148,4 +186,16 @@ public async Task<GetFileReferencesResult> GetFileReferences(
return new(
[.. fileUris.Select(x => x.ToUri()).OrderBy(x => x.ToString())]);
}

private static ImmutableArray<DiagnosticDefinition> GetDiagnostics(ImmutableDictionary<BicepSourceFile, ImmutableArray<IDiagnostic>> diagnosticsByFile)
{
return [.. diagnosticsByFile.SelectMany(kvp => kvp.Value.Select(x => new DiagnosticDefinition(
kvp.Key.FileHandle.Uri.ToUri(),
x.Code,
x.Level.ToString(),
x.Message,
x.Uri,
x.Span.Position,
x.Span.Length)))];
}
}
102 changes: 79 additions & 23 deletions src/Bicep.McpServer.UnitTests/BicepCompilerToolsTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Baselines;
using Bicep.Core.UnitTests.Utils;
using Bicep.IO.Abstraction;
using Bicep.McpServer.Core;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -30,25 +26,6 @@ private static IServiceProvider GetServiceProvider()

private readonly BicepCompilerTools tools = GetServiceProvider().GetRequiredService<BicepCompilerTools>();

[TestMethod]
public async Task GetBicepFileDiagnostics_returns_list_of_diagnostics()
{
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", """
var foo string = 123
""");

var response = await tools.GetBicepFileDiagnostics(bicepFilePath);

response.Should().HaveCountGreaterThanOrEqualTo(2);
var diagnostic = response.Should().ContainSingle(x => x.Code == "BCP033").Subject;
diagnostic.FileUri.Should().Be(IOUri.FromFilePath(bicepFilePath).ToUri());
diagnostic.Level.Should().Be("Error");
diagnostic.Message.Should().Be("Expected a value of type \"string\" but the provided value is of type \"123\".");
diagnostic.Position.Should().Be(17);
diagnostic.Length.Should().Be(3);
diagnostic.DocumentationUri.Should().Be(new Uri("https://aka.ms/bicep/core-diagnostics#BCP033"));
}

[TestMethod]
public async Task FormatBicepFile_returns_formatted_bicep_content()
{
Expand Down Expand Up @@ -88,4 +65,83 @@ param location string
"location.txt",
]);
}

[TestMethod]
public async Task BuildBicep_returns_compiled_template()
{
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", """
param location string = 'westus'
output loc string = location
""");

var response = await tools.BuildBicep(bicepFilePath);

response.Success.Should().BeTrue();
response.Template.Should().NotBeNullOrEmpty();
response.Template.Should().Contain("\"$schema\"");
response.Diagnostics.Should().NotContain(x => x.Level == "Error");
}

[TestMethod]
public async Task BuildBicep_returns_diagnostics_on_error()
{
var bicepFilePath = FileHelper.SaveResultFile(TestContext, "main.bicep", """
var foo string = 123
""");

var response = await tools.BuildBicep(bicepFilePath);

response.Success.Should().BeFalse();
response.Template.Should().BeNull();
response.Diagnostics.Should().HaveCountGreaterThanOrEqualTo(2);
var diagnostic = response.Diagnostics.Should().ContainSingle(x => x.Code == "BCP033").Subject;
diagnostic.Level.Should().Be("Error");
}

[TestMethod]
public async Task BuildBicepparam_returns_compiled_parameters()
{
var outputFolder = FileHelper.SaveResultFiles(TestContext, [
new("main.bicep", """
param location string
output loc string = location
"""),
new("main.bicepparam", """
using 'main.bicep'

param location = 'westus'
"""),
]);

var response = await tools.BuildBicepparam(Path.Combine(outputFolder, "main.bicepparam"));

response.Success.Should().BeTrue();
response.Parameters.Should().NotBeNullOrEmpty();
response.Parameters.Should().Contain("\"$schema\"");
response.Template.Should().NotBeNullOrEmpty();
response.Template.Should().Contain("\"$schema\"");
response.Diagnostics.Should().NotContain(x => x.Level == "Error");
}

[TestMethod]
public async Task BuildBicepparam_returns_diagnostics_on_error()
{
var outputFolder = FileHelper.SaveResultFiles(TestContext, [
new("main.bicep", """
param location string
"""),
new("main.bicepparam", """
using 'main.bicep'

param location = 123
"""),
]);

var response = await tools.BuildBicepparam(Path.Combine(outputFolder, "main.bicepparam"));

response.Success.Should().BeFalse();
response.Parameters.Should().BeNull();
response.Diagnostics.Should().HaveCountGreaterThanOrEqualTo(1);
response.Diagnostics.Should().Contain(x => x.Level == "Error");
}
}
Loading
Loading