diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs index 7f9f57a..a84d2b2 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/SolutionTools.cs @@ -31,12 +31,12 @@ public async Task GetSolutionInfoAsync() } [McpServerTool(Name = "solution_open", Destructive = true, Idempotent = true)] - [Description("Open a solution file in Visual Studio. This will close any currently open solution.")] + [Description("Open a solution file in Visual Studio. This will close any currently open solution. Loading runs asynchronously and this returns as soon as the load has started; use solution_info to check when the solution has finished loading.")] public async Task OpenSolutionAsync( [Description("The full absolute path to the solution file (.sln or .slnx). Supports forward slashes (/) or backslashes (\\).")] string path) { var success = await _rpcClient.OpenSolutionAsync(path); - return success ? $"Opened solution: {path}" : $"Failed to open solution: {path}"; + return success ? $"Opening solution: {path}" : $"Failed to open solution (file not found?): {path}"; } [McpServerTool(Name = "solution_close", Destructive = true, Idempotent = true)] diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index 664f09f..0e49676 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -193,20 +193,39 @@ public async Task OpenSolutionAsync(string path) { using var activity = VsixTelemetry.Tracer.StartActivity("OpenSolution"); - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - var dte = await GetDteAsync(); + var normalizedPath = NormalizePath(path); - try - { - dte.Solution.Open(path); - return true; - } - catch (Exception ex) + if (!File.Exists(normalizedPath)) { - activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - activity?.RecordException(ex); + activity?.SetStatus(ActivityStatusCode.Error, "Solution file not found"); return false; } + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + // EnvDTE's Solution.Open() is synchronous and can take a long time for large + // solutions. Awaiting it here would block this RPC call (and the calling MCP client) + // until the whole solution finishes loading, which can exceed client timeouts and + // surface as "An error occurred invoking 'solution_open'" even though the solution is + // loading normally. Fire the load off without waiting for it to finish; callers can + // poll solution_info to observe when the new solution becomes available. + _ = Task.Run(async () => + { + using var loadActivity = VsixTelemetry.Tracer.StartActivity("OpenSolution.Load"); + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + dte.Solution.Open(normalizedPath); + } + catch (Exception ex) + { + loadActivity?.SetStatus(ActivityStatusCode.Error, ex.Message); + loadActivity?.RecordException(ex); + } + }); + + return true; } public async Task CloseSolutionAsync(bool saveFirst = true)