Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
=> Proxy.FindReferencesAsync(path, line, column, maxResults);

public Task<DebuggerStatus> GetDebuggerStatusAsync() => Proxy.GetDebuggerStatusAsync();
public Task<string?> GetStartupProjectAsync() => Proxy.GetStartupProjectAsync();
public Task<bool> SetStartupProjectAsync(string projectName) => Proxy.SetStartupProjectAsync(projectName);
public Task<bool> DebugLaunchAsync() => Proxy.DebugLaunchAsync();
public Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug) => Proxy.DebugLaunchProjectAsync(projectName, noDebug);
public Task<bool> DebugLaunchWithoutDebuggingAsync() => Proxy.DebugLaunchWithoutDebuggingAsync();
public Task<bool> DebugContinueAsync() => Proxy.DebugContinueAsync();
public Task<bool> DebugBreakAsync() => Proxy.DebugBreakAsync();
Expand Down
38 changes: 30 additions & 8 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,41 @@ public async Task<string> GetDebuggerStatusAsync()
}

[McpServerTool(Name = "debugger_launch", Destructive = false)]
[Description("Start debugging the current startup project (equivalent to F5). A solution must be open with a valid startup project configured. Use debugger_status to check the resulting state.")]
public async Task<string> DebugLaunchAsync()
[Description("Start debugging a project (equivalent to F5). If projectName is specified, launches that specific project without changing the startup project. Otherwise debugs the current startup project. A solution must be open. Use debugger_status to check the resulting state.")]
public async Task<string> DebugLaunchAsync(
[Description("Optional: The display name of the project to debug (e.g., 'MyProject'). Launches this project directly without changing the startup project. Use project_list to see available project names.")] string? projectName = null)
{
var success = await _rpcClient.DebugLaunchAsync();
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
if (projectName != null)
{
var success = await _rpcClient.DebugLaunchProjectAsync(projectName, noDebug: false);
return success
? $"Debugging started for project: {projectName}"
: $"Failed to start debugging for project '{projectName}'. Use project_list to verify the project name.";
}
else
{
var success = await _rpcClient.DebugLaunchAsync();
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
}
}

[McpServerTool(Name = "debugger_launch_without_debugging", Destructive = false)]
[Description("Start the current startup project without the debugger attached (equivalent to Ctrl+F5). The application runs normally without breakpoints or stepping. A solution must be open with a valid startup project configured.")]
public async Task<string> DebugLaunchWithoutDebuggingAsync()
[Description("Start a project without the debugger attached (equivalent to Ctrl+F5). If projectName is specified, launches that specific project without changing the startup project. Otherwise runs the current startup project. The application runs normally without breakpoints or stepping. A solution must be open.")]
public async Task<string> DebugLaunchWithoutDebuggingAsync(
[Description("Optional: The display name of the project to run (e.g., 'MyProject'). Launches this project directly without changing the startup project. Use project_list to see available project names.")] string? projectName = null)
{
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
if (projectName != null)
{
var success = await _rpcClient.DebugLaunchProjectAsync(projectName, noDebug: true);
return success
? $"Started without debugging for project: {projectName}"
: $"Failed to start without debugging for project '{projectName}'. Use project_list to verify the project name.";
}
else
{
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
}
}

[McpServerTool(Name = "debugger_continue", Destructive = false)]
Expand Down
17 changes: 17 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ public async Task<string> GetProjectListAsync()
return JsonSerializer.Serialize(projects, _jsonOptions);
}

[McpServerTool(Name = "startup_project_get", ReadOnly = true)]
[Description("Get the current startup project name. Returns the project that will be launched when debugging starts.")]
public async Task<string> GetStartupProjectAsync()
{
var startupProject = await _rpcClient.GetStartupProjectAsync();
return startupProject ?? "No startup project is set";
}

[McpServerTool(Name = "startup_project_set", Destructive = false)]
[Description("Set the startup project for debugging. Use project_list to get available project names.")]
public async Task<string> SetStartupProjectAsync(
[Description("The display name of the project to set as the startup project (e.g., 'MyProject'). Use project_list to see available project names.")] string name)
{
var success = await _rpcClient.SetStartupProjectAsync(name);
return success ? $"Startup project set to: {name}" : $"Failed to set startup project: {name}";
}

[McpServerTool(Name = "project_info", ReadOnly = true)]
[Description("Get detailed information about a specific project by its display name.")]
public async Task<string> GetProjectInfoAsync(
Expand Down
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ public interface IVisualStudioRpc
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);

