Skip to content

Latest commit

 

History

History
407 lines (342 loc) · 13 KB

File metadata and controls

407 lines (342 loc) · 13 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

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 28 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.

  • 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 (data.stopReason). If the program cannot stop within the grace window (e.g. blocked in native code), it returns success with data.pending: true and the paused state is picked up asynchronously. 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