Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C

| Tool | Description | Parameters |
|------|-------------|------------|
| **start_debugging** | Start a debug session for a source code file | `fileFullPath` (required)<br>`workingDirectory` (required)<br>`testName` (optional)<br>`configurationName` (optional) |
| **start_debugging** | Start a debug session and return after VS Code accepts it | `fileFullPath` (required)<br>`workingDirectory` (required)<br>`testName` (optional)<br>`configurationName` (optional) |
| **start_debugging_with_config** | Start from an inline launch configuration and return after VS Code accepts it | `configuration` (required)<br>`workingDirectory` (required) |
| **stop_debugging** | Stop the current debug session | None |
| **step_over** | Execute the next line (step over function calls) | None |
| **step_into** | Step into function calls | None |
| **step_out** | Step out of the current function | None |
| **continue_execution** | Continue until next breakpoint | None |
| **restart_debugging** | Restart the current debug session | None |
| **step_over** | Request one step; optionally wait for its stop | `timeoutMs` (optional, maximum 300000) |
| **step_into** | Request one step; optionally wait for its stop | `timeoutMs` (optional, maximum 300000) |
| **step_out** | Request one step; optionally wait for its stop | `timeoutMs` (optional, maximum 300000) |
| **continue_execution** | Resume execution; optionally wait for its next stop | `timeoutMs` (optional, maximum 300000) |
| **wait_for_debug_stop** | Wait explicitly for pause or termination | `timeoutMs` (optional, maximum 300000) |
| **restart_debugging** | Request a restart and return after acceptance | None |
| **add_breakpoint** | Add a breakpoint at a specific line (optionally conditional) | `fileFullPath` (required)<br>`lineContent` (required)<br>`condition` (optional) |
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required)<br>`line` (required) |
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
Expand Down Expand Up @@ -479,4 +481,4 @@ If DebugMCP has helped you debug faster, please consider giving it a star on Git

MIT License - See [LICENSE](LICENSE.txt) for details

This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**.
This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**.
9 changes: 5 additions & 4 deletions docs/architecture/debugMCPServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ Each request creates a new stateless `StreamableHTTPServerTransport` instance th

| Tool | Description |
|------|-------------|
| `start_debugging` | Start a debug session |
| `start_debugging` | Start a debug session and return after acceptance |
| `stop_debugging` | Stop current session |
| `step_over/into/out` | Stepping commands |
| `continue_execution` | Continue to next breakpoint |
| `step_over/into/out` | Dispatch stepping commands without waiting for the next stop |
| `continue_execution` | Resume execution without waiting for the next stop |
| `wait_for_debug_stop` | Explicit bounded wait for pause or termination |
| `restart_debugging` | Restart session |
| `add/remove_breakpoint` | Breakpoint management |
| `clear_all_breakpoints` | Remove all breakpoints |
Expand All @@ -78,4 +79,4 @@ Each request creates a new stateless `StreamableHTTPServerTransport` instance th
## Configuration

- `debugmcp.serverPort`: Port number (default: 3001)
- `debugmcp.timeoutInSeconds`: Operation timeout (default: 180)
- `debugmcp.timeoutInSeconds`: Operation timeout (default: 180)
8 changes: 4 additions & 4 deletions docs/architecture/debugState.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Debugging operations are asynchronous - the debugger takes time to execute and u
## Usage Pattern

```
1. Capture before state: beforeState = executor.getCurrentDebugState()
2. Execute debug command
3. Poll for changes: compare beforeState with currentState
4. State changed when: file, line, frame, or session status differs
1. Dispatch a start, continue, or step command
2. Call wait_for_debug_stop explicitly when stopped state is needed
3. Retrieve DebugState after the explicit wait reports "stopped"
4. Inspect the current file, line, frame, stack, and breakpoints
```

## Design Notes
Expand Down
8 changes: 3 additions & 5 deletions docs/architecture/debuggingExecutor.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ VS Code's debug API is powerful but requires careful handling. `DebuggingExecuto

- Execute VS Code debug commands (step, continue, restart, etc.)
- Start and stop debug sessions
- Capture accepted launch and resolved session information
- Wait explicitly for stop or termination events when requested
- Manage breakpoints (add, remove, list, clear)
- Retrieve current debug state (file, line, frame info)
- Execute DAP custom requests for variables and expression evaluation
Expand Down Expand Up @@ -60,11 +62,7 @@ For data retrieval, the executor uses DAP's custom request mechanism:

