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
5 changes: 3 additions & 2 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.0.1-green.svg)](https://github.com/microsoft/DebugMCP)
[![Version](https://img.shields.io/badge/version-2.2.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 All @@ -17,10 +17,11 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe
<img src="assets/DebugMCP.gif" width="800">
</p>

## ✨ What's New in 2.0.0
## ✨ What's New in 2.2.0

- **`/debug-live` Agent Skill** — DebugMCP now ships a companion [Agent Skill](./skills/debug-live/SKILL.md) that is auto-installed into each configured harness's personal skills directory (e.g. `~/.copilot/skills/debug-live/`). Invoke it with `/debug-live` in supporting agents to load the systematic debugging workflow and trigger DebugMCP tools with the right context.
- **Robust debugging via the VS Code Testing API** — `start_debugging` with a `testName` now uses the VS Code Testing API to discover and launch the test, replacing the previous best-effort path. This works reliably across language test runners that integrate with the Testing API (pytest, Jest/Vitest, Java, .NET, Go, etc.) and produces consistent breakpoint hits inside individual test cases.
- **Concurrent debugging** - supports multiple concurrent debug sessions, allowing effective parallel agentic debugging.

## 🚀 Quick Install

Expand Down
40 changes: 39 additions & 1 deletion docs/architecture/debugMCPServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ AI Agent (MCP Client)

## Key Concepts

### Multi-window routing (multiple VS Code windows / repos)

The MCP endpoint uses a fixed port, but every open VS Code window activates the
extension. To avoid debugging the wrong workspace when several windows are open:

- **Every window** starts a loopback `ControlServer` (`src/controlServer.ts`) that runs
debug operations against *its own* `DebuggingHandler`, and advertises its workspace
folders (plus control port + token) in a shared file registry
(`src/utils/workspaceRegistry.ts`).
- **One window** wins the public MCP port and becomes the **router**. Its per-MCP-session
handler is a `RoutingDebuggingHandler` (`src/routingDebuggingHandler.ts`) that resolves
the target window from the request's `workingDirectory`/`fileFullPath` and forwards the
operation to that window's `ControlServer`. The target is cached per session so hint-less
follow-ups (step/continue/inspect) reach the same window. If the router window closes,
a worker window takes over the port on retry.

`DebugMCPServer` builds one handler **per MCP session** via a handler factory, which is
what lets concurrent agent sessions drive debuggers in different repos simultaneously.

### Tools only — no resources, no instructions

`DebugMCPServer` exposes **tools only**. Procedural workflow guidance (when to debug,
Expand All @@ -53,11 +72,30 @@ Uses stateless HTTP POST requests for MCP communication. The express server expo

Each request creates a new stateless `StreamableHTTPServerTransport` instance that is closed when the HTTP response closes. The server returns JSON-RPC error responses (not HTML pages) for malformed payloads or unsupported methods to keep client behavior predictable.

### Bounded operations (no hung tool calls)

Every layer that a tool call passes through is time-bounded so a wedged debug
adapter or an unresponsive worker window can never leave an MCP request pending
forever (which surfaces to clients as "Request timed out" and makes the whole
server look stuck). The bounds are nested innermost-first so the most specific
error wins:

- **DAP request** (`DebuggingExecutor.dapRequest`) caps each `customRequest`
(stackTrace/scopes/variables/evaluate).
- **Router → worker forward** (`RoutingDebuggingHandler`) caps the loopback
round-trip to a worker window's `ControlServer` (`timeoutInSeconds + 15s`).
- **Tool boundary** (`DebugMCPServer.runTool`) is the final backstop around every
tool invocation (`timeoutInSeconds + 30s`), guaranteeing the client always
gets a prompt response.

## Key Code Locations

- Class definition: `src/debugMCPServer.ts`
- Tool registration: `setupTools()` method (uses `McpServer.registerTool()`)
- Server startup: `start()` method (creates express app with `/mcp` route)
- Server startup / router election: `start()` method (returns whether this window owns the port)
- Per-window control server: `src/controlServer.ts`
- Cross-window routing handler: `src/routingDebuggingHandler.ts`
- Shared window registry: `src/utils/workspaceRegistry.ts`
- Agent Skill (companion, not part of the MCP surface): `skills/debug-live/SKILL.md`

## Exposed Tools
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture/debuggingExecutor.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ For data retrieval, the executor uses DAP's custom request mechanism:
| `variables` | Get variables within a scope |
| `evaluate` | Evaluate expressions in REPL context |

All custom requests go through `dapRequest()`, which caps each call so an
unresponsive adapter rejects with an error instead of hanging the caller.

### Session Readiness

A session is considered "ready" when:
Expand All @@ -80,6 +83,7 @@ This handles cases where the debugger is still initializing (common with Python)
- Interface: `IDebuggingExecutor`
- State retrieval: `getCurrentDebugState()`
- DAP requests: `getVariables()`, `evaluateExpression()`
- Bounded DAP calls: `dapRequest()`
- Session readiness: `hasActiveSession()`

## Breakpoint Management
Expand Down
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",
"description": "Let AI agents debug your code inside VS Code — breakpoints, step-through execution, variable inspection, and expression evaluation. Automatically exposes itself as an MCP (Model Context Protocol) server for seamless integration with AI assistants.",
"version": "2.1.0",
"version": "2.2.0",
"publisher": "ozzafar",
"author": {
"name": "Oz Zafar",
Expand Down
112 changes: 112 additions & 0 deletions src/controlServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation.

import * as http from 'http';
import { IDebuggingHandler } from './debuggingHandler';
import { logger } from './utils/logger';

/**
* Per-window loopback HTTP server that runs debug operations against this
* window's local DebuggingHandler. The router window forwards each MCP tool
* call here so debugging happens in the window that owns the workspace.
*
* Bound to 127.0.0.1 and gated by a per-window token read from the registry.
*/
export class ControlServer {
private server: http.Server | undefined;
private boundPort = 0;

constructor(
private readonly handler: IDebuggingHandler,
private readonly token: string
) {}

/** The ephemeral loopback port chosen by the OS, or 0 before start(). */
public getPort(): number {
return this.boundPort;
}

/** Start listening on an ephemeral loopback port. Resolves with the port. */
public async start(): Promise<number> {
return new Promise<number>((resolve, reject) => {
const server = http.createServer((req, res) => this.onRequest(req, res));
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
this.boundPort = typeof address === 'object' && address ? address.port : 0;
this.server = server;
logger.info(`DebugMCP control server listening on 127.0.0.1:${this.boundPort}`);
resolve(this.boundPort);
});
});
}

/** Stop listening. */
public async stop(): Promise<void> {
if (this.server) {
await new Promise<void>((resolve) => this.server!.close(() => resolve()));
this.server = undefined;
}
}

private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
if (req.method !== 'POST' || req.url !== '/op') {
res.writeHead(404).end();
return;
}
if (req.headers['x-debugmcp-token'] !== this.token) {
res.writeHead(403).end();
return;
}

let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', async () => {
try {
const { op, args } = JSON.parse(body || '{}') as { op: string; args?: unknown };
const result = await this.dispatch(op, args ?? {});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ result }));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: message }));
}
});
}

/** Map a control op name onto the local debugging handler. */
private dispatch(op: string, args: any): Promise<string> {
switch (op) {
case 'handleStartDebugging':
return this.handler.handleStartDebugging(args);
case 'handleStopDebugging':
return this.handler.handleStopDebugging();
case 'handleStepOver':
return this.handler.handleStepOver();
case 'handleStepInto':
return this.handler.handleStepInto();
case 'handleStepOut':
return this.handler.handleStepOut();
case 'handleContinue':
return this.handler.handleContinue();
case 'handleRestart':
return this.handler.handleRestart();
case 'handleAddBreakpoint':
return this.handler.handleAddBreakpoint(args);
case 'handleRemoveBreakpoint':
return this.handler.handleRemoveBreakpoint(args);
case 'handleClearAllBreakpoints':
return this.handler.handleClearAllBreakpoints();
case 'handleListBreakpoints':
return this.handler.handleListBreakpoints();
case 'handleGetVariables':
return this.handler.handleGetVariables(args);
case 'handleEvaluateExpression':
return this.handler.handleEvaluateExpression(args);
default:
return Promise.reject(new Error(`Unknown control op: ${op}`));
}
}
}
Loading
Loading