diff --git a/README.md b/README.md index 8347654..3df712d 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ | Tool | Description | |------|-------------| | `build_cancel` | Cancel a running build | +| `build_configuration_get` | Get the active and available build configuration/platform pairs | +| `build_configuration_set` | Change the active build configuration and platform | | `build_project` | Build a specific project | | `build_solution` | Build the entire solution | | `build_status` | Get current build status | diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index c4219a5..8cdf111 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -130,6 +130,9 @@ public Task> FindAsync(string searchText, bool matchCase, bool public Task CleanSolutionAsync() => Proxy.CleanSolutionAsync(); public Task CancelBuildAsync() => Proxy.CancelBuildAsync(); public Task GetBuildStatusAsync() => Proxy.GetBuildStatusAsync(); + public Task GetBuildConfigurationAsync() => Proxy.GetBuildConfigurationAsync(); + public Task SetBuildConfigurationAsync(string configuration, string platform) + => Proxy.SetBuildConfigurationAsync(configuration, platform); public Task> GetDocumentSymbolsAsync(string path) => Proxy.GetDocumentSymbolsAsync(path); public Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100) diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/BuildTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/BuildTools.cs index 6778ec4..0336ba5 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/BuildTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/BuildTools.cs @@ -10,6 +10,8 @@ public class BuildTools { private const string DebugSessionActiveMessage = "A debug session is currently active. Stop debugging first using debugger_stop before building."; + private const string ConfigurationDebugSessionActiveMessage = + "A debug session is currently active. Stop debugging first using debugger_stop before changing the build configuration."; private readonly RpcClient _rpcClient; private readonly JsonSerializerOptions _jsonOptions; @@ -81,4 +83,38 @@ public async Task GetBuildStatusAsync() var status = await _rpcClient.GetBuildStatusAsync(); return JsonSerializer.Serialize(status, _jsonOptions); } + + [McpServerTool(Name = "build_configuration_get", ReadOnly = true)] + [Description("Get the active solution build configuration and platform, plus every available configuration/platform pair. Use this before build_configuration_set to discover exact values such as 'Release' and 'x64'.")] + public async Task GetBuildConfigurationAsync() + { + var configuration = await _rpcClient.GetBuildConfigurationAsync(); + return JsonSerializer.Serialize(configuration, _jsonOptions); + } + + [McpServerTool(Name = "build_configuration_set", Destructive = false, Idempotent = true)] + [Description("Activate an existing solution build configuration and platform in Visual Studio, such as 'Release' and 'x64'. Call build_configuration_get first because only listed pairs are accepted. Cannot change configuration while a build or debug session is active.")] + public async Task SetBuildConfigurationAsync( + [Description("Solution configuration name, such as 'Debug' or 'Release'.")] string configuration, + [Description("Solution platform/architecture, such as 'Any CPU', 'x64', or 'ARM64'.")] string platform) + { + if (string.IsNullOrWhiteSpace(configuration) || string.IsNullOrWhiteSpace(platform)) + { + return "Configuration and platform are required. Call build_configuration_get for available values."; + } + + if (await IsDebuggingActiveAsync()) + { + return ConfigurationDebugSessionActiveMessage; + } + + var success = await _rpcClient.SetBuildConfigurationAsync(configuration.Trim(), platform.Trim()); + if (!success) + { + return $"Failed to activate build configuration '{configuration}|{platform}'. A build may be running or the pair may not exist. Call build_configuration_get for available values."; + } + + var activeConfiguration = await _rpcClient.GetBuildConfigurationAsync(); + return $"Active build configuration: {activeConfiguration.ActiveConfiguration}|{activeConfiguration.ActivePlatform}"; + } } diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/BuildModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/BuildModels.cs index 829352c..f2ff491 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/Models/BuildModels.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/BuildModels.cs @@ -1,5 +1,20 @@ +using System.Collections.Generic; + namespace CodingWithCalvin.MCPServer.Shared.Models; +public class BuildConfiguration +{ + public string Configuration { get; set; } = string.Empty; + public string Platform { get; set; } = string.Empty; +} + +public class BuildConfigurationInfo +{ + public string ActiveConfiguration { get; set; } = string.Empty; + public string ActivePlatform { get; set; } = string.Empty; + public List AvailableConfigurations { get; set; } = new(); +} + public class BuildStatus { public string State { get; set; } = string.Empty; diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 5da9c82..f21a96e 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -38,6 +38,8 @@ public interface IVisualStudioRpc Task CleanSolutionAsync(); Task CancelBuildAsync(); Task GetBuildStatusAsync(); + Task GetBuildConfigurationAsync(); + Task SetBuildConfigurationAsync(string configuration, string platform); Task> GetDocumentSymbolsAsync(string path); Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100); diff --git a/src/CodingWithCalvin.MCPServer.Tests/BuildConfigurationTests.cs b/src/CodingWithCalvin.MCPServer.Tests/BuildConfigurationTests.cs new file mode 100644 index 0000000..f8e5ee0 --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Tests/BuildConfigurationTests.cs @@ -0,0 +1,27 @@ +using CodingWithCalvin.MCPServer.Services; +using Xunit; + +namespace CodingWithCalvin.MCPServer.Tests; + +public class BuildConfigurationTests +{ + [Theory] + [InlineData("Debug", "Any CPU", "debug", "any cpu", true)] + [InlineData("Release", "x64", "Debug", "x64", false)] + [InlineData("Release", "x64", "Release", "ARM64", false)] + public void MatchesBuildConfiguration_RequiresExactConfigurationAndPlatform( + string candidateConfiguration, + string candidatePlatform, + string configuration, + string platform, + bool expected) + { + Assert.Equal( + expected, + VisualStudioService.MatchesBuildConfiguration( + candidateConfiguration, + candidatePlatform, + configuration, + platform)); + } +} diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 5e42f67..828be40 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -34,6 +34,8 @@ public interface IVisualStudioService Task CleanSolutionAsync(); Task CancelBuildAsync(); Task GetBuildStatusAsync(); + Task GetBuildConfigurationAsync(); + Task SetBuildConfigurationAsync(string configuration, string platform); Task> GetDocumentSymbolsAsync(string path); Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100); diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index b7259e2..0d79ddd 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -239,6 +239,9 @@ public Task> FindAsync(string searchText, bool matchCase, bool public Task CleanSolutionAsync() => _vsService.CleanSolutionAsync(); public Task CancelBuildAsync() => _vsService.CancelBuildAsync(); public Task GetBuildStatusAsync() => _vsService.GetBuildStatusAsync(); + public Task GetBuildConfigurationAsync() => _vsService.GetBuildConfigurationAsync(); + public Task SetBuildConfigurationAsync(string configuration, string platform) + => _vsService.SetBuildConfigurationAsync(configuration, platform); public Task> GetDocumentSymbolsAsync(string path) => _vsService.GetDocumentSymbolsAsync(path); public Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100) diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index 60d0f06..15584b8 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -43,6 +43,14 @@ private static string NormalizePath(string path) return Path.GetFullPath(path.Replace('/', '\\')); } + internal static bool MatchesBuildConfiguration( + string candidateConfiguration, + string candidatePlatform, + string configuration, + string platform) => + string.Equals(candidateConfiguration, configuration, StringComparison.OrdinalIgnoreCase) + && string.Equals(candidatePlatform, platform, StringComparison.OrdinalIgnoreCase); + private static string DetectLineEnding(string content) { if (content.Contains("\r\n")) return "\r\n"; @@ -796,6 +804,82 @@ public async Task GetBuildStatusAsync() }; } + public async Task GetBuildConfigurationAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + if (!dte.Solution.IsOpen) + { + return new BuildConfigurationInfo(); + } + + var solutionBuild = dte.Solution.SolutionBuild; + var activeConfiguration = solutionBuild.ActiveConfiguration; + + var result = new BuildConfigurationInfo + { + ActiveConfiguration = activeConfiguration?.Name ?? string.Empty, + ActivePlatform = (activeConfiguration as SolutionConfiguration2)?.PlatformName ?? string.Empty + }; + + foreach (SolutionConfiguration solutionConfiguration in solutionBuild.SolutionConfigurations) + { + if (solutionConfiguration is SolutionConfiguration2 configuration) + { + result.AvailableConfigurations.Add(new BuildConfiguration + { + Configuration = configuration.Name, + Platform = configuration.PlatformName + }); + } + } + + return result; + } + + public async Task SetBuildConfigurationAsync(string configuration, string platform) + { + using var activity = VsixTelemetry.Tracer.StartActivity("SetBuildConfiguration"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + if (!dte.Solution.IsOpen + || string.IsNullOrWhiteSpace(configuration) + || string.IsNullOrWhiteSpace(platform)) + { + return false; + } + + var solutionBuild = dte.Solution.SolutionBuild; + + if (solutionBuild.BuildState == vsBuildState.vsBuildStateInProgress) + { + return false; + } + + try + { + foreach (SolutionConfiguration solutionConfiguration in solutionBuild.SolutionConfigurations) + { + if (solutionConfiguration is SolutionConfiguration2 candidate + && MatchesBuildConfiguration(candidate.Name, candidate.PlatformName, configuration, platform)) + { + candidate.Activate(); + return true; + } + } + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + } + + return false; + } + public async Task> GetDocumentSymbolsAsync(string path) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();