### Session Readiness

A session is considered "ready" when:
1. `vscode.debug.activeDebugSession` exists
2. Location info is available (file name and line number)

This handles cases where the debugger is still initializing (common with Python).
Control commands do not wait for session readiness or the next breakpoint. The separate `waitForDebugStop()` operation listens for a stack-frame selection or termination and always has a timeout.

### State Retrieval

Expand Down
36 changes: 10 additions & 26 deletions docs/architecture/debuggingHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

## Purpose

High-level orchestration layer that coordinates debugging operations between the MCP server and VS Code's debug API. Handles the asynchronous nature of debugging by implementing state change detection.
High-level orchestration layer that coordinates debugging operations between the MCP server and VS Code's debug API. It keeps command dispatch separate from explicit waiting.

## Motivation

Debugging is inherently asynchronous - when you step over a line, the debugger takes time to execute and update its state. AI agents need reliable feedback about when operations complete. `DebuggingHandler` bridges this gap by polling for state changes and returning meaningful responses.
Debugging is inherently asynchronous, but an MCP control request should not occupy the caller until an unrelated future stop. `DebuggingHandler` returns once VS Code accepts start, continue, step, or restart commands. Callers that need the next stopped state use `wait_for_debug_stop` with a bounded timeout.

## Responsibility

- Orchestrate debugging operations (start, stop, step, breakpoints)
- Detect when debugger state has meaningfully changed after commands
- Format prompt command-acceptance responses
- Expose an explicit bounded wait for pause or termination
- Format debug state into human/AI-readable responses
- Provide root cause analysis guidance to AI agents
- Manage operation timeouts
Expand All @@ -36,26 +37,9 @@ Debugging is inherently asynchronous - when you step over a line, the debugger t

## Key Concepts

### State Change Detection
### Dispatch and wait semantics

After executing a debug command (step over, continue, etc.), the handler:
1. Captures "before" state
2. Executes the command via executor
3. Polls for state changes using exponential backoff
4. Returns the "after" state when a meaningful change is detected

### Exponential Backoff

Polling starts at 1 second intervals and increases exponentially (capped at 10 seconds for session activation, 1 second for state changes). Jitter is added to prevent thundering herd issues.

### Meaningful State Changes

A state change is considered meaningful when any of these change:
- Session active status
- Current file path
- Current line number
- Frame name (function/method)
- Frame ID
Start, continue, step, and restart commands report acceptance immediately. Launch responses also include the requested configuration name, resolved configuration and session identity when VS Code exposes them, and the current lifecycle state. `wait_for_debug_stop` listens for VS Code stack-frame or termination events and reports the stopped state or a clear timeout.

### Root Cause Analysis

Expand All @@ -65,14 +49,14 @@ When debugging stops, the handler prompts AI agents to consider whether they fou

- Class definition: `src/debuggingHandler.ts`
- Interface: `IDebuggingHandler`
- State change detection: `waitForStateChange()`, `hasStateChanged()`
- Session waiting: `waitForActiveDebugSession()`
- Explicit stop wait: `handleWaitForDebugStop()`
- Launch response formatting: `formatLaunchResult()`
- State formatting: `formatDebugState()`

## Design Patterns

- **Before/After Comparison**: All step operations capture state before and after
- **Timeout Configuration**: Controlled by `timeoutInSeconds` parameter
- **Command/Observation Separation**: Control operations dispatch; the explicit wait observes
- **Bounded Waiting**: `wait_for_debug_stop` uses `timeoutInSeconds` or a per-call timeout, capped at 300 seconds
- **Dependency Injection**: Executor and config manager are injected via constructor

