From 056c1a21ca6bd5226eb57423b1955e6aedab32cb Mon Sep 17 00:00:00 2001 From: Zsolt Domokos Date: Sat, 19 Sep 2026 18:48:17 -0400 Subject: [PATCH] fix(tools): open solution asynchronously to avoid RPC timeout dte.Solution.Open() is synchronous and blocks the UI thread until the whole solution finishes loading. Awaiting it inline blocked the RPC call until load completion, which exceeded MCP client timeouts on non-trivial solutions and surfaced as a generic 'An error occurred invoking solution_open' instead of a real result. Mirrors the async pattern already used for build_solution: validate and normalize the path, then fire the actual load off without awaiting it so the tool call returns immediately. Callers can poll solution_info to see when the new solution has finished loading. --- .../Tools/SolutionTools.cs | 4 +- .../Services/VisualStudioService.cs | 39 ++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) 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)