Skip to content

Commit 1955bb1

Browse files
committed
Merge branch 'main' into ozzafar/reduce_vsce_package_files
2 parents aba0f89 + 2f3d0f7 commit 1955bb1

10 files changed

Lines changed: 1042 additions & 95 deletions

docs/architecture/debugMCPServer.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,26 @@ AI Agent (MCP Client)
3535

3636
## Key Concepts
3737

38-
### Tools plus the debug-live skill
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+
57+
### Tools & debug-live skill
3958

4059
`DebugMCPServer` exposes **tools** for debugger capabilities. Detailed procedural guidance
4160
(when to debug, how to structure a root-cause investigation, language-specific quirks) lives
@@ -62,11 +81,30 @@ Uses stateless HTTP POST requests for MCP communication. The express server expo
6281

6382
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.
6483

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

67102
- Class definition: `src/debugMCPServer.ts`
68103
- Tool registration: `setupTools()` method (uses `McpServer.registerTool()`)
69-
- Server startup: `start()` method (creates express app with `/mcp` route)
104+
- Server startup / router election: `start()` method (returns whether this window owns the port)
105+
- Per-window control server: `src/controlServer.ts`
106+
- Cross-window routing handler: `src/routingDebuggingHandler.ts`
107+
- Shared window registry: `src/utils/workspaceRegistry.ts`
70108
- Agent Skill (procedural workflow): `skills/debug-live/SKILL.md`
71109

72110
## Exposed Tools

docs/architecture/debuggingExecutor.md

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

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

6467
A session is considered "ready" when:
@@ -81,6 +84,7 @@ This handles cases where the debugger is still initializing (common with Python)
8184
- Interface: `IDebuggingExecutor`
8285
- State retrieval: `getCurrentDebugState()`
8386
- DAP requests: `getVariables()`, `evaluateExpression()`
87+
- Bounded DAP calls: `dapRequest()`
8488
- Session readiness: `hasActiveSession()`
8589

8690
## Breakpoint Management

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)