MCP GDB Server

by signal-slot
Verified
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; import { spawn, ChildProcess } from 'child_process'; import * as readline from 'readline'; import * as fs from 'fs'; import * as path from 'path'; // Interface for GDB session interface GdbSession { process: ChildProcess; rl: readline.Interface; ready: boolean; id: string; target?: string; workingDir?: string; } // Map to store active GDB sessions const activeSessions = new Map<string, GdbSession>(); class GdbServer { private server: Server; constructor() { this.server = new Server( { name: 'mcp-gdb-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { // Clean up all active GDB sessions for (const [id, session] of activeSessions.entries()) { await this.terminateGdbSession(id); } await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'gdb_start', description: 'Start a new GDB session', inputSchema: { type: 'object', properties: { gdbPath: { type: 'string', description: 'Path to the GDB executable (optional, defaults to "gdb")' }, workingDir: { type: 'string', description: 'Working directory for GDB (optional)' } } } }, { name: 'gdb_load', description: 'Load a program into GDB', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, program: { type: 'string', description: 'Path to the program to debug' }, arguments: { type: 'array', items: { type: 'string' }, description: 'Command-line arguments for the program (optional)' } }, required: ['sessionId', 'program'] } }, { name: 'gdb_command', description: 'Execute a GDB command', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, command: { type: 'string', description: 'GDB command to execute' } }, required: ['sessionId', 'command'] } }, { name: 'gdb_terminate', description: 'Terminate a GDB session', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' } }, required: ['sessionId'] } }, { name: 'gdb_list_sessions', description: 'List all active GDB sessions', inputSchema: { type: 'object', properties: {} } }, { name: 'gdb_attach', description: 'Attach to a running process', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, pid: { type: 'number', description: 'Process ID to attach to' } }, required: ['sessionId', 'pid'] } }, { name: 'gdb_load_core', description: 'Load a core dump file', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, program: { type: 'string', description: 'Path to the program executable' }, corePath: { type: 'string', description: 'Path to the core dump file' } }, required: ['sessionId', 'program', 'corePath'] } }, { name: 'gdb_set_breakpoint', description: 'Set a breakpoint', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, location: { type: 'string', description: 'Breakpoint location (e.g., function name, file:line)' }, condition: { type: 'string', description: 'Breakpoint condition (optional)' } }, required: ['sessionId', 'location'] } }, { name: 'gdb_continue', description: 'Continue program execution', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' } }, required: ['sessionId'] } }, { name: 'gdb_step', description: 'Step program execution', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, instructions: { type: 'boolean', description: 'Step by instructions instead of source lines (optional)' } }, required: ['sessionId'] } }, { name: 'gdb_next', description: 'Step over function calls', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, instructions: { type: 'boolean', description: 'Step by instructions instead of source lines (optional)' } }, required: ['sessionId'] } }, { name: 'gdb_finish', description: 'Execute until the current function returns', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' } }, required: ['sessionId'] } }, { name: 'gdb_backtrace', description: 'Show call stack', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, full: { type: 'boolean', description: 'Show variables in each frame (optional)' }, limit: { type: 'number', description: 'Maximum number of frames to show (optional)' } }, required: ['sessionId'] } }, { name: 'gdb_print', description: 'Print value of expression', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, expression: { type: 'string', description: 'Expression to evaluate' } }, required: ['sessionId', 'expression'] } }, { name: 'gdb_examine', description: 'Examine memory', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, expression: { type: 'string', description: 'Memory address or expression' }, format: { type: 'string', description: 'Display format (e.g., "x" for hex, "i" for instruction)' }, count: { type: 'number', description: 'Number of units to display' } }, required: ['sessionId', 'expression'] } }, { name: 'gdb_info_registers', description: 'Display registers', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'GDB session ID' }, register: { type: 'string', description: 'Specific register to display (optional)' } }, required: ['sessionId'] } } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { // Route the tool call to the appropriate handler based on the tool name switch (request.params.name) { case 'gdb_start': return await this.handleGdbStart(request.params.arguments); case 'gdb_load': return await this.handleGdbLoad(request.params.arguments); case 'gdb_command': return await this.handleGdbCommand(request.params.arguments); case 'gdb_terminate': return await this.handleGdbTerminate(request.params.arguments); case 'gdb_list_sessions': return await this.handleGdbListSessions(); case 'gdb_attach': return await this.handleGdbAttach(request.params.arguments); case 'gdb_load_core': return await this.handleGdbLoadCore(request.params.arguments); case 'gdb_set_breakpoint': return await this.handleGdbSetBreakpoint(request.params.arguments); case 'gdb_continue': return await this.handleGdbContinue(request.params.arguments); case 'gdb_step': return await this.handleGdbStep(request.params.arguments); case 'gdb_next': return await this.handleGdbNext(request.params.arguments); case 'gdb_finish': return await this.handleGdbFinish(request.params.arguments); case 'gdb_backtrace': return await this.handleGdbBacktrace(request.params.arguments); case 'gdb_print': return await this.handleGdbPrint(request.params.arguments); case 'gdb_examine': return await this.handleGdbExamine(request.params.arguments); case 'gdb_info_registers': return await this.handleGdbInfoRegisters(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } private async handleGdbStart(args: any) { const gdbPath = args.gdbPath || 'gdb'; const workingDir = args.workingDir || process.cwd(); // Create a unique session ID const sessionId = Date.now().toString(); try { // Start GDB process with MI mode enabled for machine interface const gdbProcess = spawn(gdbPath, ['--interpreter=mi'], { cwd: workingDir, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] }); // Create readline interface for reading GDB output const rl = readline.createInterface({ input: gdbProcess.stdout, terminal: false }); // Create new GDB session const session: GdbSession = { process: gdbProcess, rl, ready: false, id: sessionId, workingDir }; // Store session in active sessions map activeSessions.set(sessionId, session); // Collect GDB output until ready let outputBuffer = ''; // Wait for GDB to be ready (when it outputs the initial prompt) await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('GDB start timeout')); }, 10000); // 10 second timeout rl.on('line', (line) => { // Append line to output buffer outputBuffer += line + '\n'; // Check if GDB is ready (outputs prompt) if (line.includes('(gdb)') || line.includes('^done')) { clearTimeout(timeout); session.ready = true; resolve(); } }); gdbProcess.stderr.on('data', (data) => { outputBuffer += `[stderr] ${data.toString()}\n`; }); gdbProcess.on('error', (err) => { clearTimeout(timeout); reject(err); }); gdbProcess.on('exit', (code) => { clearTimeout(timeout); if (!session.ready) { reject(new Error(`GDB process exited with code ${code}`)); } }); }); return { content: [ { type: 'text', text: `GDB session started with ID: ${sessionId}\n\nOutput:\n${outputBuffer}` } ] }; } catch (error) { // Clean up if an error occurs if (activeSessions.has(sessionId)) { const session = activeSessions.get(sessionId)!; session.process.kill(); session.rl.close(); activeSessions.delete(sessionId); } const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to start GDB: ${errorMessage}` } ], isError: true }; } } private async handleGdbLoad(args: any) { const { sessionId, program, arguments: programArgs = [] } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Normalize path if working directory is set const normalizedPath = session.workingDir && !path.isAbsolute(program) ? path.resolve(session.workingDir, program) : program; // Update session target session.target = normalizedPath; // Execute file command to load program const loadCommand = `file "${normalizedPath}"`; const loadOutput = await this.executeGdbCommand(session, loadCommand); // Set program arguments if provided let argsOutput = ''; if (programArgs.length > 0) { const argsCommand = `set args ${programArgs.join(' ')}`; argsOutput = await this.executeGdbCommand(session, argsCommand); } return { content: [ { type: 'text', text: `Program loaded: ${normalizedPath}\n\nOutput:\n${loadOutput}${argsOutput ? '\n' + argsOutput : ''}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to load program: ${errorMessage}` } ], isError: true }; } } private async handleGdbCommand(args: any) { const { sessionId, command } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Command: ${command}\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to execute command: ${errorMessage}` } ], isError: true }; } } private async handleGdbTerminate(args: any) { const { sessionId } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } try { await this.terminateGdbSession(sessionId); return { content: [ { type: 'text', text: `GDB session terminated: ${sessionId}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to terminate GDB session: ${errorMessage}` } ], isError: true }; } } private async handleGdbListSessions() { const sessions = Array.from(activeSessions.entries()).map(([id, session]) => ({ id, target: session.target || 'No program loaded', workingDir: session.workingDir || process.cwd() })); return { content: [ { type: 'text', text: `Active GDB Sessions (${sessions.length}):\n\n${JSON.stringify(sessions, null, 2)}` } ] }; } private async handleGdbAttach(args: any) { const { sessionId, pid } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { const output = await this.executeGdbCommand(session, `attach ${pid}`); return { content: [ { type: 'text', text: `Attached to process ${pid}\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to attach to process: ${errorMessage}` } ], isError: true }; } } private async handleGdbLoadCore(args: any) { const { sessionId, program, corePath } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // First load the program const fileOutput = await this.executeGdbCommand(session, `file "${program}"`); // Then load the core file const coreOutput = await this.executeGdbCommand(session, `core-file "${corePath}"`); // Get backtrace to show initial state const backtraceOutput = await this.executeGdbCommand(session, "backtrace"); return { content: [ { type: 'text', text: `Core file loaded: ${corePath}\n\nOutput:\n${fileOutput}\n${coreOutput}\n\nBacktrace:\n${backtraceOutput}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to load core file: ${errorMessage}` } ], isError: true }; } } private async handleGdbSetBreakpoint(args: any) { const { sessionId, location, condition } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Set breakpoint let command = `break ${location}`; const output = await this.executeGdbCommand(session, command); // Set condition if provided let conditionOutput = ''; if (condition) { // Extract breakpoint number from output (assumes format like "Breakpoint 1 at...") const match = output.match(/Breakpoint (\d+)/); if (match && match[1]) { const bpNum = match[1]; const conditionCommand = `condition ${bpNum} ${condition}`; conditionOutput = await this.executeGdbCommand(session, conditionCommand); } } return { content: [ { type: 'text', text: `Breakpoint set at: ${location}${condition ? ` with condition: ${condition}` : ''}\n\nOutput:\n${output}${conditionOutput ? '\n' + conditionOutput : ''}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to set breakpoint: ${errorMessage}` } ], isError: true }; } } private async handleGdbContinue(args: any) { const { sessionId } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { const output = await this.executeGdbCommand(session, "continue"); return { content: [ { type: 'text', text: `Continued execution\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to continue execution: ${errorMessage}` } ], isError: true }; } } private async handleGdbStep(args: any) { const { sessionId, instructions = false } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Use stepi for instruction-level stepping, otherwise step const command = instructions ? "stepi" : "step"; const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Stepped ${instructions ? 'instruction' : 'line'}\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to step: ${errorMessage}` } ], isError: true }; } } private async handleGdbNext(args: any) { const { sessionId, instructions = false } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Use nexti for instruction-level stepping, otherwise next const command = instructions ? "nexti" : "next"; const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Stepped over ${instructions ? 'instruction' : 'function call'}\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to step over: ${errorMessage}` } ], isError: true }; } } private async handleGdbFinish(args: any) { const { sessionId } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { const output = await this.executeGdbCommand(session, "finish"); return { content: [ { type: 'text', text: `Finished current function\n\nOutput:\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to finish function: ${errorMessage}` } ], isError: true }; } } private async handleGdbBacktrace(args: any) { const { sessionId, full = false, limit } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Build backtrace command with options let command = full ? "backtrace full" : "backtrace"; if (typeof limit === 'number') { command += ` ${limit}`; } const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Backtrace${full ? ' (full)' : ''}${limit ? ` (limit: ${limit})` : ''}:\n\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to get backtrace: ${errorMessage}` } ], isError: true }; } } private async handleGdbPrint(args: any) { const { sessionId, expression } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { const output = await this.executeGdbCommand(session, `print ${expression}`); return { content: [ { type: 'text', text: `Print ${expression}:\n\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to print expression: ${errorMessage}` } ], isError: true }; } } private async handleGdbExamine(args: any) { const { sessionId, expression, format = 'x', count = 1 } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Format examine command: x/[count][format] [expression] const command = `x/${count}${format} ${expression}`; const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Examine ${expression} (format: ${format}, count: ${count}):\n\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to examine memory: ${errorMessage}` } ], isError: true }; } } private async handleGdbInfoRegisters(args: any) { const { sessionId, register } = args; if (!activeSessions.has(sessionId)) { return { content: [ { type: 'text', text: `No active GDB session with ID: ${sessionId}` } ], isError: true }; } const session = activeSessions.get(sessionId)!; try { // Build info registers command, optionally with specific register const command = register ? `info registers ${register}` : `info registers`; const output = await this.executeGdbCommand(session, command); return { content: [ { type: 'text', text: `Register info${register ? ` for ${register}` : ''}:\n\n${output}` } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Failed to get register info: ${errorMessage}` } ], isError: true }; } } /** * Execute a GDB command and wait for the response */ private executeGdbCommand(session: GdbSession, command: string): Promise<string> { return new Promise<string>((resolve, reject) => { if (!session.ready) { reject(new Error('GDB session is not ready')); return; } // Write command to GDB's stdin if (session.process.stdin) { session.process.stdin.write(command + '\n'); } else { reject(new Error('GDB stdin is not available')); return; } let output = ''; let responseComplete = false; // Create a one-time event handler for GDB output const onLine = (line: string) => { output += line + '\n'; // Check if this line indicates the end of the GDB response if (line.includes('(gdb)') || line.includes('^done') || line.includes('^error')) { responseComplete = true; // If we've received the complete response, resolve the promise if (responseComplete) { // Remove the listener to avoid memory leaks session.rl.removeListener('line', onLine); resolve(output); } } }; // Add the line handler to the readline interface session.rl.on('line', onLine); // Set a timeout to prevent hanging const timeout = setTimeout(() => { session.rl.removeListener('line', onLine); reject(new Error('GDB command timed out')); }, 10000); // 10 second timeout // Handle GDB errors const errorHandler = (data: Buffer) => { const errorText = data.toString(); output += `[stderr] ${errorText}\n`; }; // Add error handler if (session.process.stderr) { session.process.stderr.once('data', errorHandler); } // Clean up event handlers when the timeout expires timeout.unref(); }); } /** * Terminate a GDB session */ private async terminateGdbSession(sessionId: string): Promise<void> { if (!activeSessions.has(sessionId)) { throw new Error(`No active GDB session with ID: ${sessionId}`); } const session = activeSessions.get(sessionId)!; // Send quit command to GDB try { await this.executeGdbCommand(session, 'quit'); } catch (error) { // Ignore errors from quit command, we'll force kill if needed } // Force kill the process if it's still running if (!session.process.killed) { session.process.kill(); } // Close the readline interface session.rl.close(); // Remove from active sessions activeSessions.delete(sessionId); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('GDB MCP server running on stdio'); } } // Create and run the server const server = new GdbServer(); server.run().catch((error) => { console.error('Failed to start GDB MCP server:', error); process.exit(1); });