MCP GDB Server
by signal-slot
Verified
- mcp-gdb
- src
#!/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);
});