Extending GhidraMCP with Debugging Capabilities: A Developer’s Journey #
Introduction #
The GhidraMCP project started as a tool to expose Ghidra’s static analysis capabilities—decompilation, function renaming, cross-references—via the Model Context Protocol (MCP). But reverse engineering often requires dynamic analysis, and we needed the MCP server to work seamlessly with Ghidra’s debugger as well.
This post chronicles how we extended the existing GhidraMCP server with debugging capabilities, the architectural decisions we made, the bugs we encountered and fixed along the way, and how it was used in a real-world reverse engineering scenario.
The Starting Point #
The original GhidraMCP server (on the main branch) provided:
- Function listing and search
- Decompilation
- Function/variable/data renaming
- Cross-reference queries
- String search
- Class/namespace discovery
All of this ran through a single HTTP server on port 8080. The debugger integration was entirely new territory.
The Extension: Adding Debugger Support #
We added a comprehensive set of debugging tools to the MCP server:
Execution Control #
debug_run()- Continue/resume executiondebug_stop()- Halt executiondebug_step_into()- Step into function callsdebug_step_over()- Step over function callsdebug_step_out()- Step out of current function
Breakpoint Management #
debug_set_breakpoint()- Set breakpoints (with support forsw_execute,hw_execute,read,writekinds)debug_remove_breakpoint()- Remove breakpointsdebug_list_breakpoints()- List all active breakpoints
State Inspection #
debug_registers()- Read current register valuesdebug_memory()- Read memory at addressesdebug_status()- Query debug session statedebug_get_rip()- Get current instruction pointer
Architectural Decision: Dual-Port Design #
One of the first major decisions was how to handle the distinction between CodeBrowser (static analysis) and Debugger (dynamic analysis) tools.
We chose a dual-port architecture:
- Port 8080: CodeBrowser-related endpoints (decompilation, renaming, listing)
- Port 9090: Debugger-specific endpoints (breakpoints, memory, registers, execution control)
This separation provides several benefits:
- Clarity: It’s immediately clear which context you’re working in
- Isolation: Issues in one mode don’t affect the other
- Routing: The Python bridge can easily dispatch to the correct server
The implementation required fixing the singleton pattern in the Java plugin to allow both CodeBrowser and Debugger tools to run their own HTTP servers.
Commit-by-Commit Development History #
Commit 1: “Add Ghidra debugger integration with MCP tools” (48df3c2) #
This foundational commit added the initial debugging capabilities:
- New HTTP endpoints in the Java plugin:
/debug/setBreakpoint,/debug/removeBreakpoint,/debug/listBreakpoints,/debug/run,/debug/stop,/debug/stepInto,/debug/stepOver,/debug/stepOut,/debug/registers,/debug/memory,/debug/status - Python MCP wrapper tools for all debugger functions
- Breakpoint kind support (sw_execute, hw_execute, read, write)
- Fixed pom.xml to use correct Ghidra 11.3 Debugger module paths
Commit 2: “Add dual-port support for CodeBrowser and Debugger” (1b7aca2) #
Separated the servers to prevent context confusion:
- Separate HTTP servers for CodeBrowser (8080) and Debugger (9090)
- Endpoint routing in Python bridge to dispatch debug commands to correct port
- Debug logging for requests/responses
The initial implementation had a bug: a single serverStarted boolean was shared between both tool types. When the Debugger tool loaded first and started the server on port 9090, the CodeBrowser tool saw serverStarted=true and skipped starting on port 8080. The fix used separate booleans: codeBrowserServerStarted and debuggerServerStarted.
Commit 3: “Implement programmatic debug control via Ghidra Target API” (aa51b26) #
The initial implementation of run/step commands were stubs that told users to use the Ghidra UI. This commit implemented actual control by:
- Getting the current Target from DebuggerTraceManagerService
- Using
target.collectActions()to get the appropriate action:ActionName.RESUMEfor run/continueActionName.INTERRUPTfor stopActionName.STEP_INTOfor step intoActionName.STEP_OVERfor step overActionName.STEP_OUTfor step out
- Invoking actions via
entry.invokeAsyncWithoutTimeout(false)
This mirrors exactly how Ghidra’s own DebuggerControlPlugin works.
Commit 4: “Fix register reading: use getViewValue() and getViewSnap()” (4e898c9) #
The register reading wasn’t returning data. The fix:
- Use
coords.getViewSnap()instead oftraces.getCurrentSnap() - Use
memSpace.getViewValue(platform, snap, reg)instead ofgetBytes() - Call
regVal.getUnsignedValue()to get the BigInteger value - Check
regVal.hasValue()to filter registers with known values
This followed the pattern used in Ghidra’s DebuggerRegistersProvider.java.
Commit 5: “Rename CodeBrowser UI tools and add debug_get_rip” (66f6239) #
AI agents were confusing CodeBrowser UI state with debugger state:
- Renamed
get_current_address→get_codebrowser_cursor_address - Renamed
get_current_function→get_codebrowser_cursor_function - Added
debug_get_rip()to get instruction pointer from debug session - Updated documentation to clearly distinguish UI tools from debugger tools
Commit 6: “Add trace memory support to handle runtime addresses automatically” (3691994) #
Memory reading now detects active debugger traces:
- When trace is active, reads from trace memory (handles runtime VAs)
- When no trace, falls back to static program memory
- Trace automatically maps runtime addresses to correct locations
- Memory dumps now show source (trace/program) for clarity
Commit 7: “Fix trace memory reading: add buf.clear() before getViewBytes()” (6495171) #
A subtle bug: memory reading returned all zeros because:
ByteBuffer.wrap()creates a buffer that needs explicitclear()before being written to- Without
buf.clear(),getViewBytes()reports bytes read but doesn’t populate the buffer - The fix follows the canonical pattern from Ghidra’s CachedBytePage.java:72
Commit 8: “Fix Python bridge incorrectly splitting responses character-by-character” (138c6ef) #
Several functions used "\n".join() on string responses, which in Python splits strings into individual characters:
# WRONG - splits into one char per line
return "\n".join(safe_get("decompile_function", {"address": address}))
# CORRECT - return string directly
return safe_get("decompile_function", {"address": address})Affected functions: get_function_by_address, get_codebrowser_cursor_address, get_codebrowser_cursor_function, decompile_function_by_address
Commit 9: “Disable DNS rebinding protection in MCP transport security” (9c926bb) #
Added a security setting to allow MCP transport to work correctly.
Commit 10: “Fix breakpoint removal: implement actual deletion logic” (6074ff8) #
Breakpoint removal was finding breakpoints but returning an error instead of deleting them. The code had a comment claiming a Trace was needed, but LogicalBreakpoint.delete() works without one:
- Replaced placeholder error with actual deletion using
bp.delete().get() - Returns count of removed breakpoints
Commit 11: “Fix heap memory reading: capture from live target before trace read” (8e3368f) #
Ghidra trace memory only returns bytes with state KNOWN—memory explicitly captured. Heap regions often aren’t recorded, causing reads to return zeros.
Solution: Before reading from trace, try to capture from live target first:
- Use
target.readMemory()to fetch and record memory on-demand - Falls back to trace memory if target unavailable
- Add 100ms delay for memory to be recorded into trace
Real-World Usage: Debugging a Binary #
The extended GhidraMCP was used in a real reverse engineering scenario—debugging a complex server binary to understand one of its sub-components. Here’s how it played out:
Setting Up the Session #
The binary was loaded in both Ghidra CodeBrowser (for static analysis) and Debugger (for dynamic analysis). Breakpoints were set on key functions:
# Setting breakpoints via MCP
debug_set_breakpoint(address="0x005a3830") # binarypacker::unpack
debug_set_breakpoint(address="0x00584e80") # openLicense
debug_set_breakpoint(address="0x00585720") # parseLicenseComponentsDebugging Session #
The debugging session proceeded as follows:
-
Starting the target: The debug target was launched and connected via gdbserver
-
Continuing execution: Using
debug_run()to resume execution until breakpoints -
Reading register state: When hitting breakpoints,
debug_registers()provided full register state:
Thread: ghidra.trace.database.thread.DBTraceThread@26370a21
Frame: 0
Snap: 22
Register values:
RAX=0x0 RCX=0x1 RDX=0x5555558c6ee0 RBX=0x7fffffffdf40
RSP=0x7fffffffde88 RBP=0x7fffffffdf20 RSI=0x555556549ba0 RDI=0x555556677d10
R8=0x70 R9=0x7ffff6ecbb20 R10=0x80 R11=0x4
RIP=0x7ffff6fbc680 ...- Observing execution patterns: By continuing execution and examining registers across multiple stops, patterns emerged—particular register values remained consistent (like exception type pointers), while others changed (like loop counters in ZMM registers).
Challenges Encountered #
The debugging session revealed additional challenges:
-
Container restrictions: Running in a container caused “personality set failed: Operation not permitted” errors when trying to debug certain targets, requiring elevated privileges or different execution environments.
-
GDB integration issues: The GDB-based debugging had issues populating the Ghidra trace properly, resulting in “add initial events for cli/target/process failed” errors.
-
GDB pl3_ssp register error: Ghidra’s GDB stub requests reading the
pl3_sspregister which isn’t available in ring3 (user space). The fix involved adding proper error handling for register reading failures to gracefully handle registers that can’t be read. -
Snapshots not updating: Sometimes the trace snapshot wouldn’t advance, requiring manual intervention in Ghidra’s UI.
Despite these challenges, the MCP tools enabled remote debugging without requiring direct GUI interaction—a significant improvement for automated analysis workflows.
Key Takeaways #
1. Ghidra’s Debugger API is Complex but Powerful #
The Ghidra debugger API uses several layers:
- Trace: A recording of a debug session
- Target: The live debugged process
- LogicalBreakpoint: Combines program bookmarks with trace breakpoints
Understanding how these interact was crucial. The getViewValue() vs getBytes() difference in register reading took significant investigation.
2. Test Both Static and Dynamic Contexts #
Many APIs behave differently:
- Memory reading: static program vs. trace memory
- Breakpoints: program bookmarks vs. trace locations
- Addresses: static offsets vs. runtime VAs
Always test tools in both CodeBrowser and Debugger modes.
3. Simple Bugs Are Hardest to Find #
The Python "\n".join() bug was trivial in retrospect—it multiplied across four functions and wasn’t obvious until examining how responses were processed. The ByteBuffer clear() issue was equally subtle.
4. Verify API Assumptions Against Source #
When breakpoint removal “required a Trace,” we verified against Ghidra source and found LogicalBreakpoint.delete() works without one. The original comment was incorrect.
5. Dual-Port Architecture Prevents Confusion #
Separating CodeBrowser (8080) and Debugger (9090) ports prevents AI agents from accidentally using the wrong context. Combined with clear naming (get_codebrowser_cursor_* vs debug_get_rip), this makes the tool’s purpose unambiguous.
6. Remote Debugging Opens New Workflows #
With the MCP server, debugging can be performed remotely without GUI interaction. This enables:
- Automated breakpoint placement across sessions
- Systematic register/memory examination at breakpoints
- Integration with scripting pipelines for repetitive analysis tasks
Future Improvements #
Several areas remain for enhancement:
-
Dynamic Address Handling: Make memory reading fully automatic without requiring users to distinguish between static and runtime addresses
-
Async Operations: Support long-running debug operations without blocking
-
Multiple Trace Support: Handle multiple simultaneous debug targets
-
Event Subscriptions: Allow AI agents to subscribe to debug events
-
Better Error Recovery: More descriptive errors that guide users toward solutions
-
Better Debugger Compatibility: Improve integration with different debugger backends (gdb, lldb) to handle edge cases in trace recording
Conclusion #
Extending GhidraMCP with debugging capabilities required understanding Ghidra’s complex debugger API, making architectural decisions about port allocation, and fixing numerous bugs—ranging from simple string processing errors to subtle ByteBuffer handling issues.
The real-world debugging session demonstrated that the MCP server enables remote debugging workflows without GUI interaction, though challenges remain with different debugger backends and containerized environments.
The result is an MCP server that provides a seamless bridge between AI agents and Ghidra’s powerful reverse engineering capabilities, both for static analysis and dynamic debugging.
This post documents the development of the debugging extension to GhidraMCP. For source code and contributions, see the project repository.