Skip to content

Latest commit

 

History

History
408 lines (343 loc) · 13.8 KB

usage.md

File metadata and controls

408 lines (343 loc) · 13.8 KB

Using the mcp-debugger

This document describes how to use the mcp-debugger with Large Language Models (LLMs) for step-through debugging.

Installation

Prerequisites

  • Node.js 22+
  • Python 3.7+ with debugpy (for Python debugging)

Installing from NPM

npm install -g @debugmcp/mcp-debugger

Building from Source

git clone https://github.com/debugmcp/mcp-debugger.git
cd mcp-debugger
pnpm install
npm run build

Configuration

MCP Client Configuration

Add the server to your MCP settings:

{
  "mcpServers": {
    "mcp-debugger": {
      "command": "mcp-debugger",
      "args": ["stdio"],
      "disabled": false,
      "autoApprove": ["create_debug_session", "set_breakpoint", "get_variables"]
    }
  }
}

If running from a source checkout instead of a global install, use the CLI entrypoint:

{
  "mcpServers": {
    "mcp-debugger": {
      "command": "node",
      "args": ["C:/path/to/mcp-debugger/packages/mcp-debugger/dist/cli", "stdio"],
      "disabled": false,
      "autoApprove": ["create_debug_session", "set_breakpoint", "get_variables"]
    }
  }
}

Complete Debugging Workflow Example

Here's a real example of debugging a Python script with a bug:

The Buggy Script

# swap_vars.py
# A simple script that swaps two variables, with an intentional bug for debugging.

def swap_variables(a, b):
    print(f"Initial values: a = {a}, b = {b}")
    # Intentionally buggy swap logic for demonstration
    # Correct logic would use a temporary variable: temp = a; a = b; b = temp
    # Or Python's tuple assignment: a, b = b, a
    
    a = b  # Bug: 'a' loses its original value here
    b = a  # Bug: 'b' gets the new value of 'a' (which is original 'b')
    
    print(f"Swapped values: a = {a}, b = {b}")
    return a, b

def main():
    x = 10
    y = 20
    
    print("Starting variable swap demo...")
    swapped_x, swapped_y = swap_variables(x, y)
    
    # Verification
    if swapped_x == 20 and swapped_y == 10:
        print("Swap successful!")
    else:
        print(f"Swap NOT successful. Expected x=20, y=10 but got x={swapped_x}, y={swapped_y}")

if __name__ == "__main__":
    main()

Step 1: Create a Debug Session

// Tool: create_debug_session
// Request:
{
  "language": "python",
  "name": "Investigate Swap Bug"
}
// Response:
{
  "success": true,
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "message": "Created python debug session: Investigate Swap Bug"
}

Step 2: Set Breakpoints

Set a breakpoint where the bug occurs. In host mode the path must be absolute — a relative file or scriptPath is rejected with Path must be absolute. Received: "..." (see File Paths below):

// Tool: set_breakpoint
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "file": "C:\\path\\to\\swap_vars.py",
  "line": 10
}
// Response:
{
  "success": true,
  "breakpointId": "28e06119-619e-43c0-b029-339cec2615df",
  "file": "C:\\path\\to\\swap_vars.py",
  "line": 10,
  "verified": false,
  "message": "Breakpoint set at C:\\path\\to\\swap_vars.py:10"
}

Step 3: Start Debugging

// Tool: start_debugging
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "scriptPath": "C:\\path\\to\\swap_vars.py"
}
// Response:
{
  "success": true,
  "state": "paused",
  "message": "Debugging started for C:\\path\\to\\swap_vars.py. Current state: paused",
  "data": {
    "message": "Debugging started for C:\\path\\to\\swap_vars.py. Current state: paused",
    "reason": "breakpoint"
  }
}

Step 4: Inspect the Stack

// Tool: get_stack_trace
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}
// Response:
{
  "success": true,
  "stackFrames": [
    {
      "id": 3,
      "name": "swap_variables",
      "file": "C:\\path\\to\\swap_vars.py",
      "line": 10,
      "column": 1
    },
    {
      "id": 4,
      "name": "main",
      "file": "C:\\path\\to\\swap_vars.py",
      "line": 21,
      "column": 1
    },
    {
      "id": 2,
      "name": "<module>",
      "file": "C:\\path\\to\\swap_vars.py",
      "line": 30,
      "column": 1
    }
  ],
  "count": 3
}

