tools/mcp/dotnet/AzureSDKDevToolsMCP/Tools/SpecValidationTool.cs (111 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using AzureSDKDSpecTools.Helpers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using ModelContextProtocol;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
namespace AzureSDKDevToolsMCP.Tools
{
/// <summary>
/// This tool is used to validate the TypeSpec specification for Azure SDK service.
/// </summary>
[Description("TypeSpec validation tools")]
[McpServerToolType]
public class SpecValidationTools(ITypeSpecHelper _helper, ILogger<SpecValidationTools> _logger)
{
private readonly ITypeSpecHelper typeSpecHelper = _helper;
private readonly ILogger<SpecValidationTools> logger = _logger;
/// <summary>
/// Validates the TypeSpec API specification.
/// </summary>
/// <param name="typeSpecProjectRootPath">The root path of the TypeSpec project.</param>
[McpServerTool, Description("Run TypeSpec validation. This tool runs TypeSpec validation and TypeSpec configuration validation.")]
public IList<string> RunTypeSpecValidation(string typeSpecProjectRootPath)
{
var validationResults = new List<string>();
if (!typeSpecHelper.IsTypeSpecProjectPath(typeSpecProjectRootPath))
{
validationResults.Add($"TypeSpec project is not found in {typeSpecProjectRootPath}. TypeSpec MCP tools can only be used for TypeSpec based spec projects.");
return validationResults;
}
try
{
var specRepoRootPath = GetGitRepoRootPath(typeSpecProjectRootPath);
// Run npm ci
logger.LogInformation("Running npm ci");
RunNpmCi(specRepoRootPath);
logger.LogInformation("Completed runnign npm ci");
//Run TypeSpec validation
logger.LogInformation("Running npx tsv to run the validation");
ValidateTypeSpec(typeSpecProjectRootPath, specRepoRootPath, validationResults);
logger.LogInformation("Completed running TypeSpec validation");
}
catch (Exception ex)
{
validationResults.Add($"Error: {ex.Message}");
}
return validationResults;
}
public static void RunNpmCi(string repoRoot)
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
_ = RunProcess("cmd.exe", "/C npm ci", repoRoot);
}
else
{
_ = RunProcess("npm", "ci", repoRoot);
}
}
private static string ValidateTypeSpec(string typeSpecProjectRootPath, string specRepoRootPath, IList<string> validationResults)
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
var output = RunProcess("cmd.exe", $"/C npx tsv {typeSpecProjectRootPath}", specRepoRootPath);
validationResults.Add(output);
}
else
{
var output = RunProcess("npx", $"tsv {typeSpecProjectRootPath}", specRepoRootPath);
validationResults.Add(output);
}
return "TypeSpec validation completed successfully";
}
private static string GetGitRepoRootPath(string typeSpecProjectRootPath)
{
var currentDirectory = new DirectoryInfo(typeSpecProjectRootPath);
while (currentDirectory != null && !currentDirectory.Name.Equals("specification", StringComparison.OrdinalIgnoreCase))
{
currentDirectory = currentDirectory.Parent;
}
return currentDirectory?.Parent?.FullName ?? string.Empty;
}
private static string RunProcess(string command, string args, string workingDirectory)
{
var processInfo = new ProcessStartInfo
{
FileName = command,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory
};
using var process = Process.Start(processInfo) ?? throw new Exception($"Failed to start the process: {args}");
StringBuilder output = new ();
while (!process.HasExited)
{
Thread.Sleep(2000);
process.Refresh();
output.Append(process.StandardOutput.ReadToEnd());
}
if (process.ExitCode != 0)
{
throw new Exception(process.StandardError.ReadToEnd());
}
return output.ToString();
}
}
}