Skip to content

Commit 65a3fbd

Browse files
authored
feat(debugger): add startup project tools and project-specific debug launch (#49)
Add startup_project_get and startup_project_set tools for managing the startup project. Add optional projectName parameter to debugger_launch and debugger_launch_without_debugging that launches a specific project via IVsDebuggableProjectCfg without changing the startup project.
1 parent 945d5ed commit 65a3fbd

7 files changed

Lines changed: 210 additions & 8 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
137137
=> Proxy.FindReferencesAsync(path, line, column, maxResults);
138138

139139
public Task<DebuggerStatus> GetDebuggerStatusAsync() => Proxy.GetDebuggerStatusAsync();
140+
public Task<string?> GetStartupProjectAsync() => Proxy.GetStartupProjectAsync();
141+
public Task<bool> SetStartupProjectAsync(string projectName) => Proxy.SetStartupProjectAsync(projectName);
140142
public Task<bool> DebugLaunchAsync() => Proxy.DebugLaunchAsync();
143+
public Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug) => Proxy.DebugLaunchProjectAsync(projectName, noDebug);
141144
public Task<bool> DebugLaunchWithoutDebuggingAsync() => Proxy.DebugLaunchWithoutDebuggingAsync();
142145
public Task<bool> DebugContinueAsync() => Proxy.DebugContinueAsync();
143146
public Task<bool> DebugBreakAsync() => Proxy.DebugBreakAsync();

src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,41 @@ public async Task<string> GetDebuggerStatusAsync()
2626
}
2727