Step 5: Get Variable Scopes

// Tool: get_scopes
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "frameId": 3
}
// Response:
{
  "success": true,
  "scopes": [
    {
      "name": "Locals",
      "variablesReference": 5,
      "expensive": false,
      "presentationHint": "locals",
      "source": {}
    },
    {
      "name": "Globals",
      "variablesReference": 6,
      "expensive": false,
      "source": {}
    }
  ]
}

Step 6: Inspect Variables Before the Bug

// Tool: get_variables
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "scope": 5
}
// Response:
{
  "success": true,
  "variables": [
    {"name": "a", "value": "10", "type": "int", "variablesReference": 0, "expandable": false},
    {"name": "b", "value": "20", "type": "int", "variablesReference": 0, "expandable": false}
  ],
  "count": 2,
  "variablesReference": 5
}

Step 7: Step Through the Bug

// Tool: step_over
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}
// Response:
{
  "success": true,
  "state": "paused",
  "message": "Stepped over"
}

Step 8: Check Variables After First Assignment

// Tool: get_variables
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "scope": 5
}
// Response:
{
  "success": true,
  "variables": [
    {"name": "a", "value": "20", "type": "int", "variablesReference": 0, "expandable": false},
    {"name": "b", "value": "20", "type": "int", "variablesReference": 0, "expandable": false}
  ],
  "count": 2,
  "variablesReference": 5
}

Now we can see the bug! After a = b, both variables have the value 20.

Step 8b: Evaluate Expressions (Optional)

You can also evaluate arbitrary expressions in the current debug context:

// Tool: evaluate_expression
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
  "expression": "a == b"
}
// Response:
{
  "success": true,
  "result": "True",
  "type": "bool",
  "variablesReference": 0
}

Step 9: Continue Execution

// Tool: continue_execution
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}
// Response:
{
  "success": true,
  "message": "Continued execution"
}

Step 10: Close the Session

