#!/usr/bin/env node
/**
* Standalone MCP server for VSCode debugging tools
* This server runs as a separate process and communicates with VSCode via IPC
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
CallToolRequest,
} from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
class StandaloneMcpServer {
private server: Server;
constructor() {
this.server = new Server({
name: 'vscode-mcp-debugger',
version: '1.0.0',
capabilities: {
tools: {},
},
});
this.setupToolHandlers();
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'add_breakpoint',
description: 'Add a breakpoint to a specific file and line number',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Absolute path to the file where breakpoint should be added'
},
lineNumber: {
type: 'number',
description: 'Line number where breakpoint should be added (1-based)'
},
condition: {
type: 'string',
description: 'Optional condition for the breakpoint'
}
},
required: ['filePath', 'lineNumber']
}
},
{
name: 'remove_breakpoint',
description: 'Remove a breakpoint from a specific file and line number',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Absolute path to the file where breakpoint should be removed'
},
lineNumber: {
type: 'number',
description: 'Line number where breakpoint should be removed (1-based)'
}
},
required: ['filePath', 'lineNumber']
}
},
{
name: 'start_debugging',
description: 'Start debugging with a specific configuration',
inputSchema: {
type: 'object',
properties: {
configurationName: {
type: 'string',
description: 'Name of the debug configuration to use'
},
workspaceFolderPath: {
type: 'string',
description: 'Path to the workspace folder'
}
},
required: []
}
},
{
name: 'stop_debugging',
description: 'Stop the current debugging session',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Optional session ID to stop specific session'
}
},
required: []
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'add_breakpoint':
return await this.addBreakpoint(args);
case 'remove_breakpoint':
return await this.removeBreakpoint(args);
case 'start_debugging':
return await this.startDebugging(args);
case 'stop_debugging':
return await this.stopDebugging(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
});
}
private async sendCommandToExtension(command: string, args: any): Promise<any> {
// Create a communication mechanism with the VSCode extension
// We'll use a simple file-based IPC for now
const tempDir = os.tmpdir();
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const requestFile = path.join(tempDir, `vscode_mcp_${requestId}.json`);
const responseFile = path.join(tempDir, `vscode_mcp_response_${requestId}.json`);
try {
// Write the command request
const request = {
command,
args,
responseFile,
timestamp: Date.now()
};
fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
// Wait for response (with timeout)
const maxWaitTime = 10000; // 10 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (fs.existsSync(responseFile)) {
const responseData = fs.readFileSync(responseFile, 'utf8');
const response = JSON.parse(responseData);
// Clean up files
try {
fs.unlinkSync(requestFile);
fs.unlinkSync(responseFile);
} catch (e) {
// Ignore cleanup errors
}
return response;
}
// Wait a bit before checking again
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error('Timeout waiting for VSCode extension response');
} catch (error) {
// Clean up request file on error
try {
if (fs.existsSync(requestFile)) {
fs.unlinkSync(requestFile);
}
} catch (e) {
// Ignore cleanup errors
}
throw error;
}
}
private async addBreakpoint(args: any) {
const { filePath, lineNumber, condition } = args;
try {
const result = await this.sendCommandToExtension('addBreakpoint', {
filePath,
lineNumber,
condition
});
if (result.success) {
return {
content: [
{
type: 'text',
text: `Successfully added breakpoint at ${filePath}:${lineNumber}${condition ? ` with condition: ${condition}` : ''}`
}
]
};
} else {
throw new Error(result.error || 'Failed to add breakpoint');
}
} catch (error) {
throw new Error(`Failed to add breakpoint: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async removeBreakpoint(args: any) {
const { filePath, lineNumber } = args;
try {
const result = await this.sendCommandToExtension('removeBreakpoint', {
filePath,
lineNumber
});
if (result.success) {
return {
content: [
{
type: 'text',
text: `Successfully removed breakpoint at ${filePath}:${lineNumber}`
}
]
};
} else {
return {
content: [
{
type: 'text',
text: result.error || `No breakpoint found at ${filePath}:${lineNumber}`
}
]
};
}
} catch (error) {
throw new Error(`Failed to remove breakpoint: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async startDebugging(args: any) {
const { configurationName, workspaceFolderPath } = args;
try {
const result = await this.sendCommandToExtension('startDebugging', {
configurationName,
workspaceFolderPath
});
if (result.success) {
return {
content: [
{
type: 'text',
text: `Successfully started debugging${configurationName ? ` with configuration: ${configurationName}` : ''}`
}
]
};
} else {
throw new Error(result.error || 'Failed to start debugging');
}
} catch (error) {
throw new Error(`Failed to start debugging: ${error instanceof Error ? error.message : String(error)}`);
}
}
private async stopDebugging(args: any) {
const { sessionId } = args;
try {
const result = await this.sendCommandToExtension('stopDebugging', {
sessionId
});
if (result.success) {
return {
content: [
{
type: 'text',
text: sessionId
? `Successfully stopped debugging session: ${sessionId}`
: 'Successfully stopped active debugging session'
}
]
};
} else {
return {
content: [
{
type: 'text',
text: result.error || 'No active debugging session to stop'
}
]
};
}
} catch (error) {
throw new Error(`Failed to stop debugging: ${error instanceof Error ? error.message : String(error)}`);
}
}
public async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('MCP Debugger Tools Server started'); // Use stderr for logging
}
}
// Start the server when run as a standalone script
if (require.main === module) {
const server = new StandaloneMcpServer();
server.start().catch((error) => {
console.error('Failed to start MCP server:', error);
process.exit(1);
});
}