MCP tools
The extension runs an in-process MCP server that hands Visual Studio’s own understanding of your code to the agent: navigation, references, rename, diagnostics, build, the tests it has discovered and the live debugger. Not a text search over source files, the IDE’s semantic, running view of your program.
The 80 tools below are exposed automatically; there is nothing to configure. They are prefixed
mcp__vs__ on the wire, and appear in the CLI’s /mcp listing.
Language-agnostic by design. Tools are wired through Roslyn’s per-document language services
(via reflection on the assemblies VS has already loaded) or language-agnostic APIs (EnvDTE, VS
commands), never a C#/VB-only path. There is no list of supported languages here: whatever your
Visual Studio can do, the agent can ask for.
That cuts both ways. A tool is only as capable as the installed workloads and the language service
behind the file: nav_find_references returns what your VS would return on that file: rich for
a language with a full language service, thinner for one without. debug_* needs the workload that
debugs that project type. Where a capability genuinely isn’t there, the nav_* and test_* tools
feature-detect and return supported=false with a reason instead of pretending it worked. The
others report a failure as ok=false (or success=false for the document_* tools) with a
reason.
Each tool says what it costs you. Every tool carries the standard MCP annotations:
readOnlyHint when it changes nothing, destructiveHint when it can destroy or interrupt
something you’d miss, idempotentHint when running it twice is the same as running it once. The
A client can use them to decide what needs approval. Inside this extension they gate nothing: both
panes launch the CLI with every mcp__vs__ tool pre-approved, so none of them prompts. The hints
still tell the model what a call costs, and what should be asked first belongs in your CLAUDE.md:
see Teach the agent about the IDE.
The reading tools are read-only: nav_* except rename, every *_get_* and *_list* tool, plus
ide_read_output, document_read_buffer, document_check_dirty, debug_console_read and
debug_expand. Destructive covers what you’d want to be asked about: debug_stop, debug_restart,
debug_detach, build_clean, build_cancel, nav_rename_symbol, document_run_cleanup, the
breakpoint-removing pair, ide_clear_output, the two that take something out
(project_remove_file, solution_remove_project) and solution_set_configuration, which changes
the IDE for the user and stays changed. The
stepping tools carry nothing: they move the program forward, which is neither safe to repeat nor
destructive. debug_evaluate is deliberately not read-only: evaluating can call property getters,
and it accepts assignments.
A long catalogue costs nothing per turn. Four of these tools sit in the model’s prompt at all
times: editor_get_selection, editor_get_latest_selection, editor_get_open_files and
ide_get_diagnostics. The rest are deferred: the CLI keeps their names to hand and loads a tool’s
full definition when it goes looking for one. So the number of tools here is not a running cost,
and adding one does not make the model slower.
The four are the ones that answer what is the user looking at right now: context worth having before deciding what to do, rather than in response to deciding. Everything else is something you reach for once you know what you want, which is exactly when a search for it is free. A tool marked always-loaded costs roughly fifty tokens of context on every single turn, so the bar for adding a fifth is high.
Naming. domain_verb[_object], snake_case, domain first: nav_go_to_definition,
debug_get_locals. A domain normally needs three or more tools; the rest live under ide. project is the exception,
kept apart from solution because its two tools act on a project’s files rather than on the
solution.
Argument names are camelCase, except on the tools whose names were fixed by the CLI’s own
conventions or by Solution Explorer’s: editor_open_diff and editor_close_tab (old_file_path,
new_file_path, new_file_contents, tab_name), and the project_* and solution_* tools
(project_name, file_path, project_path). The schema each tool ships is the reference.
The server introduces itself. At connection it tells the agent once what these tools are and
which of them beat the shell equivalent: build_solution over msbuild, nav_find_references over
grep, ide_get_diagnostics over parsing build output.
A call has ten minutes. One that has not returned by then is answered as a failure, usually because Visual Studio is waiting on a modal dialog. The operation itself is not cancelled.
Navigation
Section titled “Navigation”| Tool | What it does |
|---|---|
nav_find_references |
Find all references to a symbol across the solution (semantic, not text search): give the file, the 1-based line where the symbol appears, and the symbol name. Returns each reference’s file/line (usages only: the symbol’s own definition is excluded; use nav_go_to_definition for that). Which symbol it resolves depends on where you point it, and the answer changes with it: asked at a type’s declaration it finds every use of the TYPE, but asked at a ‘new Foo()’ it resolves the CONSTRUCTOR and finds only the places Foo is constructed. Point it at the declaration for the full set. The same line can appear twice at different columns for the same reason: a field declaration is a type reference, and the ‘new()’ initialising it is a constructor reference. The file must belong to a project in the open solution. Returns supported=false for languages this isn’t available for, or transiently while the solution is still loading; retry shortly before using grep. |
nav_get_document_symbols |
List a file’s symbols as a tree: each with its name, kind (Class/Method/Property/…) and 1-based line, ordered top-to-bottom, the editor’s navigation outline. Useful to locate members in a large file without reading it all. Reaches further than the other nav_* tools: besides the languages in the workspace it also answers for C++, through the project system rather than the language services. For TypeScript and JavaScript it needs the file open in an editor: a .ts declared <None> in a csproj is no workspace document until a buffer exists for it. A C++ member declared in a header reports the line of its definition in the .cpp, the way F12 navigates, so an outline of a header can point at another file. The file must belong to a project in the open solution. Returns supported=false for languages this isn’t available for, or transiently while the solution is still loading; retry shortly. This is one file; nav_search_workspace_symbols finds a name across the whole solution. |
nav_get_language_coverage |
Report which languages in the open solution the other nav_* tools can answer for, so a supported=false can be read once instead of rediscovered a tool at a time. Per language: how many projects it has, and which of go_to_definition, find_references, get_document_symbols, rename_symbol and search_workspace_symbols it provides. It also lists the solution’s projects that are outside the language workspace altogether, with the extensions they hold: C++ ones are, and of the nav_* tools only get_document_symbols reaches them, through the project system, and, per project, the source files whose extension its own language does not cover, which is how a .csproj full of TypeScript reports every tool as available and still answers for none of those files. One thing it cannot measure: TypeScript and JavaScript answer get_document_symbols only while the file is open in an editor, so a report taken with the file closed understates them. Ask it once when a nav_* tool returns supported=false and you want to know whether to retry, use another tool, or fall back to text search for the rest of the session. |
nav_go_to_definition |
Find where a symbol is defined (semantic, not text search): give the file, the 1-based line where the symbol is used, and the symbol name. Returns the defining file/line. Reaches definitions in referenced assemblies too: with no source on disk, the declaration is generated under %TEMP% and the hit carries source=‘decompiled’ (rebuilt from IL; locals renamed) or source=‘source’ (the real thing, via SourceLink). Those files are generated and read-only: read them, never edit them. The file must belong to a project in the open solution. Returns supported=false for languages this isn’t available for, or transiently while the solution is still loading; safe to retry shortly before falling back to grep. nav_find_references goes the other way, from a definition to its callers, and nav_go_to_implementation past an interface to what implements it. |
nav_go_to_implementation |
Find the implementations of a symbol (semantic): for an interface or an interface member, the concrete classes/members that implement it; for a virtual/abstract member, the overrides. Give the file, the 1-based line where the symbol appears, and the symbol name. Use this, not nav_find_references, to see the actual code behind an interface. The file must belong to a project in the open solution. Returns supported=false for languages this isn’t available for, or transiently while the solution is still loading. |
nav_rename_symbol |
Rename a symbol everywhere it’s used across the solution (semantic, not text replace): give the file, the 1-based line where the symbol appears, its current name, and the new name. Updates the definition and every reference. Atomic: if the rename would cause unresolved conflicts nothing is applied. Writes every file it touches to disk immediately: files already open in the editor get a dirty buffer instead, but nothing is held back waiting for document_save. There is no Ctrl+Z for it: undoing means renaming back. A build, a git diff or a shell command sees the new name straight away. It reaches further than the file you name: a solution-wide rename touched four files in a two-project solution, so read changedFiles (path plus how many occurrences each, sorted by path) and totalOccurrences for what actually changed, rather than assuming it was the one file. The file must belong to a project in the open solution. Returns supported=false for languages this isn’t available for; applied=false (with a reason) when the symbol can’t be renamed or the new name is invalid. It changes every file it touches, so ask before running it: nav_find_references shows the same set without changing anything. |
nav_search_workspace_symbols |
Find a symbol by name across the entire solution (the ‘Navigate To’ search). Matches declarations, not text, so a hit is a real class/method/field and comes with its kind, its container and the declaration line itself. Returns up to 50 hits, each with name, kind, file, 1-based line, container_name and preview, ordered by file then line. Returns supported=false where no project provides NavigateTo; fall back to Grep then, which searches text and will also match usages and comments. Where only some projects provide it the search still answers, and reason names the ones it could not cover, so no results there means ‘not in the projects searched’ rather than ‘nowhere’. This is the way in when the file is unknown: from a hit, nav_go_to_definition, nav_find_references and nav_get_document_symbols all take the file and line it returns. |
Editor
Section titled “Editor”| Tool | What it does |
|---|---|
editor_close_all_diffs |
Close every diff window this server opened, and only those: a window of the user’s own is left alone even if its caption says Diff. Returns how many were closed. Use it to tidy up after a series of editor_open_diff calls; editor_close_tab closes one by name. |
editor_close_tab |
Close a diff tab this server opened, by the tabName that editor_open_diff was given. It does NOT close arbitrary editor tabs: only frames in our own diff registry are touched, so the user’s documents are safe from it, and closing something they opened is not on offer. closed=false says nothing was closed, which covers all three harmless cases: the diff had already gone, the name never matched one of ours, or it names a document the user opened. Not an error either way. Use editor_close_all_diffs to clear them all. |
editor_get_latest_selection |
Get the most recent non-empty selection from any editor, even after focus has moved away, the one to use when the user selected something and then came to the chat. It is remembered by editor_get_selection, so it stays null until that has been called at least once in this session; a fresh session sees nothing here even if text is selected on screen. |
editor_get_open_files |
List the files currently open in the IDE’s editor tabs, with active/dirty flags and language id, sorted by path. dirty means the buffer differs from disk: read that one with document_read_buffer rather than from the file, and document_save writes it out. |
editor_get_selection |
Get the current text selection in the active editor: the selected text and its range, or null when no editor has focus, which includes right after the user clicks elsewhere, since the selection belongs to the focused window. Calling this also remembers the answer for editor_get_latest_selection; reach for that one instead when the user may have moved on since selecting. |
editor_open_diff |
Open a side-by-side diff between an existing file and proposed new content, so the user can see an edit before it happens. The tabName given here is what editor_close_tab takes to close it again, and editor_close_all_diffs clears every one this server opened. The proposed side is editable and the call waits until the user decides: Ctrl+S or Accept answers FILE_SAVED with the content as they saved it (which may differ from what was proposed), closing the tab or Reject answers DIFF_REJECTED. It does not write the file: whoever called it applies the content it returns. |
editor_open_file |
Open a file in the editor. Optionally select whole lines with startLine/endLine (1-based). Set activate to focus the tab. This is for showing the user something: reading a file needs no editor: use the Read tool, or document_read_buffer when the file is open and may hold unsaved changes. |
Document
Section titled “Document”| Tool | What it does |
|---|---|
document_check_dirty |
Check whether an open file has unsaved changes. Returns isOpen=false when the file isn’t open in any editor; otherwise isDirty true/false. When it is dirty the Read tool gives the version on disk and document_read_buffer the one on screen: they differ, and document_save makes them agree. |
document_format |
Format a file using the IDE’s built-in formatter. Equivalent to Ctrl+K, Ctrl+D in Visual Studio. The file must live inside the open solution’s folder; success=false otherwise. Opens the file in the editor if it isn’t already: the formatter needs a live document, and changes that buffer without saving. Reading it back with the Read tool saves it first when autosave is on (the default); document_read_buffer looks without writing. document_run_cleanup does this plus the user’s own cleanup fixers. |
document_organize_imports |
Organize and remove unused using/import directives in a file via the IDE’s Edit.RemoveAndSort command. The file must live inside the open solution’s folder; success=false otherwise. Opens the file in the editor if it isn’t already, and leaves that buffer unsaved like document_format, including that reading it back with the Read tool saves it first when autosave is on; document_read_buffer looks without writing. |
document_read_buffer |
Read an open document’s editor buffer, including changes the user hasn’t saved. Omit filePath to read the document they are currently looking at. Use the Read tool instead for the version on disk, or when the file isn’t open in the IDE. Returns isDirty so you can tell whether what you read differs from disk. startLine/endLine read one region: on a large file that is the difference between fifty lines and everything above them, and startLine comes back so the text can be placed. Without them the first 2000 lines are returned (maxLines, 0 for all). |
document_run_cleanup |
Run the IDE’s Code Cleanup on a file (Ctrl+K, Ctrl+E): formatting plus the fixers of the user’s default cleanup profile. Richer than document_format, but the extra fixers are language-dependent (C#/VB get the most). The file must live inside the open solution’s folder; success=false otherwise. Opens the file in the editor if it isn’t already, and leaves that buffer unsaved, though reading it back with the Read tool saves it first when autosave is on; document_read_buffer looks without writing. Some installations refuse the command outright: success=false then carries the IDE’s own message, and document_format is the part of it that always works. |
document_save |
Save an open file if it has unsaved changes. Returns saved=true if a save happened, false if the file wasn’t open or was already saved: the two are not told apart here, document_check_dirty separates them beforehand. Needed after document_format, document_organize_imports or document_run_cleanup, which change buffers and leave them unsaved. nav_rename_symbol does not need it: it writes to disk itself. |
Where an edit lands: the buffer or the file
Section titled “Where an edit lands: the buffer or the file”document_format, document_organize_imports and document_run_cleanup open the file they are
given and leave it dirty. The tab is not incidental: the IDE’s formatter and cleanup act on a live
document rather than on a path, so the edit sits in the editor, invisible to a build, a git diff
or a shell command until document_save. document_read_buffer is what sees it meanwhile.
Autosave cuts that short, by design. With Options → Chat → Autosave on (the default) a
PreToolUse hook saves a dirty file before Claude’s own Read, Edit or Write touches it, so
the agent never reads a stale copy of something the user is looking at. The cost is that reading
back what one of those three just changed writes it out first: the buffer goes clean and the next
build sees it. document_read_buffer avoids that: it is an MCP tool, and the hook only matches
Claude’s file tools.
The hook belongs to the Chat pane. In the CLI pane nothing saves for you: call document_save.
nav_rename_symbol is the exception: it writes to disk. Files that happen to be open go dirty
instead, but nothing waits for document_save, and there is no Ctrl+Z: undoing means renaming
back. Opening every touched file first was tried and dropped: it does buy dirty buffers, but at a
focused tab per file (four on a small solution-wide rename), and the undo is lost anyway the moment
anything reads one of them back. One predictable outcome beats two that depend on which tabs the
user left open.
| Tool | What it does |
|---|---|
build_cancel |
Stop the build currently running in the IDE and wait for it to actually stop. Reports ok=true once the IDE is free again, including when nothing was running, since that is the state you asked for; ok=false means the build is still going and the message says why. Use it when build_solution or build_clean reported a timeout (the build was left running), or when a build started outside the chat is in the way. A cancelled build leaves partial outputs, so run build_clean before trusting the next one. |
build_clean |
Clean the entire solution: delete the build outputs (bin/obj) of every project. Blocks until the clean ends. Use it when a build result looks stale, then call build_solution to rebuild; cleaning on its own produces no diagnostics. |
build_project |
Build a single project (by name) in the active configuration and return whether it succeeded plus what the Error List holds (file, line, description, severity). Blocks until done. Reports errors only unless severity (‘error’, the default, ‘warning’ or ‘all’) says otherwise; the message says how many items were left out, and ‘configuration’ says which one it built; solution_set_configuration changes it. ok/failedProjects/message are the outcome; ‘errors’ is the Error List, which the IDE updates a moment later and which can hold entries from a debug session as well as from the build: ide_read_output(‘Build’) has the compiler’s own log when the two disagree. The name is a project name, not a path; ide_get_project_structure lists them. build_solution builds everything instead. |
build_solution |
Build the entire solution and return whether it succeeded plus what the Error List holds (file, line, description, severity). Blocks until the build ends. Reports errors only unless severity (‘error’, the default, ‘warning’ or ‘all’) says otherwise; the message says how many items were left out. Prefer this to a dotnet build in the shell: it goes through the open IDE, so there is no path to resolve and no clash with a debug session. Builds whichever configuration the IDE has active and reports it back as ‘configuration’; solution_set_configuration changes it. ok/failedProjects/message are the outcome; ‘errors’ is the Error List, which the IDE updates a moment later and which can hold entries from a debug session as well as from the build. ide_read_output(‘Build’) has the compiler’s own log when the two disagree. build_project builds one project instead. |
These go through the IDE’s own Test Explorer, so they see whatever it sees: the same tests, found by the same adapters, on the build and the active configuration the IDE already has. That means every framework the installed workloads support: xUnit, NUnit, MSTest, GoogleTest, Boost, CppUnitTest, and not only .NET. Nothing is rebuilt and nothing is re-discovered.
All four take filter: a list of fragments matched against the fully-qualified test name; omit it
for everything. test_get_results also takes failedOnly.
| Tool | What it does |
|---|---|
test_list |
List the tests the IDE’s Test Explorer has discovered, with their project, class and the assembly they came from. This is the Test Explorer’s own tree: the same tests it would run, found by the same adapters, so it covers every framework the IDE supports (xUnit, NUnit, MSTest, GoogleTest, Boost, CppUnitTest…) and not just .NET. Nothing is rebuilt and nothing is re-discovered. An empty list on a solution that has tests usually means the Test Explorer has not discovered them yet: build the solution, or open the window once. |
test_run |
Run tests through the IDE’s Test Explorer, on the build and the active configuration the IDE already has: no separate restore, no second opinion about which configuration is current. Blocks until the run ends, then says whether it ran; test_get_results has the per-test outcome, including the failures with their message and stack trace. Covers every framework the IDE supports, not only .NET. Use test_run_with_debugger instead to stop on a failure and inspect it with the debug_* tools. |
test_run_with_debugger |
Run tests under the IDE’s debugger, so execution stops where one fails and the debug_* tools can read the state there: debug_get_locals, debug_get_callstack, debug_evaluate. This is the part no test runner outside the IDE can offer. Set the breakpoints you want first (debug_set_breakpoint) and filter down to the failing test, otherwise the whole suite runs under a debugger for nothing. test_run is the plain, faster version. |
test_get_results |
The last test run’s outcome, per test: name, project, duration, and for a failure its assertion message and stack trace: the stack carries the file and line, so a failure can be opened straight away instead of being hunted for. Failures are listed first. Reports what the Test Explorer holds now, so run test_run first, or read the results of a run started from the IDE. |
There is no test_cancel, and nothing cancels a run through these tools: test_run blocks until
the Test Explorer is done. Stop a run from the Test Explorer itself.
Solution
Section titled “Solution”| Tool | What it does |
|---|---|
solution_add_project |
Add an existing project file to the open solution, as Solution Explorer’s ‘Add → Existing Project’ does, and save the solution. The project file must already exist: this does not scaffold one. Returns the resolved project name. solution_remove_project is the reverse; project_add_file adds a file to a project that is already in the solution. |
solution_get_configuration |
Get the solution’s active configuration (the one build_solution and build_project compile), plus every configuration that can be asked for (‘Debug|Any CPU’, ‘Release|Any CPU’, …). Takes no arguments and changes nothing. Use it to check what a build will produce, or to see the valid names before solution_set_configuration, which is what actually switches it. Also reports startupProject (the one debug_start runs) under the name solution_set_startup_project takes, so it can be read before changing it and put back after. null when none is set, or when several are. |
solution_remove_project |
Remove a project from the solution and save it. The project’s files stay on disk: this takes it out of the solution, it does not delete it. Returns ok=false with the available project names when the name doesn’t match. The reverse of solution_add_project. |
solution_set_configuration |
Switch the solution’s active configuration (Debug, Release, …), the one build_solution and build_project compile and debug_start launches. Pass ‘Debug’ or ‘Release’, or the full ‘Release|Any CPU’ when a name has several platforms; returns ok plus the resolved configuration, or ok=false with the available ones if the name doesn’t match. This is a change to the user’s IDE and it persists: the toolbar dropdown moves and their next manual build follows it, so switch only when asked, and say so. solution_get_configuration reads the current one, and the valid names, without changing anything. |
solution_set_startup_project |
Set the solution’s startup project, the one debug_start (F5) launches. Pass the project name; returns ok plus the resolved startup project, or ok=false with the list of available projects if the name doesn’t match. |
Project
Section titled “Project”| Tool | What it does |
|---|---|
project_add_file |
Add a file that already exists on disk to a project, as Solution Explorer’s ‘Add → Existing Item’ does. Needed for project types that list every file explicitly: there, a .cs written to disk compiles in the IDE but is missing from an MSBuild command-line build, which fails silently. SDK-style projects glob their files in and need no call: one made anyway reports that the file is already included. This does not create the file: write it first, then add it. project_remove_file is the reverse. |
project_remove_file |
Remove a file from a project. The file stays on disk: this takes it out of the build, it does not delete it. The reverse of project_add_file. |
| Tool | What it does |
|---|---|
debug_apply_hot_reload |
Apply your pending code edits to the running program WITHOUT restarting it (Hot Reload / Edit-and-Continue). Use after editing a file during a debug session to see the change take effect live. Needs an active debug session. ok=true means the command ran, NOT that code changed: the IDE exposes no way to ask whether anything was pending, so calling this with nothing to apply succeeds too. What it actually did is in the output: read the ‘Hot Reload’ pane with ide_read_output. An edit Hot Reload cannot take (a changed method signature, a new type) needs debug_restart. Differs from debug_evaluate, which changes values, not code. |
debug_attach |
Attach the debugger to an already-running local process, by pid (preferred) or by a unique name substring. Use this instead of debug_start when the app is already running (web server, service, console). After attaching, the session is running: use debug_break or set a breakpoint to pause it, then inspect. Find the pid with debug_list_processes. |
debug_break |
Pause the running program immediately (Debug > Break All), without waiting for a breakpoint, so the call stack and variables can be inspected. Non-blocking, so mode comes back null rather than a guess: poll debug_get_state to see it reach ‘break’ and learn where it stopped. Calling it on an already-paused program succeeds and says so, rather than failing: there is nothing to do. Needs a session: debug_start first. |
debug_clear_breakpoints |
Remove all breakpoints in the solution: every one, tracepoints too, including any the user set themselves, so prefer debug_remove_breakpoint when you only mean to undo your own. debug_list_breakpoints shows what is there first. Works in any mode. |
debug_console_read |
Read what a debugged console application has written to its console window. This is the program’s real stdout, which is NOT in the Debug output pane: that pane only carries Debug.WriteLine, so a Console.WriteLine prompt is invisible there. Use it to see what the program printed, and to find out whether it is sitting at a prompt waiting for input; debug_console_send answers it. Needs a running debug session and a project that has a console: a GUI or web app has none. tailLines caps the lines returned (default 200, 0 for all); processId picks one process when the session debugs several. |
debug_console_send |
Send input to a debugged console application, as if it were typed at its console. Use it when debug_console_read shows the program waiting at a prompt: a Console.ReadLine that nobody answers blocks the debug session indefinitely. Pass ‘text’ (Enter is appended unless newline is false), or ‘key’ for a single named key; ‘ctrl+c’ and ‘ctrl+break’ interrupt the program rather than typing a character. Needs a running debug session and a project that has a console. |
debug_continue |
Resume execution from a paused (break) state (like F5 while paused). The program runs until the next breakpoint or it exits. Non-blocking, so mode comes back null rather than a guess: poll debug_get_state to see where it stops next. Only valid in break mode. |
debug_detach |
Detach the debugger from every process it is debugging, leaving them RUNNING (Debug ▸ Detach All). Use this after debug_attach on something you did not launch: a service, a browser, a long-running host, where debug_stop would terminate it instead. Breakpoints stop being hit and the debug_* inspection tools have nothing to report once detached. Poll debug_get_state for the mode: the transition is not immediate. |
debug_enable_breakpoint |
Enable or disable the breakpoint(s) at a file and 1-based line, or the ones on a function name. Disabling is how a breakpoint in hot code stops interrupting the session while you are trying to reach a different one, unlike debug_remove_breakpoint, which also throws away the condition and hit-count rule it was set with and cannot put them back. debug_list_breakpoints reports enabled for each. Works whether or not a session is running, and on a tracepoint as well, where disabling silences it without losing the message. |
debug_evaluate |
Evaluate an expression in the current stack frame while paused (break mode), like the Watch window: pass something like ‘order.Items.Count’. Returns the value and type. Note: evaluating can call property getters/methods in the program, so it may have side-effects; prefer reading fields/properties. You can also assign (e.g. ‘x = 5’) to change a variable’s value while paused, which is how you fix a value and retry a block with debug_set_next_statement. To see inside an object rather than read one field, debug_expand walks its members in a single call. Reads the frame debug_select_frame chose. Only valid in break mode. |
debug_expand |
Expand an expression into its members while paused (break mode), so an object comes back as a tree instead of just a type name: pass ‘order’, ‘order.Customer’, ‘this’, or ‘$exception’ when stopped on a throw (that one carries InnerException and the stack: expand it at depth 1, it is a framework type and depth 3 buries the message in static members). This is what debug_get_locals points at when it reports hasMembers=true: one call instead of a debug_evaluate per field. hasMembers on a returned node means there is more below it: expand that path to see it. truncated=true means a level had more members than maxMembers and what came back is a prefix. Note: reading a property runs its getter in the program, so this can have side-effects. Only valid in break mode. depth is 1 to 3 (default 2) and maxMembers 1 to 200 (default 50): out-of-range values are clamped, not rejected. |
debug_freeze_thread |
Freeze or thaw one thread, by the id debug_list_threads reports. A frozen thread does not run when the program resumes, which is how a race is pinned down: freeze the ones that interfere and step the one being watched. It STAYS frozen until something thaws it: a forgotten one makes the program behave in ways nothing else explains, so thaw it when the investigation is over, and debug_list_threads shows isFrozen if you lose track. Freezing everything leaves nothing to run. Only valid in break mode. Pass freeze=false to thaw. |
debug_get_callstack |
Get the call stack of the selected thread while paused (break mode): each frame’s index, function, module, and its own file/line where the frame has source. Index 0 is where execution is paused; isCurrent marks the frame debug_get_locals and debug_evaluate read, which debug_select_frame moves. This is one thread’s stack: debug_list_threads shows the others, debug_select_thread switches. Only valid in break mode: if the program is still running, poll debug_get_state until mode=‘break’. |
debug_get_exception_settings |
List the exception types the debugger will break on when they are THROWN, whether or not they are handled. Only those are returned: the full list runs to thousands of types that are all set to break on unhandled only, which is the default and says nothing. Use it before debug_set_exception_breakpoint to see what is already configured, and to explain why a debug session is stopping somewhere unexpected: a first-chance break the user turned on earlier looks like a crash until you know it is set. |
debug_get_locals |
List the parameters and local variables of the selected stack frame while paused (break mode): each with name, type and value, the parameters first and marked isArgument. Objects are flat by default: hasMembers=true means you can see inside with debug_expand(“name”), or pass depth here to walk them all at once. Reads the frame debug_select_frame chose, which is the paused one until you move it. Only valid in break mode. depth is 0 to 3 (default 0), maxMembers 1 to 200 (default 50). |
debug_get_state |
Get the current debug state: mode is ‘design’ (not debugging), ‘run’ (running), or ‘break’ (paused on a breakpoint/exception). In ‘break’ mode also returns the current file and 1-based line where execution is paused, and (if paused ON AN EXCEPTION) its type and message. That position is where the program stopped and stays put while you look around: debug_select_frame changes which frame the inspection tools read, not this. Poll this after debug_start to know when the program has hit a breakpoint or thrown. |
debug_get_thread_callstack |
Get one thread’s call stack by id WITHOUT making it the current thread. Use it to survey several threads (chasing a deadlock, seeing what a worker is blocked on) where debug_select_thread + debug_get_callstack would move the debugger’s current thread each time, taking the user’s Call Stack and Locals windows with it and never putting them back. Each frame carries its own file and line where it has source. Needs break mode; debug_list_threads has the ids. |
debug_list_breakpoints |
List all breakpoints in the solution: each with its file+line (or function name), condition and hit-count rule (if any), how many times it has been hit this session, and whether it’s enabled. Two fields answer “why did it not break”: currentHits separates ‘never reached’ from ‘reached, and the condition said no’, and bound (how many code locations it resolved to, during a session) catches the case before both, a breakpoint that will never stop anything because the line holds no code or its symbols are not loaded. One more is not a failure at all: breaks=false with a logMessage is a tracepoint, which prints to the Debug output pane and carries on by design. currentHits is refreshed when the program pauses: read while it runs, it is the count as of the last pause. Set them with debug_set_breakpoint, debug_set_function_breakpoint or debug_set_tracepoint, remove one with debug_remove_breakpoint or all with debug_clear_breakpoints. Worth a look when a run stops somewhere unexpected: a breakpoint left from earlier is the usual reason. |
debug_list_debugged_processes |
List the processes THIS debug session is attached to, not the machine’s processes, which is debug_list_processes. Each with its pid, name, thread count and transport. The one the other debug tools read comes FIRST and carries isCurrent: the call stack, the locals, the threads and the console all act on that single process without naming it, so when a solution launches several (a web app and its worker, a client and its service) this is what shows which one you are looking at, and that the others exist. Only useful once debugging has started. |
debug_list_modules |
List the modules (DLLs/EXEs) loaded into the process being debugged: name, path, version, whether symbols were loaded and whether the debugger counts it as user code. THIS IS THE TOOL FOR A BREAKPOINT THAT WILL NOT BIND: a breakpoint the debugger shows as unresolved is nearly always a module with symbolsLoaded=false, or a module that was never loaded at all. Also answers which build of a dependency is actually in the process, when that is in doubt. User-code modules come FIRST; pass userCodeOnly to drop the framework and runtime rows entirely. Works while running, not only in break mode; but the list only grows as the program loads more. |
debug_list_processes |
List local processes the debugger can attach to: pid, name (the file alone), path (the full one, which is what tells two same-named processes apart) and whether something is already debugging them. Optionally filter by a substring, matched against the full path, so a folder narrows the list as well as a name. Use this to find the process to pass to debug_attach: beingDebugged=true is why an attach would be refused, and is worth checking first, since the refusal talks about the attach rather than about the state. For the processes THIS session is debugging, use debug_list_debugged_processes; name has the same shape in both, so the two listings can be matched up. |
debug_list_threads |
List the threads of the program being debugged while paused (break mode): each with its id, name, location and whether it is frozen. The one the inspection tools read comes FIRST and carries isCurrent: everything else in this domain looks at a single thread, and this is what shows the others exist. Most threads carry no name of their own, the main one included (it is set in code and rarely is), and come back as ‘(unnamed)’: the location is what identifies them. Pass an id to debug_select_thread to look at one, or to debug_freeze_thread to hold it still. Only valid in break mode. |
debug_remove_breakpoint |
Remove the breakpoint(s) at a file and 1-based line, tracepoints included. Use debug_clear_breakpoints to remove all. There is no function form: a function breakpoint can be disabled with debug_enable_breakpoint or cleared with all the others, not removed alone. |
debug_restart |
Restart the current debug session (stop, then start again, like Debug > Restart). If not debugging, just starts. Non-blocking, so mode comes back null rather than a guess: poll debug_get_state for where it lands. |
debug_run_to_line |
Resume the paused program and stop again when it reaches this line, the Run to Cursor command. Saves stepping through a loop or a long method one statement at a time. Non-blocking: poll debug_get_state, and check WHERE it stopped, because anything on the way there (another breakpoint, a thrown exception) pauses it first, and if the line is never reached the program just runs on to the end. Adds no breakpoint of its own. Only valid in break mode. |
debug_select_frame |
Choose which call-stack frame debug_get_locals, debug_evaluate and debug_expand read, by the index debug_get_callstack reports (0 = where execution is paused). Locals belong to a frame: stopped inside a method that was called, the caller’s variables are out of scope until you select its frame: that is what “not in scope in the current frame” means. Same as double-clicking a line in the Call Stack window. Frames belong to a thread, so this moves within the selected one; debug_select_thread first if the frame you want is on another. The selection lasts until the program runs again. Only valid in break mode. |
debug_select_thread |
Choose which thread debug_get_callstack, debug_get_locals and the rest read, by the id debug_list_threads reports. They all look at one thread, so on multi-threaded code the others are invisible until you switch. The frame selection starts over at the top of the new thread’s stack (frames belong to a thread) so debug_select_frame after this, not before. Only valid in break mode. |
debug_set_breakpoint |
Add a breakpoint at a file and 1-based line. Optionally pass a condition (an expression that must be true for the breakpoint to trigger), or a hitCount to skip the first passes, the way to stop on the 500th iteration of a loop without a counter variable to test. Works whether or not a debug session is running. Combine with debug_start + debug_get_state to pause execution at this point. hitCountType says how the count is read: ‘equal’ (default) breaks on exactly that pass, ‘greaterOrEqual’ on that pass and every one after, ‘multiple’ on every Nth. A blank line or a type declaration is refused: ok=false and the reason says so, so retry on a line that has a statement. A comment line is moved to the next statement instead: the returned line is where it sits, and that is the line debug_remove_breakpoint takes. A method’s opening brace is fine. Accepting the line does not mean the debugger can stop on it: binding happens later, and debug_list_breakpoints reports bound once the session is running. |
debug_set_exception_breakpoint |
Configure the debugger to break when a specific exception type is thrown (first-chance), even if it’s caught, useful to find where an exception originates. Pass the fully-qualified type (e.g. ‘System.NullReferenceException’). breakWhenThrown=false turns it off. Works in any mode; needs a solution loaded. After it breaks, debug_get_state reports the exception type/message. group names the exception category (default ‘Common Language Runtime Exceptions’): needed for exceptions that are not CLR ones, such as C++. |
debug_set_function_breakpoint |
Add a breakpoint that triggers when a function is entered, identified by name (e.g. “MyClass.Calculate”) instead of a file and line. Optionally pass a condition, or a hitCount to skip the first calls, the way to stop on the 500th call without a counter to test. Works whether or not a debug session is running. Use when you know the method but not the exact line, or to avoid opening the file. debug_set_breakpoint takes a file and line instead, debug_list_breakpoints shows what is set, and debug_start begins the session that will hit it. hitCountType reads the count as in debug_set_breakpoint. A name that matches nothing is accepted just the same: it stays unresolved instead of failing, and debug_list_breakpoints reports bound=0 once the session is running. The returned file and line say where the name resolved to, worth checking when the name is unqualified; both are null before a session is running. |
debug_set_next_statement |
Move the instruction pointer to this line WITHOUT running the code in between, the Set Next Statement command. Skips a call that would fail, or jumps back to retry a block after fixing a value with debug_evaluate. The line must be in the file execution is paused in: the jump cannot leave the method, and asking for another file is refused here because Visual Studio would not: it reads the number as a line of the CURRENT method and moves there instead. SIDE-EFFECTFUL, unlike the rest of the debug tools: the skipped statements never run, so anything they would have assigned keeps its old value, and jumping backwards runs side effects a second time. Nothing checks that the jump makes sense; prefer asking first. Stays in break mode, and only valid in break mode. |
debug_set_tracepoint |
Add a tracepoint at a file and 1-based line: each time the line is reached it prints logMessage to the Debug output pane and the program carries on. It does NOT stop, so debug_get_state never reports a break for it and there is nothing to inspect there: use debug_set_breakpoint when you need to look at the state. This is the way to follow a value across many passes of a loop in one run, or to watch code that behaves differently when paused (a race, a timeout). Expressions in braces are evaluated (“total = {total}, i = {i}”), and ide_read_output on the Debug pane has the lines. Optionally pass a condition or a hitCount to print on some passes only. To Visual Studio a tracepoint is a kind of breakpoint, so the breakpoint tools handle it: debug_list_breakpoints shows it with breaks=false, debug_remove_breakpoint removes it, debug_enable_breakpoint switches it off and on. Each pass costs a few milliseconds (measured: about 150 a second), so a hot loop runs visibly slower while one is set on it. |
debug_start_no_debugger |
Run the solution’s startup project WITHOUT the debugger (equivalent to Ctrl+F5): breakpoints are ignored and exceptions do not break into the IDE, so none of the debug_get_* or debug_step tools will have anything to report afterwards; use debug_start when you need any of that. To run a different project, solution_set_startup_project first. Returns ok or ok=false with a reason. |
debug_start |
The debug entry point: the usual cycle is debug_set_breakpoint → debug_start → poll debug_get_state until mode=‘break’ → debug_get_callstack / debug_get_locals → debug_step. Inspection only works in break mode, and reads the frame debug_select_frame chose on the thread debug_select_thread chose. Start debugging the solution’s startup project (equivalent to F5). Non-blocking: returns once launched; the program then runs until it hits a breakpoint or exits. Poll debug_get_state to detect when it pauses (mode=‘break’). No-op if already debugging. |
debug_step |
Step the paused program by one statement. Direction: ‘over’ (run the line without entering called methods, default), ‘into’ (step into the call), ‘out’ (run to the end of the current method). Waits for the step to land and returns the file/line it reached, so a step over a slow call answers when it is done rather than straight away. If it has not landed within ten seconds (stepping over something that blocks, or the program ran on to a breakpoint) it comes back without a position and says to poll debug_get_state. Only valid in break mode. |
debug_stop |
Stop the current debug session (equivalent to Shift+F5). No-op if not debugging. What happens to the program depends on how the session began: one that debug_start launched is terminated, while one reached through debug_attach is only detached from and keeps running: Visual Studio does not kill a process it did not start. debug_detach always leaves it running and reports its PID. Non-blocking, so mode comes back null rather than a guess: poll debug_get_state to see the session reach ‘design’. |
| Tool | What it does |
|---|---|
ide_activate_output |
Bring a Visual Studio Output window pane (by name) to the foreground so the user sees it. Use at a debug checkpoint to show the relevant build/debug output before asking the user to confirm. The pane name is required. Returns ok; ok=false with availablePanes when the pane isn’t found; ide_read_output with no pane lists them. This shows a pane to the user; reading it is ide_read_output’s job and needs no activation. |
ide_clear_output |
Clear a Visual Studio Output window pane (by name). Run it before an action so a later ide_read_output returns only the fresh output, not the old history. The pane name is required (no clear-all). Returns ok; ok=false with availablePanes when the pane isn’t found. |
ide_get_diagnostics |
Get language diagnostics from the IDE’s Error List. Pass uri (file://…) to limit to one file; omit it to get all. Pass severity (‘Error’/‘Warning’/‘Info’) and/or maxResults to avoid pulling in hundreds of warnings when you only care about the errors. Returns an array of files, each with its diagnostics ([] when there are none). Visual Studio only analyses files that are open in an editor, so this can be empty for a file nothing has looked at; build_solution fills the same window from the compiler, for every file, which is what to run when this comes back empty. |
ide_get_edition |
Get the Visual Studio edition (e.g. “Enterprise”, “Professional”, “Community”). The edition and the installed workloads decide what the tools can do at all: a supported=false from a nav_* or debug_* tool is usually this rather than a bug. ide_get_version gives the version alongside it. |
ide_get_project_structure |
Get the solution structure: each project with its name, path, and the files it contains. Recurses solution folders. Useful to learn the layout, and to get the project names build_project and solution_set_startup_project want: both take a name, not a path. |
ide_get_version |
Get the running Visual Studio version: name (e.g. “Visual Studio 2026”), marketing year, and raw DTE version (e.g. “18.0”). For what the installation can actually do, the edition matters more than the version; see ide_get_edition. |
ide_get_workspace_folders |
Get the workspace folders currently open in the IDE, the solution folder, for Visual Studio. Empty when no solution is loaded, which is also why the tools needing one (build_, nav_, document_format) would fail; ide_get_project_structure lists what is inside it. |
ide_read_output |
Read text from a Visual Studio Output window pane (e.g. ‘Build’, ‘Debug’, or the running program’s output). Omit ‘pane’ to list the available pane names first: note those come back in the IDE’s language (‘Compilazione’ for Build on an Italian VS), but the built-in panes are also reachable under their English names. ‘tailLines’ caps how many lines are returned from the end (default 200), and ‘pattern’ (a case-insensitive regex) keeps only matching lines, which is how a specific message is found in a long build log without pulling all of it; matchedLines says how many there were. The filter runs before tailLines, so the result is the last N matching lines. Useful to see build/debug output or the debuggee’s console writes that don’t go through the shell, including what a frozen thread stopped printing, which is how debug_freeze_thread is checked. ide_clear_output first to read only what happens next, ide_activate_output to put a pane in front of the user. |
ide_write_output |
Write text to a Visual Studio Output window pane, creating the pane if it doesn’t exist. Use it to leave progress or a note where the user can see it: it appears in the IDE rather than only in the conversation, and it survives the turn. Prefer a pane name of your own over ‘Build’ or ‘Debug’, which VS writes to. Set ‘activate’ to bring it to the front; without it the write is silent. ide_read_output reads panes back. |
Telling the agent when to reach for them
Section titled “Telling the agent when to reach for them”The tools announce themselves, but the habits the agent brings are the ones of a terminal. A few
lines of CLAUDE.md change that: see
Teach the agent about the IDE.