This guide explains how to correctly use the MCP Debugger tools when testing debugging functionality across all supported languages (Python, Ruby, JavaScript, Rust, Go, Java, .NET/C#, and C/C++).
What to expect:
- The debugger stops at your breakpoints in user code
- Variables are accessible when stopped at user breakpoints
How it works:
- The multi-session architecture properly routes evaluate commands to the active debugging context
- You can immediately evaluate expressions when stopped at breakpoints
- When
stopOnEntryis false (the default), the debugger auto-continues past entry breakpoints so execution advances to user code automatically
What to expect:
- Variables appear in a hierarchical structure
- You may see "special variables" as a container that needs to be expanded
How to access variables:
- Call
get_variableswith the scope ID (e.g.,scope: 3for Locals) - If you get
{"name":"special variables","variablesReference":5}, this is a container - Call
get_variablesagain withscope: 5(the variablesReference) to expand it - This will reveal the actual variables (
a,b, etc.)
// File: test.js
function compute(a, b) {
const product = a * b; // Line 3 - set breakpoint here
return product;
}
compute(5, 10);Testing sequence:
# 1. Create session
session_id = create_debug_session(language="javascript")
# 2. Set breakpoint
set_breakpoint(sessionId=session_id, file="/path/to/test.js", line=3)
# 3. Start debugging
start_debugging(sessionId=session_id, scriptPath="/path/to/test.js")
# Should stop at line 3 if the breakpoint is hit; if the debugger stops at
# a Node.js internal frame first, use continue_execution to advance to user code.
# 4. Get stack trace
get_stack_trace(sessionId=session_id)
# Should show test.js in the stack, not Node.js internals
# 5. Evaluate expressions
evaluate_expression(sessionId=session_id, expression="a") # Returns: "5"
evaluate_expression(sessionId=session_id, expression="b") # Returns: "10"
evaluate_expression(sessionId=session_id, expression="typeof compute") # Returns: "function"
# 6. Step over
step_over(sessionId=session_id)
# Now at line 4
# 7. Evaluate product
evaluate_expression(sessionId=session_id, expression="product") # Returns: "50"# File: test.py
def main():
a = 1 # Line 2
b = 2 # Line 3 - set breakpoint here
c = a + b # Line 4
return c
if __name__ == "__main__":
main()Testing sequence:
# 1. Create session
session_id = create_debug_session(language="python")
# 2. Set breakpoint
set_breakpoint(sessionId=session_id, file="/path/to/test.py", line=3)
# Note: Python breakpoints initially report as unverified; they are
# verified asynchronously by debugpy once the module is loaded.
# 3. Start debugging
start_debugging(sessionId=session_id, scriptPath="/path/to/test.py")
# Stops at line 3
# 4. Get scopes (use the actual frame ID from get_stack_trace, not a hardcoded value)
stack = get_stack_trace(sessionId=session_id)
frame_id = stack["stackFrames"][0]["id"] # Use the top frame's actual ID
scopes = get_scopes(sessionId=session_id, frameId=frame_id)
# Returns: [{"name":"Locals","variablesReference":3}, {"name":"Globals","variablesReference":4}]
# 5. Get variables (first level)
vars = get_variables(sessionId=session_id, scope=3)
# May return: {"name":"special variables","variablesReference":5}
# 6. Expand special variables (if needed)
if vars.get("variablesReference"):
actual_vars = get_variables(sessionId=session_id, scope=vars["variablesReference"])
# Now returns: [{"name":"a","value":"1"}, {"name":"b","value":"2"}, ...]
# 7. Evaluate expressions
evaluate_expression(sessionId=session_id, expression="a") # Returns: "1"
evaluate_expression(sessionId=session_id, expression="a + b") # Returns: "3"
# Note: expressions can read AND modify program state -- "a = 99" assigns,
# and "obj.method()" runs the call in the debuggee. That is intentional:
# it lets you test a fix in place before editing the source. Results are
# always returned as strings, even for numbers.Solution: Use continue_execution to move past internal frames. Stack trace filtering hides internal frames by default for supported languages.
Solution: This is normal hierarchical organization. Use the variablesReference to expand:
# Step 1: Get initial scope
vars = get_variables(scope=3) # Returns special variables container
# Step 2: Expand using variablesReference
if "variablesReference" in vars:
actual_vars = get_variables(scope=vars["variablesReference"])Possible causes:
- Wrong scope: Ensure you're evaluating in the correct frame context
- Not yet defined: Variable hasn't been executed yet - step to after its assignment
- Out of scope: Variable is in a different function or scope
Solution:
- Use
get_stack_trace()to see current location - Step over assignment lines before evaluating variables
- Check the current frame ID and use it in evaluate_expression
-
Always check session state before operations:
- Must be
PAUSEDfor: evaluate, step operations, get variables - For
set_breakpoint: session must not beTERMINATED(breakpoints can be set in any non-terminated state, including before debugging starts)
- Must be
-
Use absolute paths for file references to avoid ambiguity
-
Wait for proper state after operations:
- After
start_debugging: Wait forPAUSEDstate if breakpoint set - After
continue_execution: Session becomesRUNNING - After
step_*: Wait forPAUSEDstate
- After
-
Handle variable hierarchies in Python:
- Always check for
variablesReferencein responses - Recursively expand containers to access nested variables
- Always check for
-
Frame context matters:
- If
evaluate_expressionfails, check you're using the correct frame - Use
get_stack_traceto find the right frame ID -- use theidfield from the stack frame object, not the array index - The top frame (first element in the
stackFramesarray) is usually what you want, but itsidis assigned by the debug adapter and is NOT necessarily 0
- If
- Session created successfully
- Breakpoints set and verified
- Debugging starts without timeout
- Stops at user breakpoints (not internals)
- Stack trace shows user code
- Variables are accessible (after expanding containers if needed)
- Expressions evaluate correctly
- Step operations work as expected
- Continue execution resumes properly
- Session closes cleanly
Prerequisites: Rust toolchain (rustc, cargo) installed. CodeLLDB is vendored automatically during pnpm install -- the root postinstall hook runs pnpm vendor:adapters. Re-vendor at any time with pnpm vendor:adapters (or just the LLDB copy: pnpm --filter @debugmcp/codelldb-common run build:adapter), or point CODELLDB_PATH at an existing CodeLLDB executable -- the resolver uses the value verbatim as the adapter path, so a directory will not work.
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="rust")
# 2. Set breakpoint (use absolute path to the source file)
set_breakpoint(sessionId=session_id, file="/path/to/src/main.rs", line=5)
# 3. Start debugging (scriptPath is the source file; the adapter resolves the
# enclosing Cargo project and may build/locate the binary before debugging)
start_debugging(sessionId=session_id, scriptPath="/path/to/src/main.rs")
# 4. Get stack trace and use actual frame IDs
stack = get_stack_trace(sessionId=session_id)
frame_id = stack["stackFrames"][0]["id"]
# 5. Inspect variables
get_local_variables(sessionId=session_id)Prerequisites: Ruby 2.7+ installed. rdbg must be available through the standard debug gem.
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="ruby")
# 2. Set breakpoint
set_breakpoint(sessionId=session_id, file="/path/to/app.rb", line=12)
# 3. Start debugging
start_debugging(sessionId=session_id, scriptPath="/path/to/app.rb")
# 4. Inspect variables
get_local_variables(sessionId=session_id)Prerequisites: Go 1.18+ installed. Delve debugger must be installed: go install github.com/go-delve/delve/cmd/dlv@latest.
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="go")
# 2. Set breakpoint
set_breakpoint(sessionId=session_id, file="/path/to/main.go", line=10)
# 3. Start debugging
start_debugging(sessionId=session_id, scriptPath="/path/to/main.go")
# 4. Inspect variables
get_local_variables(sessionId=session_id)Prerequisites: JDK 21+ installed. Uses JDI bridge -- the adapter attempts to locate pre-compiled bridge classes and may compile them on demand at command-build time if not found.
Key notes:
- Compile target code with
javac -gfor full variable inspection - For breakpoints, you can use a fully-qualified class name (e.g.,
"com.example.MyClass") instead of a file path mainClassis derived, not passed: the adapter reads the launch config'sprogram(which defaults toscriptPath) and turns a.javapath into its base name, so/path/to/Main.javayieldsMain. Any otherprogramvalue is used verbatim -- passadapterLaunchConfig: {"program": "com.example.Main"}when the class name does not match the file name. AmainClasskey you pass yourself is overwritten by this derivationclasspathgoes throughdapLaunchArgs(oradapterLaunchConfig) and becomes the JVM's-cp; absolute is safest -- since #642 the JVM runs in the launch config'scwd(which the session layer defaults to the script's directory when you pass none), so a relative classpath resolves against that, not against the server's install directory. The program'sargscome fromstart_debugging's top-levelargs. The launch keys the JDI bridge actually reads aremainClass,classpath,stopOnEntry,javaPath,vmArgs,args,cwd(applied as the JVM's working directory; a non-directory fails the launch loudly), andenv(merged into the JVM's environment; a JSONnullvalue removes the variable) --sourcePathis forwarded to the bridge but never read
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="java")
# 2. Set breakpoint (using FQCN)
set_breakpoint(sessionId=session_id, file="com.example.Main", line=10)
# 3. Start debugging with adapter-specific config
start_debugging(
sessionId=session_id,
scriptPath="/path/to/Main.java",
dapLaunchArgs={"classpath": "/path/to/classes"},
# mainClass comes from `program`; override it when the FQCN differs
# from the file name:
adapterLaunchConfig={"program": "com.example.Main"}
)
# 4. Inspect variables
get_local_variables(sessionId=session_id)Prerequisites: netcoredbg must be installed (set NETCOREDBG_PATH or add to PATH). A .NET SDK is needed to compile your target application.
Key notes:
- PDB symbols must be in Portable format (compile with
/debug:portable) - Uses TCP-to-stdio bridge on all platforms
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="dotnet")
# 2. Set breakpoint
set_breakpoint(sessionId=session_id, file="/path/to/Program.cs", line=10)
# 3. Start debugging (pass compiled target, not source file)
start_debugging(
sessionId=session_id,
scriptPath="/path/to/bin/Debug/net8.0/YourApp.dll",
dapLaunchArgs={"program": "/path/to/bin/Debug/net8.0/YourApp.dll"}
)
# 4. Inspect variables
get_local_variables(sessionId=session_id)Prerequisites: Nothing extra to debug a prebuilt executable -- CodeLLDB is vendored (the same copy the Rust adapter uses). A compiler on PATH (g++/clang++/c++, or gcc/clang/cc for C) is needed only when you hand the adapter a lone source file.
Key notes:
- One language id,
cpp, covers both C and C++ - Compile with
-gdwarf-4 -O0.-gdwarf-4matters on Windows: MinGW gcc 11+ defaults to DWARF-5, whose line tables LLDB cannot read out of PE-COFF binaries scriptPathtakes either the compiled executable or a lone.c/.cppsource file; a source file is auto-compiled into a.debug-mcp/directory next to it and rebuilt when stale (adapterLaunchConfig: {"forceRebuild": true}forces it)- Function breakpoints are the sturdiest addressing here -- a bare
mainresolves fine - On Windows prefer MinGW-w64/MSYS2 g++ (DWARF). MSVC PDB fidelity is partial;
CPP_MSVC_BEHAVIOR(warndefault /error/continue) controls the detection warning - Attach is by PID only:
attach_to_processwithprocessId. Host/port attach is rejected withUNSUPPORTED_OPERATION. On Linux, mindkernel.yama.ptrace_scope
Testing sequence:
# 1. Create session
session_id = create_debug_session(language="cpp")
# 2. Set breakpoint (source file + line, or a function name)
set_breakpoint(sessionId=session_id, file="/path/to/main.cpp", line=12)
# 3. Start debugging (compiled binary, or the .cpp source to auto-compile)
start_debugging(sessionId=session_id, scriptPath="/path/to/myapp")
# 4. Inspect variables
get_local_variables(sessionId=session_id)set_breakpoint takes more than file + line. The content-addressing levers are gated
by DEBUG_MCP_BP_ADDRESSING (content, the default, exposes everything; assert drops
statement, nearLine and function, keeping expectedContent; line drops
expectedContent too, leaving plain file + line) -- read the live set_breakpoint
schema rather than assuming.
set_breakpoint {"sessionId": "...", "file": "/abs/app.py", "statement": "total = sum(prices)"}
set_breakpoint {"sessionId": "...", "function": "apply_bulk_discount"}
set_breakpoint {"sessionId": "...", "file": "/abs/app.py", "line": 51,
"expectedContent": "total = sum(prices)"}
statementaddresses by content, like an Edit-tool match -- a distinctive substring is enough, and an exact whole-line match beats substring matches. It can only land on a line containing your text, and it survives source edits acrossrestart_debugging. If the text appears on several lines the error lists every match; addnearLineto pick one. Passstatementorline, not both.functionsets a DAP function breakpoint on entry to a symbol -- no file, no line. Supported by the Python, Go, Rust, C/C++, .NET, Java, and JavaScript adapters. It composes withconditiononly.expectedContentis an assertion, not addressing: if the target line does not contain the text, the breakpoint is not set and the error shows what is actually on that line and its neighbors -- the cheapest way to catch a stale line number.logMessageturns a breakpoint into a logpoint: it never pauses, and{curly brace}expressions are interpolated intoget_outputwhile the program runs at full speed. Supported by the Python, JavaScript, Go, Rust, C/C++, and mock adapters; not by Java, .NET, or Ruby.
Three tools manage breakpoints after they are set. All take effect immediately while the program is running or paused:
list_breakpoints {"sessionId": "..."}-- verified state and adapter-assigned ids for every breakpoint (addfileto scope it). Session-global function breakpoints appear separately asfunctionBreakpoints. Works before launch, during, and after exit.remove_breakpoint {"sessionId": "...", "breakpointId": "..."}-- or address it byfunction, or byfile+line(which removes every breakpoint at that location).clear_breakpoints {"sessionId": "..."}-- removes all of them, or all in onefile. Clearing zero breakpoints is success, not an error.
Read the program's own output with get_output. Anything the program prints --
console.log, print, panics, stack traces, logpoint messages -- lands there, not in the
result of the tool that resumed it:
get_output {"sessionId": "...", "since": 0}
It is buffered per launch (last 1000 entries), works while the program is running and
after it exits, and is cursor-based: pass the previous response's nextSince as since
to fetch only what is new. hasMore: true means the limit (default 100, max 1000) cut
the page short. The same transcript is also exposed as the MCP resource
debug://sessions/{id}/output, which supports resources/subscribe if your client would
rather be notified than poll.
breakOnExceptions (on both start_debugging and attach_to_process) decides
whether a throw pauses the session:
"uncaught"-- pause at the crash site instead of letting the session terminate. This is the launch default (Ruby is the exception: rdbg has no uncaught-only filter, so it stays"none")."all"-- also pause on caught/raised exceptions. Language-dependent, and noisy in code that uses exceptions for control flow."none"-- let a crashing program run to termination. Attach always defaults to"none"; attach sessions never apply a language default.
restart_debugging relaunches with the same configuration as the last
start_debugging and re-applies every current breakpoint. It works while running, paused,
or after the program exited, and is not available for attach sessions or sessions that
were never launched. The output buffer starts fresh, so read from since: 0 afterwards.
Statement-anchored breakpoints are re-resolved against the edited source, which is what
makes the edit / restart / re-check loop hold up.
Secret redaction is on by default. Credential-shaped values are masked in
get_variables, get_local_variables, evaluate_expression results, and captured output,
so a token in scope does not land in the transcript:
{ "name": "gh_token", "value": "<redacted:github-pat>", "type": "str", "redacted": true }Two layers do it: known token shapes (PATs, sk- keys, JWTs, PEM blocks, Bearer
credentials, connection-string passwords) and exact sensitive variable names
(password, api_key, ...; matching is exact after normalization, so tokenCount is
untouched). Only the display is masked -- the program still holds the real value -- and a
response that masked anything carries a redaction field saying so. Start the server with
DEBUG_MCP_NO_REDACT=1 to turn it off when the credential handling itself is what you are
debugging. A related flag, DEBUG_MCP_VARIABLE_ACCESS=explicit, makes names required on
get_variables/get_local_variables; the tool schema says so when it is on.
expose_session hands a live session to a human. It opens a read-only DAP mirror on
127.0.0.1 (ephemeral port) and returns a token the IDE must send as mirrorToken:
expose_session {"sessionId": "..."}
unexpose_session {"sessionId": "..."}
The IDE can inspect threads, stack, scopes, variables, and evaluate; continue/step/pause
and breakpoint changes are rejected, so execution control stays with the MCP session. It
is idempotent, and closes on unexpose_session, close_debug_session, restart, or
debuggee exit. Two cautions: the mirror shows raw, unredacted values, and DAP
evaluate runs arbitrary code in the debuggee -- treat the token as an execution
capability, not a view-only credential.
The MCP Debugger is fully functional for Python, Ruby, JavaScript, Rust, Go, Java, .NET/C#, and C/C++. The key insights are:
- JavaScript: Stack trace filtering hides internal frames; may need
continue_executionif initially stopped at internals - Python: Use variablesReference to expand variable containers
- Ruby: Supports launch and attach flows through
rdbg; use Bundler mode for Rails and RSpec-style entrypoints - Rust: CodeLLDB adapter is vendored; the GNU toolchain is required for reliable debugging -- MSVC-built binaries may produce errors with CodeLLDB. Set
RUST_MSVC_BEHAVIORenv var to control MSVC handling - Go: Uses Delve's native DAP support
- Java: Use FQCN for breakpoints;
mainClassis derived fromprogram/scriptPath, not passed -- sendclasspathviadapLaunchArgs - .NET: Requires netcoredbg; uses TCP-to-stdio bridge
- C/C++: One language id (
cpp) covers both; nothing to install for prebuilt binaries -- compile with-gdwarf-4 -O0. Attach is by PID only - All languages: Use actual frame IDs from
get_stack_trace(not hardcoded 0), and ensure proper state and context for operations
Following this guide will help you successfully test and use all debugging features without encountering the previously reported issues.