## Error Handling
Expand Down
18 changes: 11 additions & 7 deletions skills/debug-live/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ allowed-tools:
- step_into
- step_out
- continue_execution
- wait_for_debug_stop
- get_variables_values
- evaluate_expression
---
Expand Down Expand Up @@ -53,13 +54,13 @@ If you can step through the code in a few tool calls, do that instead of specula
data boundaries (where input enters, where output is produced).
3. **Start the session.** Call `start_debugging` with the source file path. For a single
test, pass `testName`; the server routes through VS Code's Testing API so test runners
like `dotnet test` / `pytest` / `jest` work correctly. The call returns when the
program either hits a breakpoint (`stopped`) or runs to completion without pausing
(`terminated`).
like `dotnet test` / `pytest` / `jest` work correctly. The call returns as soon as
VS Code accepts the request.
4. **Navigate and inspect.** Use `step_over`, `step_into`, `step_out`, `continue_execution`
to move through code. Use `get_variables_values` to see local/global state and
`evaluate_expression` to test hypotheses live (call methods, read properties, run
list comprehensions, etc.).
to move through code. Omit `timeoutMs` to return after command acceptance, or
provide a bounded `timeoutMs` to wait for the resulting pause or termination in
the same call. Use `wait_for_debug_stop` when waiting separately, then inspect
with `get_variables_values` or `evaluate_expression`.
5. **Find the root cause** (see framework below). Don't stop at the first wrong thing
you see — trace it back to *why*.
6. **Clean up.** Call `clear_all_breakpoints` when you're done so you don't pollute the
Expand Down Expand Up @@ -181,7 +182,8 @@ Before ending the debug session, confirm you can answer:
```text
add_breakpoint fileFullPath=/repo/src/calculate.py lineContent="result = parse(raw)"
start_debugging fileFullPath=/repo/src/calculate.py workingDirectory=/repo
# session pauses on the breakpoint
wait_for_debug_stop timeoutMs=30000
# session is now paused on the breakpoint
get_variables_values scope=local
evaluate_expression expression="type(raw).__name__"
step_into
Expand All @@ -193,6 +195,7 @@ clear_all_breakpoints
```text
add_breakpoint fileFullPath=C:\Repo\Calculator.Tests\CalculatorTests.cs lineContent="Assert.Equal(5, _calc.Add(2, 3));"
start_debugging fileFullPath=C:\Repo\Calculator.Tests\CalculatorTests.cs workingDirectory=C:\Repo testName=Add_ReturnsSum
wait_for_debug_stop timeoutMs=30000
# pauses inside the test
step_into
get_variables_values
Expand All @@ -203,6 +206,7 @@ get_variables_values
restart_debugging
# session restarts with the same configuration; breakpoints persist
continue_execution
wait_for_debug_stop timeoutMs=30000
```

---
Expand Down
70 changes: 56 additions & 14 deletions src/debugMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,21 @@ export class DebugMCPServer {
* `skills/debug-live/SKILL.md`.
*/
private setupTools(server: McpServer) {
const controlWaitTimeout = z.number().int().positive().max(300_000).optional().describe(
'Optional bounded wait in milliseconds. Omit to return immediately after VS Code accepts the command; ' +
'provide a positive integer to wait at most that long for the next pause or termination. Prefer a short, ' +
'explicit timeout appropriate to the expected operation (for example, 30000). The MCP call remains open ' +
'while waiting, and client cancellation may not reach the server. If the timeout expires, use ' +
'wait_for_debug_stop for any additional bounded wait instead of repeating the control command.'
);

// Start debugging tool
server.registerTool('start_debugging', {
description: 'Start a VS Code debug session for a source file, optionally for a single test method. ' +
'Returns promptly after VS Code accepts or rejects the launch request; it never waits for a breakpoint, pause, or termination. ' +
'The JSON response reports the requested configuration name, acceptance, resolved configuration and session ID/name/type when available, ' +
'and the immediate state (starting, running, paused, or terminated). If the session is already paused, it also includes stopped-state details. ' +
'Call wait_for_debug_stop explicitly when you need to wait for the next pause or termination. ' +
'Use when investigating bugs, failing tests, wrong/null variable values, unexpected runtime behavior, ' +
'or any "it doesn\'t work" report where stepping through the code is cheaper than speculation.',
inputSchema: {
Expand All @@ -236,6 +248,10 @@ export class DebugMCPServer {
// Start debugging from a raw inline configuration (language-agnostic).
server.registerTool('start_debugging_with_config', {
description: 'Start a VS Code debug session from a raw, caller-supplied launch.json-style configuration object — ' +
'returns promptly after VS Code accepts or rejects the launch request and never waits for a breakpoint, pause, or termination. ' +
'The JSON response reports the requested configuration, acceptance, resolved configuration and session ID/name/type when available, ' +
'and the immediate state (starting, running, paused, or terminated). If already paused, it also includes stopped-state details. ' +
'Call wait_for_debug_stop explicitly when you need to wait for the next pause or termination. ' +
'no launch.json entry required. Use this to debug an ARBITRARY program/command (any language with an ' +
'installed debug extension) when there is no suitable existing configuration. The caller owns all ' +
'toolchain specifics: e.g. for a TypeScript file pass {"type":"node","request":"launch","program":"...",' +
Expand Down Expand Up @@ -273,39 +289,65 @@ export class DebugMCPServer {

// Step over tool
server.registerTool('step_over', {
description: 'Execute the current line of code without diving into it.',
}, async () => {
const result = await this.debuggingHandler.handleStepOver();
description: 'Request one step over. Omit timeoutMs to return as soon as VS Code accepts the command; provide it to wait up to that many milliseconds for the next pause or termination and return stopped-state details.',
inputSchema: {
timeoutMs: controlWaitTimeout
},
}, async (args: { timeoutMs?: number }, extra) => {
const result = await this.debuggingHandler.handleStepOver(args, extra.signal);
return { content: [{ type: 'text' as const, text: result }] };
});

// Step into tool
server.registerTool('step_into', {
description: 'Dive into the current line of code.',
}, async () => {
const result = await this.debuggingHandler.handleStepInto();
description: 'Request one step into. Omit timeoutMs to return as soon as VS Code accepts the command; provide it to wait up to that many milliseconds for the next pause or termination and return stopped-state details.',
inputSchema: {
timeoutMs: controlWaitTimeout
},
}, async (args: { timeoutMs?: number }, extra) => {
const result = await this.debuggingHandler.handleStepInto(args, extra.signal);
return { content: [{ type: 'text' as const, text: result }] };
});

// Step out tool
server.registerTool('step_out', {
description: 'Step out of the current function',
}, async () => {
const result = await this.debuggingHandler.handleStepOut();
description: 'Request one step out. Omit timeoutMs to return as soon as VS Code accepts the command; provide it to wait up to that many milliseconds for the next pause or termination and return stopped-state details.',
inputSchema: {
timeoutMs: controlWaitTimeout
},
}, async (args: { timeoutMs?: number }, extra) => {
const result = await this.debuggingHandler.handleStepOut(args, extra.signal);
return { content: [{ type: 'text' as const, text: result }] };
});

// Continue execution tool
server.registerTool('continue_execution', {
description: 'Resume program execution until the next breakpoint is hit or the program completes.',
}, async () => {
const result = await this.debuggingHandler.handleContinue();
description: 'Request that execution resume. Omit timeoutMs to return as soon as VS Code accepts the command; provide it to wait up to that many milliseconds for the next pause or termination and return stopped-state details.',
inputSchema: {
timeoutMs: controlWaitTimeout
},
}, async (args: { timeoutMs?: number }, extra) => {
const result = await this.debuggingHandler.handleContinue(args, extra.signal);
return { content: [{ type: 'text' as const, text: result }] };
});

server.registerTool('wait_for_debug_stop', {
description: 'Explicitly wait for the active debug session to pause or terminate, with a bounded timeout. Prefer an explicit short timeout (for example, 30000) and call this tool again if more waiting is needed; the MCP call remains open while waiting, and client cancellation may not reach the server. The JSON response outcome is stopped, terminated, or timeout. A stopped result includes the current source location, stack and breakpoint details, plus a stop reason when inferable; timeout returns a clear message.',
inputSchema: {
timeoutMs: z.number().int().positive().max(300_000).optional().describe(
'Maximum milliseconds to wait. Prefer an explicit short value appropriate to the expected operation ' +
'(for example, 30000); call wait_for_debug_stop again if more time is needed. If omitted, this defaults ' +
'to debugmcp.timeoutInSeconds converted to milliseconds, capped at 300000.'
)
},
}, async (args: { timeoutMs?: number }, extra) => {
const result = await this.debuggingHandler.handleWaitForDebugStop(args, extra.signal);
return { content: [{ type: 'text' as const, text: result }] };
});

// Restart debugging tool
server.registerTool('restart_debugging', {
description: 'Restart the debug session from the beginning with the same configuration.',
description: 'Request a debug-session restart and return as soon as VS Code accepts the command. This never waits for the restarted session to pause or terminate; call wait_for_debug_stop explicitly when stopped-state details are needed.',
}, async () => {
const result = await this.debuggingHandler.handleRestart();
return { content: [{ type: 'text' as const, text: result }] };
Expand Down Expand Up @@ -674,4 +716,4 @@ export class DebugMCPServer {
isInitialized(): boolean {
return this.initialized;
}
}
}
Loading