Skip to content

fix(build): make build/clean tools non-blocking to match async contract - #96

Merged
CalvinAllen merged 1 commit into
CodingWithCalvin:mainfrom
fivestar1103:fix/build/async-non-blocking-build
Jul 8, 2026
Merged

fix(build): make build/clean tools non-blocking to match async contract#96
CalvinAllen merged 1 commit into
CodingWithCalvin:mainfrom
fivestar1103:fix/build/async-non-blocking-build

Conversation

@fivestar1103

Copy link
Copy Markdown
Contributor

Problem

build_solution, build_project, and clean_solution are documented as asynchronous ("runs asynchronously; use build_status to check progress; returns immediately after starting the build"), but they block until the build/clean fully completes.

This shows up in daily real-world use: I drive builds through this MCP on a large Unreal Engine 5 (C++) solution, and build_solution consistently returns The operation timed out. instead of Build started. Because the blocking call also freezes the Visual Studio UI thread for the whole build, build_status and build_cancel can't run either — so the documented "start build, then poll build_status" workflow is impossible on any large solution.

Root cause

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs calls the EnvDTE SolutionBuild APIs with the wait flag set to true, right after SwitchToMainThreadAsync():

dte.Solution.SolutionBuild.Build(true);                       // BuildSolutionAsync
dte.Solution.SolutionBuild.BuildProject(config, path, true);  // BuildProjectAsync
dte.Solution.SolutionBuild.Clean(true);                       // CleanSolutionAsync

The final boolean is WaitForBuildToFinish / WaitForCleanToFinish; true blocks on the UI thread until the whole operation finishes.

Fix

Pass false so the build/clean is queued and the call returns immediately. This restores the documented async behavior and lets build_status observe the InProgress -> Done transition as intended.

Testing

No automated coverage — the repo has no test harness yet (#11); this is a minimal per-call flag flip.

BuildSolution, BuildProject and CleanSolution called the EnvDTE SolutionBuild APIs with WaitForBuildToFinish/WaitForCleanToFinish set to true, on the UI thread.
This blocks the Visual Studio main thread until the entire build/clean completes, which contradicts the tool description ("runs asynchronously; use build_status to check progress; returns immediately after starting the build").

Two consequences:
- For large solutions the RPC call never returns in time and the MCP client reports "The operation timed out.",
  instead of "Build started".
- While the UI thread is blocked, build_status/build_cancel cannot run,
  so the documented polling workflow is impossible.

Pass false so the build/clean is queued and the call returns immediately,
letting build_status observe the InProgress -> Done transition as intended.
@CalvinAllen
CalvinAllen merged commit 4fc4306 into CodingWithCalvin:main Jul 8, 2026
2 checks passed
CalvinAllen added a commit that referenced this pull request Sep 3, 2026
Adds seven MCP tools over Test Explorer: test_run_all, test_debug_all,
test_run, test_debug, test_cancel, test_status and test_stats.

The extension cannot reference Microsoft.VisualStudio.TestWindow.Interfaces.dll
at compile time. It is absent from the Microsoft.VisualStudio.SDK metapackage,
nuget.org carries nothing newer than 11.0.61030 (2012), and $(DevEnvDir) is
undefined under dotnet build, which is how this repo builds locally and in CI.
TestExplorerInterop therefore resolves ITestExplorerStatsService and
IOperationState by MEF contract name and reads them reflectively. The surface
needed is one bool, one struct of four ints and one event, and it is identical
in VS 2022 17.14 and VS 2026 18.0.

Run tools start the run and return immediately, matching the non-blocking
contract the build tools were corrected to in #96. Completion is observed by
subscribing to IOperationState rather than by blocking, so the UI thread is
never held; test_status reports the collapsed run state and current counts.

test_stats distinguishes "Test Explorer not initialized" from "no tests", so
an uninitialized window cannot be misread as a solution with zero tests.

The *InContext commands act on the caret, so test_run and test_debug resolve
the requested class or method through the workspace symbol search, open the
file and position the caret before issuing the command.

Test Explorer exposes no documented cancel command, so test_cancel probes
candidate names and uses the first the running Visual Studio recognises.

Commands are probed with Command.IsAvailable before execution, because
ExecuteCommand throws when a command is disabled, which for Test Explorer is
the normal state before discovery finishes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants