ClrVoyant 0.2.0
dotnet tool install --global ClrVoyant --version 0.2.0
dotnet new tool-manifest
dotnet tool install --local ClrVoyant --version 0.2.0
#tool dotnet:?package=ClrVoyant&version=0.2.0
nuke :add-package ClrVoyant --version 0.2.0
ClrVoyant
A local MCP server that lets an agent (e.g. Claude) debug .NET applications:
launch a program, set breakpoints, step, inspect variables / call stacks /
threads, and enumerate all in-flight async Tasks — everything a developer looks
at while debugging — deterministically, headless, with no IDE.
Style is the same as the Azure DevOps / Azure MCP servers: run it locally, point your agent at it, and ask in natural language:
"Launch project X, set a breakpoint where the bug is, run it, read the variables and the async tasks, and tell me why ABC happens."
The agent decides where to break and what to inspect (it reads your source with its own tools); ClrVoyant gives it the deterministic debugger primitives.
How it works
A single .NET process orchestrates two engines behind one IDE-neutral tool contract:
- netcoredbg (bundled, MIT) over the Debug Adapter Protocol — control + live introspection (breakpoints, step, threads, call stack, variables, evaluate).
- ClrMD — reads the heap at a stop to
enumerate all
Tasks and reconstruct the async await/continuation graph (the "Tasks window" equivalent, which DAP cannot provide). On Windows via a PSS snapshot; on Linux via a passive read of the (netcoredbg-held) process — both validated to coexist with netcoredbg holding the process.
See docs/architecture.md for the design as built, and docs/ for the full documentation (tool reference, POD/remote debugging, security model, ADRs).
What you can do with it
Concrete scenarios it delivers today (Windows or Linux, .NET 8/9/10):
- Debug an app you launch — start a program, break, step, read variables / call stack / threads. The base loop. (tools)
- Attach to a running process — discover the PID (
list_processes, filters to .NET) and attach; for long-running or externally-started processes. - Debug a unit test in one step —
debug_test(project[, testName])launches the test host suspended, attaches, and breaks in your test/production code. - Understand stuck
asynccode — enumerate every in-flightTaskwith its state and the await/continuation graph (list_tasks,get_async_graph). This is the "Tasks window" that DAP-only debuggers structurally can't provide. - Debug several processes at once — multi-session, with
wait_for_any_stopto orchestrate stops that arrive asynchronously; optional child-process auto-attach for parents that spawn workers. - Debug without source — only have a deployed build's DLLs + PDBs, not the
.cs?list_methodsreads the assembly metadata to find method names, thenset_function_breakpoint("Namespace.Type.Method")binds straight from the PDB — no source file needed. Attach/call stack/variables/Tasks all work the same. - Debug inside a container / Kubernetes POD — run it as an authenticated HTTP sidecar co-located with the target (PDBs required); only the agent is remote, the engine stays local to the process. (remote debugging)
Requirements
- Supported architectures:
win-x64,linux-x64,linux-arm64. The engine debugs locally, so the tool runs at the same architecture as the target — an arm64 tool for an arm64 process. (Nowin-arm64or macOS: netcoredbg ships no such build.) Each architecture is exercised by its own CI leg, including anarm64runner on real hardware — see ADR-0011. - .NET SDK 8 / 9 / 10 (the target apps you debug must be .NET 8+; .NET Framework is not supported)
Install
The cleanest way is as a .NET global tool — you get a clrvoyant command on
your PATH, so client config never points at a build folder. The published package
bundles netcoredbg for every supported RID (pinned + SHA-256 verified at build
time) and the tool picks the one matching your host, so it installs and runs
offline with no runtime download — only a missing-bundle fallback fetches.
# From a published package (once it's on NuGet):
dotnet tool install -g ClrVoyant
# Or from source right now:
dotnet pack src/ClrVoyant.Server -c Release -o ./nupkg
dotnet tool install -g ClrVoyant --add-source ./nupkg
Then point your MCP client at the command (.mcp.json / Claude config; VS Code's
mcp.json uses the key servers):
{
"mcpServers": {
"clrvoyant": { "command": "clrvoyant" }
}
}
The install scripts do build + register in one step:
./scripts/install.ps1 -Client cursor # claude-code | claude-desktop | cursor | windsurf | vscode
./scripts/install.ps1 # default: just prints the snippet
./scripts/install.sh --client cursor # bash equivalent (uses jq to merge)
Prefer not to install a tool? Build and point at the binary directly:
dotnet build ClrVoyant.slnx -c Release # bundles netcoredbg next to the server
and set "command" to the built ClrVoyant.Server executable. To use your own
netcoredbg, set CLRVOYANT_NETCOREDBG; to forbid the first-run download (air-gapped),
set CLRVOYANT_NO_FETCH=1 and pre-provide the engine.
For debugging a .NET app in Kubernetes, run ClrVoyant as a sidecar over HTTP — see deploy/ and docs/remote-debugging-pod.md.
Tip: have the agent call get_debug_instructions first — it returns a short
playbook of the tools and their ordering.
Transports
- stdio (default) — the agent spawns the server locally; one client per process.
- HTTP — set
CLRVOYANT_TRANSPORT=httpfor a Streamable-HTTP server a remote agent can reach (e.g. shipped inside a POD next to the app). HTTP requiresCLRVOYANT_AUTH_TOKEN(a bearer token) and bindsCLRVOYANT_HTTP_URL(defaulthttp://0.0.0.0:3001); it fails closed without a token, because a debug server that can launch processes andevaluatecode is an RCE surface.
For debugging a .NET app running in Kubernetes, run ClrVoyant as a sidecar: see deploy/ (Dockerfile + sidecar manifest + the security model).
Tools
Onboarding
get_debug_instructions()— a short playbook (typical loop, tool ordering, gotchas); call it first
Sessions (multi-session: every tool takes an optional sessionId; omit for
the active one)
debug_launch(program, args?, cwd?, stopAtEntry?)— launch a built.dlldebug_attach(pid)— attach to a running process (e.g. a child process)list_processes(dotnetOnly?)— list processes (pid, parent, name, isDotNet) to find a target to attach (e.g. the app process in a shared-PID-namespace POD)debug_test(testProject, testName?, configuration?)— rundotnet testwith the host suspended and attach in one step (theVSTEST_HOST_DEBUGrecipe, automated)restart_debugging(sessionId?)— relaunch a launched session in place: same id, same args, breakpoints preserved (not valid for attached sessions)set_auto_attach(enabled)— auto-attach .NET child processes of debugged processes (off by default; matched by parent PID, no suspend-at-startup)debug_stop(sessionId?),debug_status(sessionId?),list_sessions()wait_for_any_stop(timeoutMs?)— block until any session breaks (key for several processes running asynchronously)
Breakpoints & execution (wait-based: continue/step_* block until the next
stop and return the new location)
set_breakpoint(file, line?, content?, condition?, hitCondition?, logMessage?)— prefercontent(the source text of the line) over a rawline: it survives line-number drift;linethen only disambiguates duplicate matchesset_function_breakpoint(functionName, condition?, hitCondition?)— break by method name (Method/Type.Method/Namespace.Type.Method), no source line; binds from the PDB, so it's the no-source path.list_function_breakpoints()list_methods(assemblyPath, typeFilter?, methodFilter?)— discover method names in a built.dll(static metadata read, no process) to feedset_function_breakpointremove_breakpoint(bpId),clear_all_breakpoints(),list_breakpoints(),set_exception_breakpoints(filters)continue(threadId?, timeoutMs?),step_over/step_into/step_out,pause()
Introspection (when stopped)
get_threads(),get_callstack(threadId?, ...)get_scopes(frameId),get_variables(variablesReference)evaluate(expression, frameId?, context?)— ⚠️ executes code in the debuggee (side effects); preferget_variablesfor plain inspectionget_exception_info(threadId?)
Async / Tasks (ClrMD, when stopped)
list_tasks(status?)— all Tasks with status and async methodget_task(address)get_async_graph()— await/continuation graphget_async_callstack(address)— logical async chain from a Task
Typical agent loop
debug_launchthe app →set_breakpointat the suspect linecontinue→ returnsstopped at File.cs:NNget_callstack→get_scopes→get_variablesto read statelist_tasks/get_async_graphto see what async work is pending/stuckstep_over/continueto narrow it down → diagnose
Debugging tests
Tests run in a testhost process. Use debug_test(testProject, testName?) — it
runs dotnet test with VSTEST_HOST_DEBUG=1 (so the host suspends and announces
its PID), parses that PID and attaches in one step; set breakpoints right after it
returns and execution resumes into them. The dotnet test driver is killed when
you debug_stop the session. Build the tests in Debug for symbols and locals.
Under the hood this automates the manual recipe (kept here for reference):
$env:VSTEST_HOST_DEBUG=1; dotnet test <project> -c Debug prints Process Id: NNNN
and waits; debug_attach(NNNN) releases it via Debugger.IsAttached.
Alternatives & how to choose
There are several MCP debuggers. They make different trade-offs — this is about what you get and what you give up, not "we're best". Pick by what you debug.
| If you want… | Consider | What you give up |
|---|---|---|
Deep .NET debugging — async/Task state, headless, remote/POD |
ClrVoyant (this) | breadth: it is .NET 8+ only, and headless (no visual IDE) |
| Many languages via one DAP server (Python, JS, Go, Rust, Java, .NET…) | debugmcp/mcp-debugger | no async/Task/heap view — DAP can't enumerate Tasks, so .NET async state is invisible |
| Debugging inside VS Code with a human watching | microsoft/DebugMCP | requires VS Code running (not truly headless); bounded by the VS Code Debug API; no async Tasks |
| Python/Node/Java/browser, no IDE | bastiencb/claude-mcp-debugger | no .NET; no async/heap inspection |
The one thing ClrVoyant has that DAP-based tools structurally cannot: a "Tasks
window" — enumerate every in-flight Task and its await/continuation graph (via
ClrMD), the thing you actually need to debug a stuck async method. The price is
focus: it does .NET only, deeply, instead of many languages shallowly.
Fuller breakdown with sources: docs/comparison.md.
Known limitations
- One debugger per process. ClrVoyant can't attach to a process another
debugger already owns — e.g. an app started with the Visual Studio / Rider
debugger (F5 / Start Debugging). A .NET process allows a single debugger
(ICorDebug), so the second attach is refused. Run the target without the IDE
debugger (Start Without Debugging / Ctrl+F5 /
dotnet run), or detach the IDE first (VS: Debug → Detach All, which leaves the app running). This is an OS/runtime constraint, not a ClrVoyant limitation. - Optimized (Release) builds degrade line-level debugging. With optimizations
on, the JIT reorders and elides code, so line breakpoints may not bind where you
expect and locals can read as unavailable — a universal debugger limitation, not
specific to ClrVoyant. For reliable line breakpoints and locals, build the target
unoptimized (Debug, or
<Optimize>false</Optimize>) and ship its PDBs. Attach, call stacks,evaluate, and the asyncTask/heap view work regardless. - Child-process auto-attach (
set_auto_attach) discovers children by parent PID without suspending them, so a child's very first startup instants may run before the debugger attaches. Suspend-at-startup (tier 3) is not yet implemented. evaluateruns code in the target — treat as dangerous.pauserequires a thread known from a prior stop (netcoredbg does not enumerate threads of a freely-running process).- Targets must be .NET 8+ (netcoredbg does not debug .NET Framework).
Tests & coverage
dotnet test tests/ClrVoyant.Tests --settings coverlet.runsettings
Unit tests exercise the real logic — the wait-based execution model, the
breakpoint store, content-based breakpoint resolution, and Task status/async-method
decoding — with a fake engine; integration tests drive a real netcoredbg + ClrMD
against SampleApp (launch → breakpoint → inspect → enumerate Tasks). Line coverage
is high as a by-product, but the point is the behaviours above, not the number. See
docs/testing.md.
Layout
src/ClrVoyant.Core models, IDebugEngine, Session, SessionManager
src/ClrVoyant.Dap DAP client + DapEngine (netcoredbg)
src/ClrVoyant.Inspection ClrMD TaskInspector (Tasks + async graph) + method discovery
src/ClrVoyant.Server MCP stdio host + tools
tools/netcoredbg bundled debug engine (all RIDs staged into the package)
samples/SampleApp a target app for tests
spike/ the de-risking proof-of-concept
scripts/mcp-driver.ps1 stdio test harness
Acknowledgments
ClrVoyant stands on:
- netcoredbg (Samsung, MIT) — the .NET debug engine, driven over DAP.
- ClrMD (Microsoft, MIT) — heap/Task introspection, the basis of the "Tasks window".
- The Debug Adapter Protocol (Microsoft) and the Model Context Protocol (Anthropic) and its C# SDK.
Prior art that shaped the design space: microsoft/DebugMCP and debugmcp/mcp-debugger. See THIRD-PARTY-NOTICES.md for licenses.
License
MIT © Matteo Panzacchi
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.