// Tool: close_debug_session
// Request:
{
  "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}
// Response:
{
  "success": true,
  "message": "Closed debug session: a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}

Important Implementation Details

Session IDs

  • All session IDs are UUIDs in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • Sessions can terminate unexpectedly, always check if a session exists before operations

Variable Scope References

  • The variablesReference from get_scopes is what you pass to get_variables
  • This is NOT the same as the frame ID from get_stack_trace
  • Common mistake: Using frame ID instead of variablesReference

Variable Response Size Guards

  • Variable responses are size-guarded: oversized values are cut (the variable carries truncated: true) and very large scopes come back as a capped list plus a top-level truncation summary with an explanatory notice
  • Narrow the request to escape the cap: pass names: ["a", "b"] to get_variables or get_local_variables to fetch specific variables in full. Requested names that were not found are listed in the response's notFound

Stack Trace Filtering

  • get_stack_trace filters internal/runtime frames by default (for JavaScript: Node internals, node_modules dependencies, and async separators). When any are hidden the response carries hiddenFrames (the count) and a note saying so; pass includeInternals: true to get the full stack. A frame with unresolvedSource: true has a file that is a label, not an openable path, and the note says what it is under that adapter (a js-debug source map the package did not ship, a CodeLLDB native frame without debug info, a class the JDI bridge names by package path)

Breakpoint Behavior

  • Breakpoints initially show "verified": false because verification happens asynchronously by the debug adapter once the module is loaded (e.g., debugpy verifies after the script starts)
  • Avoid setting breakpoints on non-executable lines (comments, blank lines)
  • Best lines for breakpoints: assignments, function calls, conditionals

File Paths

  • The server uses SimpleFileChecker for both path validation and resolution. It returns a FileExistenceResult containing the effectivePath (the resolved path actually used downstream). The server passes this effectivePath to SessionManager for all subsequent operations (breakpoints, launch, source context)
  • In container mode, resolvePathForRuntime() rewrites paths to be under the workspace root (default /workspace/), then SimpleFileChecker validates existence at that resolved location
  • In host mode, SimpleFileChecker rejects non-absolute resolved paths during preflight existence checks (relative paths may still pass through other code paths)
  • Use forward slashes (/) or escaped backslashes (\\) in JSON

Common Errors and Solutions

"Managed session not found"

{
  "code": -32603,
  "message": "MCP error -32603: Failed to continue execution: Managed session not found: {sessionId}"
}

Solution: The session has terminated. Create a new session.

Invalid Scope Reference

{
  "code": -32602,
  "message": "scope (variablesReference) parameter is required and must be a number"
}

Solution: Use the variablesReference from get_scopes, not the frame ID.

Fully Implemented Features

All 29 tools are fully implemented, including:

  • restart_debugging: One call terminates the current debuggee (if any) and relaunches with the same configuration; breakpoints re-apply automatically and the output buffer starts fresh (read from since: 0). Works while running, paused, or after the program exited; attach sessions are rejected with a clear error.

  • list_breakpoints / remove_breakpoint / clear_breakpoints: Full breakpoint lifecycle management. Listing shows each breakpoint's verified state and adapter-assigned id; removal (by id, by function name, or by file + line) and clearing take effect immediately while the program is running or paused, and still work after the program exits so breakpoints can be adjusted before a relaunch.

  • wait_for_stop: Blocks until the session next pauses or ends and reports the stop (lastStop, location) or the exit code. The explicit wait after continue_execution — which returns immediately — and after any pending: true answer; timeout (ms, default 30000, max 600000) bounds the call, not the program, and a wait that runs out answers pending: true so it can simply be repeated.

  • pause_execution: Sends a DAP pause request and waits briefly (up to ~5s) for the program to stop; on a fresh stop it returns the stop reason (stopReason), the first visible frame (location, with context) and, when the answer is anchored off the thread the stop was reported on or no frame is program code, threadId/note. If the program cannot stop within the grace window (e.g. blocked in native code), it returns success with pending: true (top level and under data) and the paused state is picked up asynchronously (wait_for_stop blocks until it is). The session normally must be in the running state, but calling pause on an already paused session succeeds as a no-op.

  • get_output: Returns the debuggee's stdout/stderr/console output, buffered per launch from DAP output events. Cursor-based (since/nextSince) for incremental polling; output stays readable after the program exits until the session is closed. The same data is exposed as a subscribable MCP resource (debug://sessions/{id}/output). Once a session has launched or attached, resources/list also offers debug://sessions/{id}/proxy-log — a sanitized, bounded tail of that session's debug proxy log (at most the final 64 KiB, trimmed to 80 lines) for diagnosing a failed or misbehaving launch. It is a point-in-time snapshot and is deliberately not subscribable.

  • evaluate_expression: Evaluates arbitrary expressions in the current debug context. When frameId is omitted, the server resolves the same shared inspection anchor that get_stack_trace and get_local_variables use: the top frame of the stopped thread, or — when that thread reports no frames — a sibling thread whose frames the language policy recognizes as user code, which is then adopted and disclosed in the response's anchorNote. Pass frameId (from get_stack_trace) when you want a specific frame: an explicit id is authoritative and bypasses that selection entirely. timeout (ms, default 30000, max 600000) bounds the wait for the evaluation; on expiry the request fails but the expression may keep executing in the debuggee. Expressions with side effects are allowed (can modify program state).

  • expose_session / unexpose_session: Opens a read-only DAP mirror endpoint (loopback-only, token-gated) so an IDE such as VS Code can attach to the live session and inspect the paused state — threads, stack, scopes, variables, evaluate — while execution control stays with the MCP session. See tool-reference.md for the VS Code launch.json recipe.

Best Practices

  1. Always create a session first - No debugging operations work without an active session
  2. Check the stack trace - Understand where you are in the code before inspecting variables
  3. Get scopes before variables - You need the variablesReference to inspect variables
  4. Handle errors gracefully - Sessions can terminate, files might not exist
  5. Use meaningful session names - Helps when debugging multiple scripts