Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ We have built a Bicep MCP server with agentic tools to support Bicep code genera
- `get_extension_resource_type_schema`: Gets the schema for a specific extension resource type. Accepts a canonical OCI artifact reference, resource type, and API version.
- `list_well_known_extensions`: Lists well-known Bicep extensions (e.g., Microsoft Graph) with their dynamically-discovered version tags from MCR. This is not an exhaustive list; other extensions may exist. Use this to discover extensions and their versions for use with the extension resource type tools.
- `list_avm_metadata`: Lists up-to-date metadata for all Azure Verified Modules (AVM). The return value is a newline-separated list of AVM metadata. Each line includes the module name, description, versions, and documentation URI for a specific module.
- `get_bicep_file_diagnostics`: Analyzes a Bicep file (`.bicep`) or Bicep parameters file (`.bicepparam`) and returns all compilation diagnostics including errors, warnings, and informational messages.
- `format_bicep_file`: Formats a Bicep file (`.bicep`) or Bicep parameters file (`.bicepparam`) according to official Bicep formatting standards, respecting `bicepconfig.json` settings.
- `build_bicep`: Compiles a Bicep file (`.bicep`) and returns the generated ARM template plus diagnostics. Accepts an absolute file path, an in-memory content payload, or both.
- `build_bicepparam`: Compiles a Bicep parameters file (`.bicepparam`) and returns generated parameters/template JSON plus diagnostics. Accepts an absolute file path, an in-memory content payload, or both.
- `format_bicep_file`: Formats a Bicep file (`.bicep`) or Bicep parameters file (`.bicepparam`) according to official Bicep formatting standards, respecting `bicepconfig.json` settings. Accepts an absolute file path, an in-memory content payload, or both.
- `get_file_references`: Analyzes a Bicep or Bicep parameters file and returns a list of all files it references, including modules, parameter files, and other dependencies.
- `decompile_arm_template_file`: Converts an ARM template JSON file into Bicep syntax (`.bicep`). Accepts files with `.json`, `.jsonc`, or `.arm` extensions.
- `decompile_arm_parameters_file`: Converts an ARM template parameters JSON file into Bicep parameters syntax (`.bicepparam`). Accepts files with `.json`, `.jsonc`, or `.arm` extensions.
Expand Down
91 changes: 64 additions & 27 deletions src/Bicep.McpServer.Core/BicepCompilerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bicep.Core.Diagnostics;
using Bicep.Core.Extensions;
using Bicep.Core.PrettyPrintV2;
using Bicep.Core.Semantics;
using Bicep.Core.SourceGraph;
using Bicep.IO.Abstraction;
using ModelContextProtocol.Server;
Expand All @@ -17,6 +18,9 @@ namespace Bicep.McpServer.Core;
public sealed class BicepCompilerTools(
BicepCompiler compiler)
{
private static readonly IOUri InMemoryBicepFileUri = new(IOUriScheme.Untitled, null, "/main.bicep");
private static readonly IOUri InMemoryBicepParamFileUri = new(IOUriScheme.Untitled, null, "/main.bicepparam");

public record DiagnosticDefinition(
[Description("The .bicep or .bicepparam file URI")]
Uri FileUri,
Expand Down Expand Up @@ -69,18 +73,19 @@ Compiles a Bicep file (.bicep) to an ARM template JSON string and returns the re
- 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.
Provide an absolute file path, in-memory content, or both. If content is provided, it is compiled in-memory and filePath (if provided) is used as the entrypoint URI context. If compilation fails due to errors, the Template field will be null.
Comment thread
anthony-c-martin marked this conversation as resolved.
Outdated
""")]
public async Task<BuildBicepResult> BuildBicep(
[Description("The path to the .bicep file")] string filePath)
[Description("The path to the .bicep file. Required if content is not provided; when content is provided, this sets URI context.")] string? filePath = null,
[Description("The in-memory .bicep file content. If provided, this content is compiled instead of reading from disk.")] string? content = null)
{
var fileUri = IOUri.FromFilePath(filePath);
if (!fileUri.HasBicepExtension())
{
throw new ArgumentException("The specified file must have a .bicep extension.", nameof(filePath));
}

var compilation = await compiler.CreateCompilation(fileUri);
var compilation = await CreateCompilation(
filePath,
content,
InMemoryBicepFileUri,
fileUri => fileUri.HasBicepExtension(),
"The specified file must have a .bicep extension.",
(fileUri, fileContent) => compiler.SourceFileFactory.CreateBicepFile(fileUri, fileContent));
var result = compilation.Emitter.Template();

return new BuildBicepResult(result.Success, result.Template, GetDiagnostics(result.Diagnostics));
Expand All @@ -96,18 +101,19 @@ Compiles a Bicep parameters file (.bicepparam) to a parameters JSON string and r
- 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.
Provide an absolute file path, in-memory content, or both. If content is provided, it is compiled in-memory and filePath (if provided) is used as the entrypoint URI context. 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)
[Description("The path to the .bicepparam file. Required if content is not provided; when content is provided, this sets URI context.")] string? filePath = null,
[Description("The in-memory .bicepparam file content. If provided, this content is compiled instead of reading from disk.")] string? content = null)
{
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 compilation = await CreateCompilation(
filePath,
content,
InMemoryBicepParamFileUri,
fileUri => fileUri.HasBicepParamExtension(),
"The specified file must have a .bicepparam extension.",
(fileUri, fileContent) => compiler.SourceFileFactory.CreateBicepParamFile(fileUri, fileContent));
var result = compilation.Emitter.Parameters();

return new BuildBicepparamResult(result.Success, result.Parameters, result.Template?.Template, GetDiagnostics(result.Diagnostics));
Expand All @@ -127,18 +133,19 @@ Formats a Bicep file (.bicep) or Bicep parameters file (.bicepparam) according t
- Newline character (LF, CRLF, CR)
- Whether to insert a final newline

The file path must be absolute. Formatting preserves semantic meaning and only changes whitespace and layout. Files with syntax errors will still be formatted to the extent possible.
Provide an absolute file path, in-memory content, or both. If content is provided, it is formatted in-memory and filePath (if provided) is used as the entrypoint URI context. Formatting preserves semantic meaning and only changes whitespace and layout. Files with syntax errors will still be formatted to the extent possible.
""")]
public async Task<FormatResult> FormatBicepFile(
[Description("The path to the .bicep or .bicepparam file")] string filePath)
[Description("The path to the .bicep or .bicepparam file. Required if content is not provided; when content is provided, this sets URI context.")] string? filePath = null,
[Description("The in-memory .bicep or .bicepparam file content. If provided, this content is formatted instead of reading from disk.")] string? content = null)
{
var fileUri = IOUri.FromFilePath(filePath);
if (!fileUri.HasBicepExtension() && !fileUri.HasBicepParamExtension())
{
throw new ArgumentException("The specified file must have a .bicep or .bicepparam extension.", nameof(filePath));
}

var compilation = await compiler.CreateCompilation(fileUri);
var compilation = await CreateCompilation(
filePath,
content,
InMemoryBicepFileUri,
fileUri => fileUri.HasBicepExtension() || fileUri.HasBicepParamExtension(),
"The specified file must have a .bicep or .bicepparam extension.",
(fileUri, fileContent) => compiler.SourceFileFactory.CreateSourceFile(fileUri, fileContent));
var sourceFile = compilation.GetEntrypointSemanticModel().SourceFile;

var options = sourceFile.Configuration.Formatting.Data;
Expand Down Expand Up @@ -198,4 +205,34 @@ private static ImmutableArray<DiagnosticDefinition> GetDiagnostics(ImmutableDict
x.Span.Position,
x.Span.Length)))];
}