2828
[McpServerTool(Name = "debugger_launch", Destructive = false)]
29-
[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.")]
30-
public async Task<string> DebugLaunchAsync()
29+
[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.")]
30+
public async Task<string> DebugLaunchAsync(
31+
[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)
3132
{
32-
var success = await _rpcClient.DebugLaunchAsync();
33-
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
33+
if (projectName != null)
34+
{
35+
var success = await _rpcClient.DebugLaunchProjectAsync(projectName, noDebug: false);
36+
return success
37+
? $"Debugging started for project: {projectName}"
38+
: $"Failed to start debugging for project '{projectName}'. Use project_list to verify the project name.";
39+
}
40+
else
41+
{
42+
var success = await _rpcClient.DebugLaunchAsync();
43+
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
44+
}
3445
}
3546

3647
[McpServerTool(Name = "debugger_launch_without_debugging", Destructive = false)]
37-
[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.")]
38-
public async Task<string> DebugLaunchWithoutDebuggingAsync()
48+
[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.")]
49+
public async Task<string> DebugLaunchWithoutDebuggingAsync(
50+
[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)
3951
{
40-
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
41-
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
52+
if (projectName != null)
53+
{
54+
var success = await _rpcClient.DebugLaunchProjectAsync(projectName, noDebug: true);
55+
return success
56+
? $"Started without debugging for project: {projectName}"
57+
: $"Failed to start without debugging for project '{projectName}'. Use project_list to verify the project name.";
58+
}
59+
else
60+
{
61+
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
62+
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
63+
}
4264
}
4365

4466
[McpServerTool(Name = "debugger_continue", Destructive = false)]

src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ public async Task<string> GetProjectListAsync()
6161
return JsonSerializer.Serialize(projects, _jsonOptions);
6262
}
6363

64+
[McpServerTool(Name = "startup_project_get", ReadOnly = true)]
65+
[Description("Get the current startup project name. Returns the project that will be launched when debugging starts.")]
66+
public async Task<string> GetStartupProjectAsync()
67+
{
68+
var startupProject = await _rpcClient.GetStartupProjectAsync();
69+
return startupProject ?? "No startup project is set";
70+
}
71+
72+
[McpServerTool(Name = "startup_project_set", Destructive = false)]
73+
[Description("Set the startup project for debugging. Use project_list to get available project names.")]
74+
public async Task<string> SetStartupProjectAsync(
75+
[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)
76+
{
77+
var success = await _rpcClient.SetStartupProjectAsync(name);
78+
return success ? $"Startup project set to: {name}" : $"Failed to set startup project: {name}";
79+
}
80+
6481
[McpServerTool(Name = "project_info", ReadOnly = true)]
6582
[Description("Get detailed information about a specific project by its display name.")]
6683
public async Task<string> GetProjectInfoAsync(

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ public interface IVisualStudioRpc
4242
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
4343

4444
Task<DebuggerStatus> GetDebuggerStatusAsync();
45+
Task<string?> GetStartupProjectAsync();
46+
Task<bool> SetStartupProjectAsync(string projectName);
4547
Task<bool> DebugLaunchAsync();
48+
Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug);
4649
Task<bool> DebugLaunchWithoutDebuggingAsync();
4750
Task<bool> DebugContinueAsync();
4851
Task<bool> DebugBreakAsync();

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public interface IVisualStudioService
3838
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
3939

4040
Task<DebuggerStatus> GetDebuggerStatusAsync();
41+
Task<string?> GetStartupProjectAsync();
42+
Task<bool> SetStartupProjectAsync(string projectName);
4143
Task<bool> DebugLaunchAsync();
44+
Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug);
4245
Task<bool> DebugLaunchWithoutDebuggingAsync();
4346
Task<bool> DebugContinueAsync();
4447
Task<bool> DebugBreakAsync();

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
195195
=> _vsService.FindReferencesAsync(path, line, column, maxResults);
196196

197197
public Task<DebuggerStatus> GetDebuggerStatusAsync() => _vsService.GetDebuggerStatusAsync();
198+
public Task<string?> GetStartupProjectAsync() => _vsService.GetStartupProjectAsync();
199+
public Task<bool> SetStartupProjectAsync(string projectName) => _vsService.SetStartupProjectAsync(projectName);
198200
public Task<bool> DebugLaunchAsync() => _vsService.DebugLaunchAsync();
201+
public Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug) => _vsService.DebugLaunchProjectAsync(projectName, noDebug);
199202
public Task<bool> DebugLaunchWithoutDebuggingAsync() => _vsService.DebugLaunchWithoutDebuggingAsync();
200203
public Task<bool> DebugContinueAsync() => _vsService.DebugContinueAsync();
201204
public Task<bool> DebugBreakAsync() => _vsService.DebugBreakAsync();

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,157 @@ public async Task<DebuggerStatus> GetDebuggerStatusAsync()
11671167
return status;
11681168
}
11691169

1170+
public async Task<string?> GetStartupProjectAsync()
1171+
{
1172+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1173+
var dte = await GetDteAsync();
1174+
1175+
try
1176+
{
1177+
if (dte.Solution?.SolutionBuild?.StartupProjects is Array startupProjects && startupProjects.Length > 0)
1178+
{
1179+
return startupProjects.GetValue(0) as string;
1180+
}
1181+
1182+
return null;
1183+
}
1184+
catch (Exception ex)
1185+
{
1186+
VsixTelemetry.TrackException(ex);
1187+
return null;
1188+
}
1189+
}
1190+
1191+
public async Task<bool> SetStartupProjectAsync(string projectName)
1192+
{
1193+
using var activity = VsixTelemetry.Tracer.StartActivity("SetStartupProject");
1194+
1195+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1196+
var dte = await GetDteAsync();
1197+
1198+
try
1199+
{
1200+
dte.Solution.SolutionBuild.StartupProjects = projectName;
1201+
return true;
1202+
}
1203+
catch (Exception ex)
1204+
{
1205+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
1206+
activity?.RecordException(ex);
1207+
return false;
1208+
}
1209+
}
1210+
1211+
public async Task<bool> DebugLaunchProjectAsync(string projectName, bool noDebug)
1212+
{
1213+
using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunchProject");
1214+
1215+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1216+
var dte = await GetDteAsync();
1217+
1218+
try
1219+
{
1220+
EnvDTE.Project? targetProject = null;
1221+
1222+
foreach (EnvDTE.Project project in dte.Solution.Projects)
1223+
{
1224+
targetProject = FindProjectByName(project, projectName);
1225+
if (targetProject != null)
1226+
{
1227+
break;
1228+
}
1229+
}
1230+
1231+
if (targetProject == null)
1232+
{
1233+
return false;
1234+
}
1235+
1236+
var solution = ServiceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
1237+
if (solution == null)
1238+
{
1239+
return false;
1240+
}
1241+
1242+
ErrorHandler.ThrowOnFailure(
1243+
solution.GetProjectOfUniqueName(targetProject.UniqueName, out var hierarchy));
1244+
1245+
if (hierarchy is not IVsGetCfgProvider getCfgProvider)
1246+
{
1247+
return false;
1248+
}
1249+
1250+
ErrorHandler.ThrowOnFailure(getCfgProvider.GetCfgProvider(out var cfgProvider));
1251+
1252+
if (cfgProvider is not IVsCfgProvider2 cfgProvider2)
1253+
{
1254+
return false;
1255+
}
1256+
1257+
var configName = targetProject.ConfigurationManager.ActiveConfiguration.ConfigurationName;
1258+
var platformName = targetProject.ConfigurationManager.ActiveConfiguration.PlatformName;
1259+
1260+
ErrorHandler.ThrowOnFailure(
1261+
cfgProvider2.GetCfgOfName(configName, platformName, out var cfg));
1262+
1263+
if (cfg is not IVsDebuggableProjectCfg debuggableProjectCfg)
1264+
{
1265+
return false;
1266+
}
1267+
1268+
var launchFlags = noDebug
1269+
? (uint)__VSDBGLAUNCHFLAGS.DBGLAUNCH_NoDebug
1270+
: 0u;
1271+
1272+
ErrorHandler.ThrowOnFailure(debuggableProjectCfg.DebugLaunch(launchFlags));
1273+
1274+
return true;
1275+
}
1276+
catch (Exception ex)
1277+
{
1278+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
1279+
activity?.RecordException(ex);
1280+
return false;
1281+
}
1282+
}
1283+
1284+
private static EnvDTE.Project? FindProjectByName(EnvDTE.Project project, string name)
1285+
{
1286+
ThreadHelper.ThrowIfNotOnUIThread();
1287+
1288+
try
1289+
{
1290+
if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder)
1291+
{
1292+
if (project.ProjectItems != null)
1293+
{
1294+
foreach (ProjectItem item in project.ProjectItems)
1295+
{
1296+
if (item.SubProject != null)
1297+
{
1298+
var found = FindProjectByName(item.SubProject, name);
1299+
if (found != null)
1300+
{
1301+
return found;
1302+
}
1303+
}
1304+
}
1305+
}
1306+
1307+
return null;
1308+
}
1309+
1310+
return string.Equals(project.Name, name, StringComparison.OrdinalIgnoreCase)
1311+
? project
1312+
: null;
1313+
}
1314+
catch (Exception ex)
1315+
{
1316+
VsixTelemetry.TrackException(ex);
1317+
return null;
1318+
}
1319+
}
1320+
11701321
public async Task<bool> DebugLaunchAsync()
11711322
{
11721323
using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunch");

0 commit comments

Comments
 (0)