Task<DebuggerStatus> GetDebuggerStatusAsync();
Task<string?> GetStartupProjectAsync();
Task<bool> SetStartupProjectAsync(string projectName);
Task<bool> DebugLaunchAsync();
Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug);
Task<bool> DebugLaunchWithoutDebuggingAsync();
Task<bool> DebugContinueAsync();
Task<bool> DebugBreakAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public interface IVisualStudioService
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);

Task<DebuggerStatus> GetDebuggerStatusAsync();
Task<string?> GetStartupProjectAsync();
Task<bool> SetStartupProjectAsync(string projectName);
Task<bool> DebugLaunchAsync();
Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug);
Task<bool> DebugLaunchWithoutDebuggingAsync();
Task<bool> DebugContinueAsync();
Task<bool> DebugBreakAsync();
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 @@ -195,7 +195,10 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
=> _vsService.FindReferencesAsync(path, line, column, maxResults);

public Task<DebuggerStatus> GetDebuggerStatusAsync() => _vsService.GetDebuggerStatusAsync();
public Task<string?> GetStartupProjectAsync() => _vsService.GetStartupProjectAsync();
public Task<bool> SetStartupProjectAsync(string projectName) => _vsService.SetStartupProjectAsync(projectName);
public Task<bool> DebugLaunchAsync() => _vsService.DebugLaunchAsync();
public Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug) => _vsService.DebugLaunchProjectAsync(projectName, noDebug);
public Task<bool> DebugLaunchWithoutDebuggingAsync() => _vsService.DebugLaunchWithoutDebuggingAsync();
public Task<bool> DebugContinueAsync() => _vsService.DebugContinueAsync();
public Task<bool> DebugBreakAsync() => _vsService.DebugBreakAsync();
Expand Down
151 changes: 151 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,157 @@ public async Task<DebuggerStatus> GetDebuggerStatusAsync()
return status;
}

public async Task<string?> GetStartupProjectAsync()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();

try
{
if (dte.Solution?.SolutionBuild?.StartupProjects is Array startupProjects && startupProjects.Length > 0)
{
return startupProjects.GetValue(0) as string;
}

return null;
}
catch (Exception ex)
{
VsixTelemetry.TrackException(ex);
return null;
}
}

public async Task<bool> SetStartupProjectAsync(string projectName)
{
using var activity = VsixTelemetry.Tracer.StartActivity("SetStartupProject");

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();

try
{
dte.Solution.SolutionBuild.StartupProjects = projectName;
return true;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
return false;
}
}

public async Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug)
{
using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunchProject");

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();

try
{
EnvDTE.Project? targetProject = null;

foreach (EnvDTE.Project project in dte.Solution.Projects)
{
targetProject = FindProjectByName(project, projectName);
if (targetProject != null)
{
break;
}
}

if (targetProject == null)
{
return false;
}

var solution = ServiceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
if (solution == null)
{
return false;
}

ErrorHandler.ThrowOnFailure(
solution.GetProjectOfUniqueName(targetProject.UniqueName, out var hierarchy));

if (hierarchy is not IVsGetCfgProvider getCfgProvider)
{
return false;
}

ErrorHandler.ThrowOnFailure(getCfgProvider.GetCfgProvider(out var cfgProvider));

if (cfgProvider is not IVsCfgProvider2 cfgProvider2)
{
return false;
}

var configName = targetProject.ConfigurationManager.ActiveConfiguration.ConfigurationName;
var platformName = targetProject.ConfigurationManager.ActiveConfiguration.PlatformName;

ErrorHandler.ThrowOnFailure(
cfgProvider2.GetCfgOfName(configName, platformName, out var cfg));

if (cfg is not IVsDebuggableProjectCfg debuggableProjectCfg)
{
return false;
}

var launchFlags = noDebug
? (uint)__VSDBGLAUNCHFLAGS.DBGLAUNCH_NoDebug
: 0u;

ErrorHandler.ThrowOnFailure(debuggableProjectCfg.DebugLaunch(launchFlags));

return true;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
return false;
}
}

private static EnvDTE.Project? FindProjectByName(EnvDTE.Project project, string name)
{
ThreadHelper.ThrowIfNotOnUIThread();

try
{
if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder)
{
if (project.ProjectItems != null)
{
foreach (ProjectItem item in project.ProjectItems)
{
if (item.SubProject != null)
{
var found = FindProjectByName(item.SubProject, name);
if (found != null)
{
return found;
}
}
}
}

return null;
}

return string.Equals(project.Name, name, StringComparison.OrdinalIgnoreCase)
? project
: null;
}
catch (Exception ex)
{
VsixTelemetry.TrackException(ex);
return null;
}
}

public async Task<bool> DebugLaunchAsync()
{
using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunch");
Expand Down
Loading