private async Task<Compilation> CreateCompilation(
string? filePath,
string? content,
IOUri inMemoryFileUri,
Func<IOUri, bool> hasExpectedExtension,
string invalidExtensionError,
Func<IOUri, string, ISourceFile> sourceFileFactory)
{
if (filePath is null && content is null)
{
throw new ArgumentException("Either 'filePath' or 'content' must be provided.");
}

var fileUri = filePath is null ? inMemoryFileUri : IOUri.FromFilePath(filePath);
if (!hasExpectedExtension(fileUri))
{
throw new ArgumentException(invalidExtensionError, filePath is null ? nameof(content) : nameof(filePath));
}

if (content is null)
{
return await compiler.CreateCompilation(fileUri);
}

var workspace = new ActiveSourceFileSet();
workspace.UpsertSourceFile(sourceFileFactory(fileUri, content));

return await compiler.CreateCompilation(fileUri, workspace);
}
}
61 changes: 61 additions & 0 deletions src/Bicep.McpServer.UnitTests/BicepCompilerToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ param foo string
response.Content.Should().Contain("param foo string");
}

[TestMethod]
public async Task FormatBicepFile_formats_in_memory_bicep_content()
{
var response = await tools.FormatBicepFile(content: """
param foo string
""");

response.Content.Should().Contain("param foo string");
}

[TestMethod]
public async Task GetFileReferences_returns_referenced_files()
{
Expand Down Expand Up @@ -82,6 +92,20 @@ public async Task BuildBicep_returns_compiled_template()
response.Diagnostics.Should().NotContain(x => x.Level == "Error");
}

[TestMethod]
public async Task BuildBicep_returns_compiled_template_for_in_memory_content()
{
var response = await tools.BuildBicep(content: """
param location string = 'westus'
output loc string = location
""");

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()
{
Expand Down Expand Up @@ -123,6 +147,43 @@ param location string
response.Diagnostics.Should().NotContain(x => x.Level == "Error");
}

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

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

param location = 'westus'
""");

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

[TestMethod]
public async Task BuildBicep_compiles_in_memory_content_when_filePath_and_content_are_both_provided()
{
var response = await tools.BuildBicep(filePath: "/main.bicep", content: """
param location string = 'westus'
output loc string = location
""");

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

[TestMethod]
public async Task BuildBicepparam_returns_diagnostics_on_error()
{
Expand Down
Loading