Skip to content
Open
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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
public Task<bool> CleanSolutionAsync() => Proxy.CleanSolutionAsync();
public Task<bool> CancelBuildAsync() => Proxy.CancelBuildAsync();
public Task<BuildStatus> GetBuildStatusAsync() => Proxy.GetBuildStatusAsync();
public Task<BuildConfigurationInfo> GetBuildConfigurationAsync() => Proxy.GetBuildConfigurationAsync();
public Task<bool> SetBuildConfigurationAsync(string configuration, string platform)
=> Proxy.SetBuildConfigurationAsync(configuration, platform);

public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => Proxy.GetDocumentSymbolsAsync(path);
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
Expand Down
36 changes: 36 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/BuildTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,4 +83,38 @@ public async Task<string> 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<string> 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<string> 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}";
}
}
15 changes: 15 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/BuildModels.cs
Original file line number Diff line number Diff line change
@@ -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<BuildConfiguration> AvailableConfigurations { get; set; } = new();
}

public class BuildStatus
{
public string State { get; set; } = string.Empty;
Expand Down
2 changes: 2 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public interface IVisualStudioRpc
Task<bool> CleanSolutionAsync();
Task<bool> CancelBuildAsync();
Task<BuildStatus> GetBuildStatusAsync();
Task<BuildConfigurationInfo> GetBuildConfigurationAsync();
Task<bool> SetBuildConfigurationAsync(string configuration, string platform);

Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Expand Down
27 changes: 27 additions & 0 deletions src/CodingWithCalvin.MCPServer.Tests/BuildConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public interface IVisualStudioService
Task<bool> CleanSolutionAsync();
Task<bool> CancelBuildAsync();
Task<BuildStatus> GetBuildStatusAsync();
Task<BuildConfigurationInfo> GetBuildConfigurationAsync();
Task<bool> SetBuildConfigurationAsync(string configuration, string platform);

Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Expand Down
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
public Task<bool> CleanSolutionAsync() => _vsService.CleanSolutionAsync();
public Task<bool> CancelBuildAsync() => _vsService.CancelBuildAsync();
public Task<BuildStatus> GetBuildStatusAsync() => _vsService.GetBuildStatusAsync();
public Task<BuildConfigurationInfo> GetBuildConfigurationAsync() => _vsService.GetBuildConfigurationAsync();
public Task<bool> SetBuildConfigurationAsync(string configuration, string platform)
=> _vsService.SetBuildConfigurationAsync(configuration, platform);

public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => _vsService.GetDocumentSymbolsAsync(path);
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
Expand Down
84 changes: 84 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -796,6 +804,82 @@ public async Task<BuildStatus> GetBuildStatusAsync()
};
}

public async Task<BuildConfigurationInfo> 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<bool> 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<List<SymbolInfo>> GetDocumentSymbolsAsync(string path)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Expand Down