This guide covers Rust debugging support in the MCP Debugger using the CodeLLDB debug adapter.
Install Rust from rustup.rs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shThe Rust adapter automatically downloads CodeLLDB when you build it:
cd packages/codelldb-common
npm run build:adapter # Downloads and extracts CodeLLDBThe Rust adapter bundles the CodeLLDB binaries into packages/codelldb-common/vendor/codelldb. Vendoring runs automatically when you install or build the workspace. The published CLI installs CodeLLDB via per-platform @debugmcp/codelldb-* optional dependencies — npm picks the one matching your os/cpu, so no manual setup is needed. If you installed with --omit=optional, set the CODELLDB_PATH environment variable to a local CodeLLDB installation (for example from the VSCode extension) or run pnpm --filter @debugmcp/codelldb-common run build:adapter from a cloned repository to download your platform binaries.
pnpm install(postinstall hook)pnpm vendororpnpm vendor:adapterspnpm --filter @debugmcp/codelldb-common run build:adapternode packages/codelldb-common/scripts/vendor-codelldb.js
Default behavior: Locally, the vendoring script downloads all supported platforms (win32-x64, linux-x64, linux-arm64, darwin-x64, darwin-arm64) so Docker builds can reuse the same artifacts. Set
CODELLDB_VENDOR_ALL=falseto vendor only the current host platform. In CI environments, the script vendors only the current platform unlessCODELLDB_VENDOR_ALL=trueis explicitly set.
# Force a fresh download for the current platform
pnpm vendor:force
# Skip vendoring temporarily (for air-gapped dev shells)
SKIP_ADAPTER_VENDOR=true pnpm installCODELLDB_VERSION: override the release tag (downloaded by the vendor script, which defaults to1.11.8)CODELLDB_FORCE_REBUILD=true: ignore cached binaries and re-downloadCODELLDB_PLATFORMS=win32-x64,linux-x64,linux-arm64,darwin-x64,darwin-arm64: vendor specific platforms (comma-separated)CODELLDB_VENDOR_ALL=false: opt out of the "vendor every platform" default and fall back to host-only downloadsCODELLDB_VENDOR_LOCAL_ONLY=true: disable network downloads entirely and fail if the requested platform isn't already vendored (used by Docker builds that copy pre-fetched artifacts)CODELLDB_KEEP_TEMP=true: retain the downloaded VSIX and extracted temp folders for inspectionCODELLDB_EXTRACT_TIMEOUT_MS: watchdog for VSIX extraction (default120000); a stalled unzip is aborted, retried, and surfaced as a failure instead of dying silentlySKIP_ADAPTER_VENDOR=true: opt out entirely (used by CI jobs that pre-bake artifacts)
- Run
pnpm vendor:statusto see which adapters are ready for the current machine. - Re-run
pnpm --filter @debugmcp/codelldb-common run build:adapterand check the[CodeLLDB vendor]logs:HTTP response: ...confirms GitHub access.Artifact magic header: 504b0304 (PK..)verifies a valid VSIX download.
- If you see
Failed to vendor, re-run withCODELLDB_KEEP_TEMP=trueto inspect the downloaded file underpackages/codelldb-common/vendor/codelldb/temp. - Network errors or 404 responses usually indicate a typo in
CODELLDB_VERSIONor a transient GitHub outage—try again withpnpm vendor:force. - On Windows, ensure PowerShell is allowed to create executables in the workspace (no antivirus quarantine).
- ✅ Breakpoints: Set breakpoints in Rust source files
- ✅ Stepping: Step over, into, and out of functions
- ✅ Variable inspection: View local variables, including complex types
- ✅ Stack traces: Full call stack with Rust symbols
- ✅ Cargo integration: Debug Cargo projects directly
- ✅ Debug/Release builds: Support for both build configurations
- ✅ Async debugging: Debug tokio and async-std applications
- Smart Type Display: Collections like
Vec,HashMap, andStringare displayed in a readable format - Ownership Tracking: See borrowed vs owned values
- Pattern Matching: Step through match expressions
- Macro Expansion: Debug through macro-generated code
{
"tool": "create_debug_session",
"arguments": {
"language": "rust",
"name": "My Rust Debug Session"
}
}# For debug build (recommended for debugging)
cargo build
# For release build (optimized, harder to debug)
cargo build --release{
"tool": "set_breakpoint",
"arguments": {
"sessionId": "your-session-id",
"file": "src/main.rs",
"line": 10
}
}For a simple binary:
{
"tool": "start_debugging",
"arguments": {
"sessionId": "your-session-id",
"scriptPath": "target/debug/my_program"
}
}For a Cargo project with arguments:
{
"tool": "start_debugging",
"arguments": {
"sessionId": "your-session-id",
"scriptPath": "target/debug/my_program",
"args": ["--verbose", "input.txt"],
"dapLaunchArgs": {
"cargo": {
"bin": "my_program",
"release": false
}
}
}
}Our test suite uses tests/e2e/rust-example-utils.ts to make sure every rust smoke test compiles and reuses binaries deterministically. The helper does three things you can mirror in your own workflows:
-
Build with the GNU toolchain when available. On Windows it looks up
dlltool.exe(via@debugmcp/adapter-rust’sfindDlltoolExecutable) and runscargo +stable-gnu build --target x86_64-pc-windows-gnu. If that fails it logs a warning and falls back to an explicit MSVC-targeted build (+stable-msvc build --target x86_64-pc-windows-msvc). You can follow the same pattern manually:# GNU-preferred build on Windows rustup target add x86_64-pc-windows-gnu cargo +stable-gnu build --target x86_64-pc-windows-gnu
# Non-Windows hosts usually only need the default debug build cargo build -
Resolve the correct binary path for
start_debugging. Breakpoints should always reference the.rssource file (absolute paths avoid MCP resolution issues), butstart_debugging.scriptPathmust point to the compiled artifact:- Windows GNU:
examples/rust/<name>/target/x86_64-pc-windows-gnu/debug/<name>.exe - Windows MSVC:
examples/rust/<name>/target/x86_64-pc-windows-msvc/debug/<name>.exe - Windows generic fallback:
examples/rust/<name>/target/debug/<name>.exe - Unix-like hosts:
examples/rust/<name>/target/debug/<name>
Tokio/async builds use exactly the same rule—the helper’s
prepareRustExample('async_example')just compiles a different crate and returns{ sourcePath, binaryPath }so the smoke tests can reuse those paths. - Windows GNU:
-
Cache builds between tests. Once an example has been compiled, the helper stores both paths in-memory so the rest of the suite can run without triggering another
cargo build. For local work you can emulate this with Cargo’s default incremental builds: as long as you relaunch the sametarget/<triple>/debug/<binary>the MCPstart_debuggingcall does not care when the binary was produced.
Tip: The
prepareRustExamplefunction is an internal test helper, not a supported public utility. You can replicate its approach in your own build tooling. The important detail is that breakpoints reference source (src/main.rs) whilestart_debugging.scriptPathreferences the compiled executable. This is true for synchronous and async (Tokio) binaries alike.
The Rust adapter provides special support for Cargo projects:
{
"dapLaunchArgs": {
"cargo": {
"bin": "my_program", // Binary target name
"release": false // false for debug build, true for release
},
"args": ["--help"], // Program arguments (top-level, not inside cargo)
"env": { // Environment variables
"RUST_LOG": "debug"
}
}
}The cargo object supports these fields:
bin: Binary target nameexample: Example target nametest: Test target namerelease: Build in release mode (boolean, default: false)
For Cargo workspaces with multiple packages:
{
"dapLaunchArgs": {
"cargo": {
"bin": "my-crate" // Specify which binary target to debug
}
}
}# Build the test executable
cargo test --no-run
# Find the test executable (usually in target/debug/deps/)
ls target/debug/deps/*-*
# Debug it{
"tool": "start_debugging",
"arguments": {
"sessionId": "your-session-id",
"scriptPath": "target/debug/deps/my_crate-abc123def456",
"args": ["test_name", "--nocapture"]
}
}{
"tool": "start_debugging",
"arguments": {
"sessionId": "your-session-id",
"scriptPath": "target/debug/my_program",
"dapLaunchArgs": {
"env": {
"RUST_BACKTRACE": "1",
"RUST_LOG": "debug",
"DATABASE_URL": "postgres://localhost/mydb"
}
}
}
}When debugging async Rust code with tokio:
#[tokio::main]
async fn main() {
// Set breakpoint here to debug async initialization
// Your async code here
my_async_function().await;
}{
"tool": "get_variables",
"arguments": {
"sessionId": "your-session-id",
"scope": 5
}
}Response will show Rust types clearly:
{
"variables": [
{"name": "my_vec", "value": "[1, 2, 3, 4, 5]", "type": "Vec<i32>"},
{"name": "my_string", "value": "\"Hello, Rust!\"", "type": "String"},
{"name": "my_option", "value": "Some(42)", "type": "Option<i32>"},
{"name": "my_result", "value": "Ok(\"success\")", "type": "Result<&str, Error>"}
]
}A binary built by cargo build on the host embeds host-absolute source paths (e.g. /home/user/proj/src/main.rs) in its DWARF debug info. When the mcp-debugger container mounts the project at /workspace, breakpoint requests use /workspace/... paths and CodeLLDB cannot match them — file+line breakpoints silently never bind (function breakpoints and pause still work; issue #363).
In container mode (MCP_CONTAINER=true) the adapter auto-derives a best-effort sourceMap for prebuilt binaries by scanning the binary for embedded host paths whose suffixes exist under the workspace mount (MCP_WORKSPACE_ROOT, default /workspace). If derivation misses, pass the mapping explicitly — a caller-supplied sourceMap always wins:
{
"scriptPath": "/workspace/target/debug/myapp",
"dapLaunchArgs": {
"sourceMap": { "/home/user/proj": "/workspace" }
}
}Alternatively, build inside the container (or with RUSTFLAGS="--remap-path-prefix=/home/user/proj=/workspace") so the DWARF paths match the mount directly.
-
Ensure debug symbols are included:
- Use debug builds (
cargo build) not release - Or add debug symbols to release:
[profile.release] debug = true
- Use debug builds (
-
Check optimization level:
- Optimizations can prevent breakpoints
- Use
opt-level = 0for debugging
-
Verify file paths:
- Use absolute paths or paths relative to workspace root
- Ensure the file exists and matches the compiled code
This happens in release builds or with optimizations enabled:
[profile.dev]
opt-level = 0 # No optimization for better debuggingIf CodeLLDB is not found:
# Re-run the vendor script
cd packages/codelldb-common
npm run build:adapter
# Check if it was downloaded (fixed layout under vendor/codelldb/ with per-platform subdirectories)
ls vendor/codelldb/
# e.g., vendor/codelldb/win32-x64/, vendor/codelldb/linux-x64/, vendor/codelldb/darwin-arm64/- Use
tokio::time::sleepinstead ofstd::thread::sleepin async contexts - Set breakpoints inside async blocks, not on the
async fndeclaration - Use
.awaitpoints as natural breakpoint locations
- Use debug builds for development: They're slower but much easier to debug
- Limit scope of debugging: Use conditional breakpoints to avoid stopping too often
- Use logging: Combine debugging with
env_loggerortracingfor better insights - Profile before optimizing: Use
cargo flamegraphorperfto find bottlenecks
{
"tool": "set_breakpoint",
"arguments": {
"sessionId": "your-session-id",
"file": "src/main.rs",
"line": 42,
"condition": "counter > 100"
}
}Evaluate Rust expressions in the current debug context. Note that CodeLLDB evaluates expressions through LLDB, so some Rust-specific syntax (e.g., closures, trait methods) may not be supported:
{
"tool": "evaluate_expression",
"arguments": {
"sessionId": "your-session-id",
"expression": "my_vec.len()"
}
}See the examples/rust/ directory for complete examples:
hello_world/: Basic Rust debuggingasync_example/: Async/await with Tokio- More examples coming soon!
- Macro debugging: Stepping through macros can be confusing due to expansion
- Inline functions: May not have breakpoint locations
- Generic functions: Need concrete instantiation for breakpoints
- Async stack traces: Can be deep due to runtime machinery