Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to DebugMCP will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [2.3.0] - 2026-07-28

### Changed
- **`get_variables_values` now retrieves variables by name.** Pass an explicit `variableNames` array; only the variables you name are returned (max 50, no wildcards). Unknown names are reported back so the caller gets feedback instead of silence.

### Added
- **`list_variable_names` tool** - lists the names and types of variables at the current execution point without reading any values, so an agent can discover what is in scope and then request only what it needs.

### Breaking
- `get_variables_values` calls without a `variableNames` array now fail with an actionable error pointing at `list_variable_names`.

## [2.1.0] - 2026-06-23

### Added
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![VS Code](https://img.shields.io/badge/VS%20Code-1.104.0+-blue.svg)](https://code.visualstudio.com/)
[![Version](https://img.shields.io/badge/version-2.2.4-green.svg)](https://github.com/microsoft/DebugMCP)
[![Version](https://img.shields.io/badge/version-2.3.0-green.svg)](https://github.com/microsoft/DebugMCP)
[![VS Marketplace](https://img.shields.io/badge/VS%20Marketplace-Install-blue.svg)](https://marketplace.visualstudio.com/items?itemName=ozzafar.debugmcpextension)

> ⭐ **If you find DebugMCP useful, please [star the repo on GitHub](https://github.com/microsoft/DebugMCP)!** It helps others discover the project and motivates continued development.
Expand Down Expand Up @@ -65,7 +65,8 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required)<br>`line` (required) |
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
| **list_breakpoints** | List all active breakpoints | None |
| **get_variables_values** | Get variables and their values at current execution point | `scope` (optional: 'local', 'global', 'all') |
| **list_variable_names** | List names and types of variables in scope, without reading any values | `scope` (optional: 'local', 'global', 'all') |
| **get_variables_values** | Get the values of specifically named variables at the current execution point | `variableNames` (required, e.g. `["user","response"]`)<br>`scope` (optional: 'local', 'global', 'all') |
| **evaluate_expression** | Evaluate an expression in debug context | `expression` (required) |

> **Note:** The MCP server exposes **tools** for debugger actions, while the procedural
Expand Down Expand Up @@ -301,10 +302,12 @@ Configure DebugMCP behavior in VSCode settings:

### Security model

DebugMCP exposes powerful debugger primitives (`evaluate_expression`, `start_debugging`, …) over an unauthenticated local HTTP endpoint. To keep that surface safe, the server enforces two controls:
DebugMCP exposes powerful debugger primitives (`evaluate_expression`, `start_debugging`, …) over an unauthenticated local HTTP endpoint. To keep that surface safe, the server enforces four controls:

1. **Loopback-only bind.** The HTTP server binds to the IPv4 and IPv6 loopback addresses (`127.0.0.1` and `::1`) by default, so other hosts on your network cannot reach `http://<your-ip>:3001/mcp`. Binding both families ensures clients that resolve `localhost` to either family connect successfully. The `debugmcp.bindHost` setting (string or array of strings) lets you opt into a different interface (for example, when forwarding the port into a remote container), but doing so exposes the unauthenticated debugger to anything that can route to that address — do not point it at `0.0.0.0` or a LAN address on an untrusted network.
2. **Host / Origin header validation.** Every request must carry a `Host` header naming a loopback address (`localhost`, `127.0.0.1`, or `[::1]`); any port suffix in the `Host` must also match the server's listening port. Requests with any other `Host` — including those that arrive via DNS rebinding from a malicious webpage — are rejected with HTTP 403. The same loopback check is applied to the `Origin` header when present.
3. **Least-privilege variable inspection.** `get_variables_values` requires an explicit `variableNames` list (max 50, no wildcards) and returns only those variables. It no longer dumps every variable in scope, which previously handed the agent unrelated process state that it never asked for. Use `list_variable_names` to discover what exists; that tool returns names and types only and never reads a value.
4. **Secret redaction on variable inspection.** Values with credential-bearing or whose values matches a known credential shape are replaced with `<redacted: possible secret>` before the response leaves the extension. The same applies to `evaluate_expression`, which is otherwise the trivial bypass. Secrets nested inside a rendered container (an environment mapping, a config object) are not scrubbed entry-by-entry — the decision is made from the variable's own name and value, so a struct is returned intact unless it is itself credential-named or its rendering carries a recognizable credential. Null-ish values (`None`, `undefined`, `''`) are never redacted so "my token is empty" bugs remain debuggable. Redaction is always on and cannot be turned off.


## FAQ
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/debugMCPServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ error wins:
| `add/remove_breakpoint` | Breakpoint management |
| `clear_all_breakpoints` | Remove all breakpoints |
| `list_breakpoints` | List active breakpoints |
| `get_variables_values` | Inspect variable values |
| `get_variables_values` | Read the values of specifically named variables |
| `list_variable_names` | List variable names/types in scope, without values |
| `evaluate_expression` | Evaluate expressions |

## Configuration
Expand Down
24 changes: 24 additions & 0 deletions docs/architecture/debuggingHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,37 @@ A state change is considered meaningful when any of these change:

When debugging stops, the handler prompts AI agents to consider whether they found the root cause or just a symptom, encouraging deeper investigation.

### Secret Redaction

Variable inspection is the point where live process memory crosses the trust boundary into
an AI agent (and usually a remote model provider). Two controls apply there:

**Data minimization.** `handleGetVariables` requires an explicit `variableNames` list
(validated by `normalizeRequestedNames()`: non-empty, no wildcards, capped at
`maxRequestedVariables`) and returns only those variables. Unknown names are reported back
so the caller gets feedback instead of silence. `handleListVariableNames` exists for
discovery and returns names and types **only** — it never emits a value, so it needs no
redaction.

**Redaction.** Values that are returned still pass through `src/utils/secretRedaction.ts`,
which withholds credential-looking values by name (`api_key`, `password`, `token`, …) and by
content (JWT, PEM private key, `AKIA…`, `ghp_…`, `Bearer …`, `Password=…`, …).
`handleEvaluateExpression` is covered too, since evaluating `os.environ` is the trivial
bypass for per-variable controls. Null-ish values are deliberately left intact so
missing-credential bugs stay debuggable. The decision is made from the variable's own name
and its own value only — a struct is never descended into, so a `config` object that happens
to contain a `password` field is returned intact. Redaction is unconditional — there is no
setting to disable it.

## Key Code Locations

- Class definition: `src/debuggingHandler.ts`
- Interface: `IDebuggingHandler`
- State change detection: `waitForStateChange()`, `hasStateChanged()`
- Session waiting: `waitForActiveDebugSession()`
- State formatting: `formatDebugState()`
- Variable selection: `handleGetVariables()`, `handleListVariableNames()`, `normalizeRequestedNames()`
- Secret redaction: `src/utils/secretRedaction.ts`

## Design Patterns

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "debugmcpextension",
"displayName": "DebugMCP — Agentic Debugging for VS Code, Cursor & More",
"description": "Your AI agent debugs for you — right inside VS Code, Cursor & other VS Code-based editors. Let Copilot, Cline, Cursor, Codex & any MCP agent set breakpoints, step through code, and inspect variables live instead of guessing from logs.",
"version": "2.2.4",
"version": "2.3.0",
"publisher": "ozzafar",
"author": {
"name": "Oz Zafar",
Expand Down
15 changes: 12 additions & 3 deletions skills/debug-live/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ allowed-tools:
- step_out
- continue_execution
- pause_execution
- list_variable_names
- get_variables_values
- evaluate_expression
---
Expand Down Expand Up @@ -60,9 +61,15 @@ If you can step through the code in a few tool calls, do that instead of specula
4. **Navigate and inspect.** Use `step_over`, `step_into`, `step_out`, `continue_execution`
to move through code. Use `pause_execution` to interrupt a freely-running program
(e.g. a busy loop or embedded target) when there is no breakpoint to stop at.
Use `get_variables_values` to see local/global state and
Use `list_variable_names` to see what is in scope (names and types only, no values),
then `get_variables_values` with the specific `variableNames` you care about, and
`evaluate_expression` to test hypotheses live (call methods, read properties, run
list comprehensions, etc.).

> `get_variables_values` **requires** explicit `variableNames` — it will not dump the
> whole scope. This is deliberate: a scope dump hands you unrelated process state such
> as API keys, tokens and environment variables. Values that still look like credentials
> are replaced with `<redacted: possible secret>`; don't try to work around it.
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 @@ -189,7 +196,8 @@ Before ending the debug session, confirm you can answer:
add_breakpoint fileFullPath=/repo/src/calculate.py line=42
start_debugging fileFullPath=/repo/src/calculate.py workingDirectory=/repo
# session pauses on the breakpoint
get_variables_values scope=local
list_variable_names scope=local
get_variables_values variableNames=["raw","total"] scope=local
evaluate_expression expression="type(raw).__name__"
step_into
# … iterate until root cause found …
Expand All @@ -202,7 +210,8 @@ add_breakpoint fileFullPath=C:\Repo\Calculator.Tests\CalculatorTests.cs line=1
start_debugging fileFullPath=C:\Repo\Calculator.Tests\CalculatorTests.cs workingDirectory=C:\Repo testName=Add_ReturnsSum
# pauses inside the test
step_into
get_variables_values
list_variable_names
get_variables_values variableNames=["result","expected"]
```

### Verifying a fix without re-launching VS Code
Expand Down
2 changes: 2 additions & 0 deletions src/controlServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export class ControlServer {
return this.handler.handleListBreakpoints();
case 'handleGetVariables':
return this.handler.handleGetVariables(args);
case 'handleListVariableNames':
return this.handler.handleListVariableNames(args);
case 'handleEvaluateExpression':
return this.handler.handleEvaluateExpression(args);
default:
Expand Down
21 changes: 16 additions & 5 deletions src/debugMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export class DebugMCPServer {

// Add breakpoint tool
server.registerTool('add_breakpoint', {
description: 'Set a breakpoint to pause execution at a critical line of code. Essential for debugging: pause before potential errors, examine state at decision points, or verify code paths. Breakpoints let you inspect variables and control flow at exact moments. Provide an optional condition to create a conditional breakpoint that only pauses when the expression evaluates to true (e.g. "i == 5" or "user.id === null").',
description: 'Set a breakpoint to pause execution at a critical line of code. Breakpoints let you inspect variables and control flow at exact moments.',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
line: z.number().int().describe('Line number (1-based) where the breakpoint should be set'),
Expand All @@ -272,7 +272,7 @@ export class DebugMCPServer {

// Add logpoint tool
server.registerTool('add_logpoint', {
description: 'Add a logpoint: a breakpoint that logs a message instead of pausing execution. Ideal for tracing values across many iterations or hot paths without stopping, or where a hard pause would distort timing. Embed expressions in curly braces to interpolate runtime values, e.g. "user id={user.id}, count={items.length}". Provide an optional condition to only log when it evaluates to true.',
description: 'Add a logpoint: a breakpoint that logs a message instead of pausing execution. Ideal for tracing values across many iterations or hot paths without stopping, or where a hard pause would distort timing. Embed expressions in curly braces to interpolate runtime values, e.g. "user id={user.id}".',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
line: z.number().int().describe('Line number (1-based) where the logpoint should be set'),
Expand Down Expand Up @@ -302,18 +302,29 @@ export class DebugMCPServer {
description: 'View all currently set breakpoints across all files.',
}, async () => this.runTool('list_breakpoints', () => debuggingHandler.handleListBreakpoints()));

// List variable names tool (discovery without reading any values)
server.registerTool('list_variable_names', {
description: 'List the names and types of variables visible at the current execution point, without reading their values. Use this to discover what exists, then request the variables you actually need with get_variables_values.',
inputSchema: {
scope: z.enum(['local', 'global', 'all']).optional().describe("Variable scope: 'local', 'global', or 'all'"),
},
}, async (args: { scope?: 'local' | 'global' | 'all' }) =>
this.runTool('list_variable_names', () => debuggingHandler.handleListVariableNames(args)));

// Get variables tool
server.registerTool('get_variables_values', {
description: 'Inspect all variable values at the current execution point. This is your window into program state - see what data looks like at runtime, verify assumptions, identify unexpected values, and understand why code behaves as it does.',
description: 'Read the values of specific named variables at the current execution point.',
inputSchema: {
variableNames: z.array(z.string()).min(1).max(50)
.describe('Names of the variables to read, e.g. ["user", "response"]. Required - wildcards are not supported.'),
scope: z.enum(['local', 'global', 'all']).optional().describe("Variable scope: 'local', 'global', or 'all'"),
},
}, async (args: { scope?: 'local' | 'global' | 'all' }) =>
}, async (args: { variableNames: string[]; scope?: 'local' | 'global' | 'all' }) =>
this.runTool('get_variables_values', () => debuggingHandler.handleGetVariables(args)));

// Evaluate expression tool
server.registerTool('evaluate_expression', {
description: 'Powerful runtime expression evaluator: Test hypotheses, check computed values, call methods, or inspect object properties in the live debug context. Goes beyond simple variable inspection - evaluate any valid expression in the target language.',
description: 'Powerful runtime expression evaluator: Test hypotheses, check computed values, call methods, or inspect object properties in the live debug context. Evaluate any valid expression in the target language.',
inputSchema: {
expression: z.string().describe('Expression to evaluate in the current programming language context'),
},
Expand Down
Loading
Loading