using Microsoft.Build.Framework;
using Scriban;
using Scriban.Runtime;
using System.Text.Json;
namespace Sbroenne.ExcelMcp.Build.Tasks;
/// <summary>
/// MSBuild task that generates skill files from Scriban templates.
/// </summary>
public class GenerateSkillFile : Microsoft.Build.Utilities.Task
{
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
/// <summary>Path to the Scriban template file (.sbn)</summary>
[Required]
public string TemplatePath { get; set; } = "";
/// <summary>Output path for the generated file</summary>
[Required]
public string OutputPath { get; set; } = "";
/// <summary>Path to the generated _SkillManifest.g.cs file containing JSON metadata</summary>
public string? ManifestPath { get; set; }
/// <summary>Executes the task to generate the skill file from the template.</summary>
/// <returns>true if the task succeeded; otherwise, false.</returns>
public override bool Execute()
{
try
{
if (!File.Exists(TemplatePath))
{
Log.LogError($"Template file not found: {TemplatePath}");
return false;
}
// Read and parse template
var templateContent = File.ReadAllText(TemplatePath);
var template = Template.Parse(templateContent);
if (template.HasErrors)
{
foreach (var error in template.Messages)
{
Log.LogError($"Template error: {error}");
}
return false;
}
// Build model from manifest
var model = BuildModelFromManifest();
// Render template
var scriptObject = new ScriptObject();
scriptObject.Import(model, renamer: member => member.Name.ToLowerInvariant());
var context = new TemplateContext();
context.PushGlobal(scriptObject);
var output = template.Render(context);
// Ensure output directory exists
var outputDir = Path.GetDirectoryName(OutputPath);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
// Write output
File.WriteAllText(OutputPath, output);
Log.LogMessage(MessageImportance.High, $"Generated: {OutputPath}");
return true;
}
catch (Exception ex)
{
Log.LogErrorFromException(ex, showStackTrace: true);
return false;
}
}
private SkillTemplateModel BuildModelFromManifest()
{
var model = new SkillTemplateModel();
if (string.IsNullOrEmpty(ManifestPath) || !File.Exists(ManifestPath))
{
Log.LogWarning($"Manifest file not found: {ManifestPath}. Skill will have no command reference.");
return model;
}
// Read the generated _SkillManifest.g.cs file and extract JSON
var manifestContent = File.ReadAllText(ManifestPath);
var json = ExtractJsonFromManifest(manifestContent);
if (string.IsNullOrEmpty(json))
{
Log.LogWarning($"Could not extract JSON from manifest: {ManifestPath}");
return model;
}
// Parse JSON
try
{
var manifest = JsonSerializer.Deserialize<SkillManifest>(json!, JsonOptions);
if (manifest != null)
{
model.ToolCount = manifest.TotalCommands;
model.OperationCount = manifest.TotalOperations;
model.CliCommands = manifest.Commands?.Select(c => new CliCommand
{
Name = c.Name ?? "",
Description = c.Description ?? "",
Actions = c.Actions?.ToList() ?? new List<string>(),
Parameters = c.Parameters?.Select(p => new CliParameter
{
Name = p.Name ?? "",
Description = p.Description ?? ""
}).ToList() ?? new List<CliParameter>()
}).ToList();
Log.LogMessage(MessageImportance.Normal, $"Loaded manifest: {model.ToolCount} commands, {model.OperationCount} operations");
}
}
catch (JsonException ex)
{
Log.LogWarning($"Failed to parse manifest JSON: {ex.Message}");
}
return model;
}
private static string? ExtractJsonFromManifest(string content)
{
// The manifest file contains: public const string Json = @"{...}";
// We need to extract the JSON between @" and ";
const string startMarker = "public const string Json = @\"";
const string endMarker = "\";";
var startIndex = content.IndexOf(startMarker, StringComparison.Ordinal);
if (startIndex < 0)
return null;
startIndex += startMarker.Length;
var endIndex = content.LastIndexOf(endMarker, StringComparison.Ordinal);
if (endIndex <= startIndex)
return null;
var json = content.Substring(startIndex, endIndex - startIndex);
// The JSON uses doubled quotes ("") for escaping in verbatim string
// Convert back to regular JSON quotes
json = json.Replace("\"\"", "\"");
return json;
}
}
/// <summary>JSON manifest structure from the generator.</summary>
internal sealed class SkillManifest
{
public List<ManifestCommand>? Commands { get; set; }
public int TotalCommands { get; set; }
public int TotalOperations { get; set; }
}
/// <summary>Command entry in the manifest.</summary>
internal sealed class ManifestCommand
{
public string? Name { get; set; }
public string? McpTool { get; set; }
public string? Description { get; set; }
public string[]? Actions { get; set; }
public ManifestParameter[]? Parameters { get; set; }
}
/// <summary>Parameter entry in the manifest.</summary>
internal sealed class ManifestParameter
{
public string? Name { get; set; }
public string? Description { get; set; }
}