From c4673e213b00ee676bf94f6062f3c5bd4b512303 Mon Sep 17 00:00:00 2001 From: Yumitoya8569 <77916251+Yumitoya8569@users.noreply.github.com> Date: Thu, 7 May 2026 17:58:54 +0800 Subject: [PATCH 1/4] fix: Real support for .Net xUint test --- package-lock.json | 25 +++++- package.json | 4 +- src/debugMCPServer.ts | 8 ++ src/testHostAutoAttacher.ts | 109 +++++++++++++++++++++++++ src/utils/debugConfigurationManager.ts | 13 +-- 5 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 src/testHostAutoAttacher.ts diff --git a/package-lock.json b/package-lock.json index 86e9416..69e90a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,25 @@ { "name": "debugmcpextension", - "version": "1.1.0", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "debugmcpextension", - "version": "1.1.0", + "version": "1.1.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "@types/express": "^5.0.3", "express": "^5.2.1", "jsonc-parser": "^3.3.1", + "ps-list": "^9.0.0", "zod": "^3.25.76" }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^25.3.0", + "@types/ps-list": "^6.0.0", "@types/vscode": "^1.104.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", @@ -425,6 +427,13 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/ps-list": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/ps-list/-/ps-list-6.0.0.tgz", + "integrity": "sha512-FuoItqYGkFXGrrlTraXZ5d4b7bNc9MBRWLAusHZWI9j8+LFoF+z938cizoetAQ3ui6K0BomAw5sFQYS2NVbV3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -3165,6 +3174,18 @@ "node": ">= 0.10" } }, + "node_modules/ps-list": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-9.0.0.tgz", + "integrity": "sha512-lxMEoIL/BQlk2KunFzxwUPwMvjFH7x7cmvzSLsSHpyMXl9FFfLUlfKrYwFc4wx/ZaIxxuXC4n8rjQ1CX/tkXVQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index bd9b478..0edb056 100644 --- a/package.json +++ b/package.json @@ -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": "1.1.3", + "version": "1.1.4", "publisher": "ozzafar", "author": { "name": "Oz Zafar", @@ -93,11 +93,13 @@ "@types/express": "^5.0.3", "express": "^5.2.1", "jsonc-parser": "^3.3.1", + "ps-list": "^9.0.0", "zod": "^3.25.76" }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "^25.3.0", + "@types/ps-list": "^6.0.0", "@types/vscode": "^1.104.0", "@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/parser": "^8.56.0", diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index 9e2369c..91f5127 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -11,6 +11,7 @@ import { DebuggingHandler, IDebuggingHandler } from '.'; +import { TestHostAutoAttacher } from './testHostAutoAttacher'; import { logger } from './utils/logger'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; @@ -26,12 +27,14 @@ export class DebugMCPServer { private initialized: boolean = false; private debuggingHandler: IDebuggingHandler; private transports: Map = new Map(); + private testHostAutoAttacher: TestHostAutoAttacher; constructor(port: number, timeoutInSeconds: number) { // Initialize the debugging components with dependency injection const executor = new DebuggingExecutor(); const configManager = new ConfigurationManager(); this.debuggingHandler = new DebuggingHandler(executor, configManager, timeoutInSeconds); + this.testHostAutoAttacher = new TestHostAutoAttacher(); this.port = port; } @@ -385,6 +388,11 @@ export class DebugMCPServer { // No need to track and close them manually this.transports.clear(); + // Dispose the testhost auto attacher + if (this.testHostAutoAttacher) { + this.testHostAutoAttacher.dispose(); + } + // Close the HTTP server if (this.httpServer) { await new Promise((resolve) => { diff --git a/src/testHostAutoAttacher.ts b/src/testHostAutoAttacher.ts new file mode 100644 index 0000000..db47417 --- /dev/null +++ b/src/testHostAutoAttacher.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. + +import * as vscode from 'vscode'; +import { logger } from './utils/logger'; + +/** + * Handles auto-attach to testhost process for .NET test debugging. + * Listens for coreclr launch sessions and automatically attaches to testhost. + */ +export class TestHostAutoAttacher { + private psList?: (options?: any | undefined) => Promise; + private readonly disposables: vscode.Disposable[] = []; + private readonly pidToSession = new Map(); + + + constructor() { + this.disposables.push(vscode.debug.onDidStartDebugSession(async (session) => { + + if (session.type !== 'coreclr' || !session.name.includes('DebugMCP .NET Test')) { + return; + } + + try { + const pid = await this.waitForTestHost(); + if (!vscode.debug.activeDebugSession || vscode.debug.activeDebugSession.id !== session.id) { + return; + } + + if (this.pidToSession.has(pid)) { + logger.error(`PID has been attached before (PID: ${pid})`); + return; + } + + logger.info(`Found testhost PID=${pid}, attaching...`); + logger.info(`Session=${session.id}, Name=${session.name}`); + this.pidToSession.set(pid, session.id) + await this.attachToTestHost(pid, session); + logger.info(`Successfully attached to testhost (PID: ${pid})`); + } catch (err) { + logger.error('Failed to auto attach testhost', err); + } + })); + + this.disposables.push(vscode.debug.onDidTerminateDebugSession(async (session) => { + for (const [pid, sid] of this.pidToSession) { + if (sid === session.id) { + this.pidToSession.delete(pid) + } + } + })); + } + + + /** + * Wait for testhost process to appear + */ + public async waitForTestHost(timeoutMs: number = 15000): Promise { + this.psList = this.psList ?? await import('ps-list').then(m => m.default); + + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (!vscode.debug.activeDebugSession) break; + + const processes = await this.psList(); + + const testhost = processes + .filter(p => + p.name?.includes('testhost') + && !this.pidToSession.has(p.pid) + ) + .sort((a, b) => b.pid - a.pid) + .at(0); + + if (testhost?.pid) { + return testhost.pid; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + throw new Error('testhost process not found'); + } + + /** + * Attach debugger to testhost process + */ + public async attachToTestHost(pid: number, session: vscode.DebugSession): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + await vscode.debug.startDebugging(workspaceFolder, { + type: 'coreclr', + request: 'attach', + name: 'DebugMCP Attach testhost', + processId: pid.toString() + }); + } catch (error) { + throw new Error(`Failed to attach to testhost: ${error}`); + } + } + + /** + * Dispose the listener + */ + public dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); + } + } +} diff --git a/src/utils/debugConfigurationManager.ts b/src/utils/debugConfigurationManager.ts index 967f997..422e4ab 100644 --- a/src/utils/debugConfigurationManager.ts +++ b/src/utils/debugConfigurationManager.ts @@ -173,7 +173,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { const cwd = path.dirname(fileFullPath); // Build test-specific configurations based on language - if (testName && detectedLanguage !== 'coreclr') { + if (testName) { return await this.createTestDebugConfig(detectedLanguage, fileFullPath, cwd, testName); } @@ -419,13 +419,16 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { name: `DebugMCP .NET Test: ${testName}`, program: 'dotnet', args: [ - 'test', - '--filter', `FullyQualifiedName~${testName}`, - '--no-build' + 'test', + '--no-build', + '--filter', `FullyQualifiedName~${testName}` ], console: 'integratedTerminal', cwd: cwd, - stopAtEntry: false + stopAtEntry: false, + env: { + VSTEST_HOST_DEBUG: '1' + } }; default: From 82a0362c2e61fb8a9b1c70279e9f2cd1d7b29af2 Mon Sep 17 00:00:00 2001 From: Yumitoya8569 <77916251+Yumitoya8569@users.noreply.github.com> Date: Fri, 8 May 2026 18:21:16 +0800 Subject: [PATCH 2/4] fix: .csproj is not always in the same directory as the .cs target, current assumption is incorrect. --- src/testHostAutoAttacher.ts | 8 +++++--- src/utils/debugConfigurationManager.ts | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/testHostAutoAttacher.ts b/src/testHostAutoAttacher.ts index db47417..59e7a3a 100644 --- a/src/testHostAutoAttacher.ts +++ b/src/testHostAutoAttacher.ts @@ -33,7 +33,7 @@ export class TestHostAutoAttacher { logger.info(`Found testhost PID=${pid}, attaching...`); logger.info(`Session=${session.id}, Name=${session.name}`); - this.pidToSession.set(pid, session.id) + this.pidToSession.set(pid, session.id); await this.attachToTestHost(pid, session); logger.info(`Successfully attached to testhost (PID: ${pid})`); } catch (err) { @@ -44,7 +44,7 @@ export class TestHostAutoAttacher { this.disposables.push(vscode.debug.onDidTerminateDebugSession(async (session) => { for (const [pid, sid] of this.pidToSession) { if (sid === session.id) { - this.pidToSession.delete(pid) + this.pidToSession.delete(pid); } } })); @@ -59,7 +59,9 @@ export class TestHostAutoAttacher { const start = Date.now(); while (Date.now() - start < timeoutMs) { - if (!vscode.debug.activeDebugSession) break; + if (!vscode.debug.activeDebugSession) { + break; + } const processes = await this.psList(); diff --git a/src/utils/debugConfigurationManager.ts b/src/utils/debugConfigurationManager.ts index 0630452..fa48adb 100644 --- a/src/utils/debugConfigurationManager.ts +++ b/src/utils/debugConfigurationManager.ts @@ -111,7 +111,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { // Build test-specific configurations based on language if (testName) { - return await this.createTestDebugConfig(detectedLanguage, fileFullPath, cwd, testName); + return await this.createTestDebugConfig(detectedLanguage, workingDirectory, fileFullPath, cwd, testName); } const configs: { [key: string]: vscode.DebugConfiguration } = { @@ -267,6 +267,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { */ private async createTestDebugConfig( language: string, + workingDirectory: string, fileFullPath: string, cwd: string, testName: string @@ -361,7 +362,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { '--filter', `FullyQualifiedName~${testName}` ], console: 'integratedTerminal', - cwd: cwd, + cwd: workingDirectory, stopAtEntry: false, env: { VSTEST_HOST_DEBUG: '1' From 7592783029770e5f5bdf1503b977d6ea2a01b184 Mon Sep 17 00:00:00 2001 From: Yumitoya8569 <77916251+Yumitoya8569@users.noreply.github.com> Date: Fri, 8 May 2026 18:23:19 +0800 Subject: [PATCH 3/4] fix: Continuous requests should not cause server errors. --- src/debugMCPServer.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index e0be3c6..457f902 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -28,6 +28,7 @@ export class DebugMCPServer { private debuggingHandler: IDebuggingHandler; private transports: Map = new Map(); private testHostAutoAttacher: TestHostAutoAttacher; + private allowNextTransport = true; constructor(port: number, timeoutInSeconds: number) { // Initialize the debugging components with dependency injection @@ -351,16 +352,28 @@ export class DebugMCPServer { app.post('/mcp', async (req: any, res: any) => { logger.info('New MCP request received'); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // Stateless mode - no session management - }); - res.on('close', () => { - transport.close(); - logger.info('MCP transport closed'); - }); - try { + let waitCnt = 0; + while (!this.allowNextTransport && waitCnt <= 15) { + waitCnt++; + await new Promise(resolve => setTimeout(resolve, 200)); + } + + if (!this.allowNextTransport) { + throw new Error("Error wait for transport release"); + } + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // Stateless mode - no session management + }); + res.on('close', () => { + transport.close(); + this.allowNextTransport = true; + logger.info('MCP transport closed'); + }); + await this.mcpServer!.connect(transport); + this.allowNextTransport = false; await transport.handleRequest(req, res, req.body); } catch (error) { logger.error('Error while handling MCP request', error); From fbf17c2c78d9d77bc2015df53d8c59d3d7a81276 Mon Sep 17 00:00:00 2001 From: Yumitoya8569 <77916251+Yumitoya8569@users.noreply.github.com> Date: Mon, 11 May 2026 09:15:57 +0800 Subject: [PATCH 4/4] fix: Improve transport queuing logic --- src/debugMCPServer.ts | 51 +++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index 457f902..2534209 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -28,7 +28,8 @@ export class DebugMCPServer { private debuggingHandler: IDebuggingHandler; private transports: Map = new Map(); private testHostAutoAttacher: TestHostAutoAttacher; - private allowNextTransport = true; + private transportQueue: Array<{ resolve: () => void; reject: (err: Error) => void }> = []; + private processingRequest = false; constructor(port: number, timeoutInSeconds: number) { // Initialize the debugging components with dependency injection @@ -351,29 +352,14 @@ export class DebugMCPServer { // Streamable HTTP endpoint — handles MCP protocol messages app.post('/mcp', async (req: any, res: any) => { logger.info('New MCP request received'); + await this.acquireTransportLock(); - try { - let waitCnt = 0; - while (!this.allowNextTransport && waitCnt <= 15) { - waitCnt++; - await new Promise(resolve => setTimeout(resolve, 200)); - } - - if (!this.allowNextTransport) { - throw new Error("Error wait for transport release"); - } - - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // Stateless mode - no session management - }); - res.on('close', () => { - transport.close(); - this.allowNextTransport = true; - logger.info('MCP transport closed'); - }); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // Stateless mode - no session management + }); + try { await this.mcpServer!.connect(transport); - this.allowNextTransport = false; await transport.handleRequest(req, res, req.body); } catch (error) { logger.error('Error while handling MCP request', error); @@ -408,6 +394,10 @@ export class DebugMCPServer { }); }); } + } finally { + transport.close(); + this.releaseTransportLock(); + logger.info('MCP transport closed'); } }); @@ -481,4 +471,23 @@ export class DebugMCPServer { isInitialized(): boolean { return this.initialized; } + + private async acquireTransportLock(): Promise { + if (!this.processingRequest) { + this.processingRequest = true; + return; + } + return new Promise((resolve, reject) => { + this.transportQueue.push({ resolve, reject }); + }); + } + + private releaseTransportLock(): void { + if (this.transportQueue.length > 0) { + const next = this.transportQueue.shift()!; + next.resolve(); + } else { + this.processingRequest = false; + } + } } \ No newline at end of file