Skip to content

Commit 2f3d0f7

Browse files
authored
Merge pull request #104 from microsoft/ozzafar/fix_major_issues
Fix major issues (detailed in description)
2 parents c7cc190 + 5e0d4c9 commit 2f3d0f7

12 files changed

Lines changed: 1045 additions & 97 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe
44

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

1010
> **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.
@@ -17,10 +17,11 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe
1717
<img src="assets/DebugMCP.gif" width="800">
1818
</p>
1919

20-
## ✨ What's New in 2.0.0
20+
## ✨ What's New in 2.2.0
2121

2222
- **`/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.
2323
- **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.
24+
- **Concurrent debugging** - supports multiple concurrent debug sessions, allowing effective parallel agentic debugging.
2425

2526
## 🚀 Quick Install
2627

docs/architecture/debugMCPServer.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ AI Agent (MCP Client)
3535

3636
## Key Concepts
3737

38+
### Multi-window routing (multiple VS Code windows / repos)
39+
40+
The MCP endpoint uses a fixed port, but every open VS Code window activates the
41+
extension. To avoid debugging the wrong workspace when several windows are open:
42+
43+
- **Every window** starts a loopback `ControlServer` (`src/controlServer.ts`) that runs
44+
debug operations against *its own* `DebuggingHandler`, and advertises its workspace
45+
folders (plus control port + token) in a shared file registry
46+
(`src/utils/workspaceRegistry.ts`).
47+
- **One window** wins the public MCP port and becomes the **router**. Its per-MCP-session
48+
handler is a `RoutingDebuggingHandler` (`src/routingDebuggingHandler.ts`) that resolves
49+
the target window from the request's `workingDirectory`/`fileFullPath` and forwards the
50+
operation to that window's `ControlServer`. The target is cached per session so hint-less
51+
follow-ups (step/continue/inspect) reach the same window. If the router window closes,
52+
a worker window takes over the port on retry.
53+
54+
`DebugMCPServer` builds one handler **per MCP session** via a handler factory, which is
55+
what lets concurrent agent sessions drive debuggers in different repos simultaneously.
56+
3857
### Tools only — no resources, no instructions
3958

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

5473
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.
5574

75+
### Bounded operations (no hung tool calls)
76+
77+
Every layer that a tool call passes through is time-bounded so a wedged debug
78+
adapter or an unresponsive worker window can never leave an MCP request pending
79+
forever (which surfaces to clients as "Request timed out" and makes the whole
80+
server look stuck). The bounds are nested innermost-first so the most specific
81+
error wins:
82+
83+
- **DAP request** (`DebuggingExecutor.dapRequest`) caps each `customRequest`
84+
(stackTrace/scopes/variables/evaluate).
85+
- **Router → worker forward** (`RoutingDebuggingHandler`) caps the loopback
86+
round-trip to a worker window's `ControlServer` (`timeoutInSeconds + 15s`).
87+
- **Tool boundary** (`DebugMCPServer.runTool`) is the final backstop around every
88+
tool invocation (`timeoutInSeconds + 30s`), guaranteeing the client always
89+
gets a prompt response.
90+
5691
## Key Code Locations
5792

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

63101
## Exposed Tools

docs/architecture/debuggingExecutor.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ For data retrieval, the executor uses DAP's custom request mechanism:
5858
| `variables` | Get variables within a scope |
5959
| `evaluate` | Evaluate expressions in REPL context |
6060

61+
All custom requests go through `dapRequest()`, which caps each call so an
62+
unresponsive adapter rejects with an error instead of hanging the caller.
63+
6164
### Session Readiness
6265

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

8589
## Breakpoint Management

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "debugmcpextension",
33
"displayName": "DebugMCP",
44
"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.",
5-
"version": "2.1.0",
5+
"version": "2.2.0",
66
"publisher": "ozzafar",
77
"author": {
88
"name": "Oz Zafar",

src/controlServer.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) Microsoft Corporation.
2+
3+
import * as http from 'http';
4+
import { IDebuggingHandler } from './debuggingHandler';
5+
import { logger } from './utils/logger';
6+
7+
/**
8+
* Per-window loopback HTTP server that runs debug operations against this
9+
* window's local DebuggingHandler. The router window forwards each MCP tool
10+
* call here so debugging happens in the window that owns the workspace.
11+
*
12+
* Bound to 127.0.0.1 and gated by a per-window token read from the registry.
13+
*/
14+
export class ControlServer {
15+
private server: http.Server | undefined;
16+
private boundPort = 0;
17+
18+
constructor(
19+
private readonly handler: IDebuggingHandler,
20+
private readonly token: string
21+
) {}
22+
23+
/** The ephemeral loopback port chosen by the OS, or 0 before start(). */
24+
public getPort(): number {
25+
return this.boundPort;
26+
}
27+
28+
/** Start listening on an ephemeral loopback port. Resolves with the port. */
29+
public async start(): Promise<number> {
30+
return new Promise<number>((resolve, reject) => {
31+
const server = http.createServer((req, res) => this.onRequest(req, res));
32+
server.on('error', reject);
33+
server.listen(0, '127.0.0.1', () => {
34+
const address = server.address();
35+
this.boundPort = typeof address === 'object' && address ? address.port : 0;
36+
this.server = server;
37+
logger.info(`DebugMCP control server listening on 127.0.0.1:${this.boundPort}`);
38+
resolve(this.boundPort);
39+
});
40+
});
41+
}
42+
43+
/** Stop listening. */
44+
public async stop(): Promise<void> {
45+
if (this.server) {
46+
await new Promise<void>((resolve) => this.server!.close(() => resolve()));
47+
this.server = undefined;
48+
}
49+
}
50+
51+
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
52+
if (req.method !== 'POST' || req.url !== '/op') {
53+
res.writeHead(404).end();
54+
return;
55+
}
56+
if (req.headers['x-debugmcp-token'] !== this.token) {
57+
res.writeHead(403).end();
58+
return;
59+
}
60+
61+
let body = '';
62+
req.on('data', (chunk) => {
63+
body += chunk;
64+
});
65+
req.on('end', async () => {
66+
try {
67+
const { op, args } = JSON.parse(body || '{}') as { op: string; args?: unknown };
68+
const result = await this.dispatch(op, args ?? {});
69+
res.writeHead(200, { 'Content-Type': 'application/json' });
70+
res.end(JSON.stringify({ result }));
71+
} catch (error) {
72+
const message = error instanceof Error ? error.message : String(error);
73+
res.writeHead(500, { 'Content-Type': 'application/json' });
74+
res.end(JSON.stringify({ error: message }));
75+
}
76+
});
77+
}
78+
79+
/** Map a control op name onto the local debugging handler. */
80+
private dispatch(op: string, args: any): Promise<string> {
81+
switch (op) {
82+
case 'handleStartDebugging':
83+
return this.handler.handleStartDebugging(args);
84+
case 'handleStopDebugging':
85+
return this.handler.handleStopDebugging();
86+
case 'handleStepOver':
87+
return this.handler.handleStepOver();
88+
case 'handleStepInto':
89+
return this.handler.handleStepInto();
90+
case 'handleStepOut':
91+
return this.handler.handleStepOut();
92+
case 'handleContinue':
93+
return this.handler.handleContinue();
94+
case 'handleRestart':
95+
return this.handler.handleRestart();
96+
case 'handleAddBreakpoint':
97+
return this.handler.handleAddBreakpoint(args);
98+
case 'handleRemoveBreakpoint':
99+
return this.handler.handleRemoveBreakpoint(args);
100+
case 'handleClearAllBreakpoints':
101+
return this.handler.handleClearAllBreakpoints();
102+
case 'handleListBreakpoints':
103+
return this.handler.handleListBreakpoints();
104+
case 'handleGetVariables':
105+
return this.handler.handleGetVariables(args);
106+
case 'handleEvaluateExpression':
107+
return this.handler.handleEvaluateExpression(args);
108+
default:
109+
return Promise.reject(new Error(`Unknown control op: ${op}`));
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)