Headless Ghidra MCP server with P-code emulation and multi-console ROM triage.
A unified MCP (Model Context Protocol) server bridging Ghidra's headless static analysis with BizHawk's live emulation — switch between decompiling a ROM and running it on real hardware in the same session.
GBA ROMs: If analyzing Game Boy Advance ROMs, install pudii/gba-ghidra-loader in your Ghidra installation for proper ROM header parsing, mirrored memory regions, and I/O register maps. The loader repository has pre-built
.gpafiles for Ghidra 11.x.
| Dependency | Version | Required | Notes |
|---|---|---|---|
| Python | >= 3.10 | Yes | Runtime for the MCP server |
| Ghidra | 11.x or 12.x | Yes | Headless or GUI install; GHIDRA_INSTALL_DIR must point here |
| Java (JDK) | >= 17 | Yes | Bundled with Ghidra; needed for JVM bridge |
| pyghidra | >= 3.0 | Yes | Python-to-Ghidra bridge; installed automatically |
| BizHawk (EmuHawk) | Latest stable | No | Only needed for live emulation tools; BIZHAWK_EXE_PATH optional |
| Docker | Latest | No | Only needed for containerized deployment |
┌─────────────────────────────────────────────────────────────────┐
│ MCP Client (Claude / Cursor) │
│ sends JSON-RPC over stdin/stdout │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ ghidra-bizhawk-mcp │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ MCP Server (server.py) — tool registry, stdio dispatch ││
│ └──────────────────────────┬──────────────────────────────────┘│
│ │ │
│ ┌──────────────┴──────────────┐ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌──────────────────────────────┐ │
│ │ GhidraSession │ │ BizhawkBridge │ │
│ │ pyghidra → JVM │ │ TCP client → localhost:8766│ │
│ │ decompile, etc. │ └──────────────┬───────────────┘ │
│ └────────────────────┘ │ │
└───────────────────────────────────────────┼─────────────────────┘
│ TCP (newline-delimited JSON)
▼
┌──────────────────────────────┐
│ BizHawk (EmuHawk.exe) │
│ built-in socket server │
│ ┌────────────────────────┐ │
│ │ bridge.lua │ │
│ │ memory read/write │ │
│ │ joypad, savestate │ │
│ │ frame advance │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
The MCP server communicates with the MCP client exclusively over stdin/stdout — no HTTP or network listener. The only local TCP socket is a loopback-only connection (127.0.0.1:8766) between the server and BizHawk's built-in Lua socket server. This is used solely for live-emulation features and is not exposed to the network.
ghidra-bizhawk-mcp includes native out-of-the-box support for retro-reversing automation pipelines via Ghidra's static analysis, plus live emulation via BizHawk's multi-system emulator. The server bundles:
GhidraNesgba-ghidra-loaderNTRGhidraghidra-switch-loaderghidra_psx_ldrGhidra-SegaMasterSystem-LoaderThe primary entry point is triage_and_load_retro_rom. Call it with any ROM path and the server handles the rest:
# Auto-detect platform, map language, provision session
triage_and_load_retro_rom(rom_path="/data/game.gba")
# → platform: "Game Boy Advance (GBA)"
# → loader: "GBA ROM Loader"
# → arch: "ARM:LE:32:v4t"
# Decompile the main entry point on the same session
decompile_function(address="0x00001c2c")
# → decompiled C code for the GBA ROM entry routine
# Search for a known pattern (e.g. 32-bit ARM store-multiple)
search_bytes(pattern="09 08 00 01")
# → matching addresses labelled "gba_ram_start"
Instead of forcing your AI agent to spend cycles manually identifying architecture maps, register layouts, or memory segments, chain the automated ingestion pipeline:
triage_and_load_retro_rom with a target file path.NES\x1a, NTR, NSO0, GBA, SNES title vectors, PS-X EXE, SEGA, TMR SEGA, SEGA ENTERPRISES), binds the matching Ghidra language module (6502:LE:16, ARM:LE:32:v4t, AARCH64:LE:64, 65816:LE:24, MIPS:LE:32, 68000:BE:32, Z80:16, SuperH4:LE:32), loads standard address memory blocks, and links automated signature cache arrays.emulate_slice or emulate_slice_with_taint tools to analyze localized console loops — no physical console hardware or open GDB networking ports needed.| Tool | Description |
|---|---|
triage_and_load_retro_rom | Reads raw file magic bytes to detect NES, SNES, GBA, NDS, Switch, PSX, Genesis, SMS, or Dreamcast ROMs. Provisions a correctly-language-mapped Ghidra session and auto-restores cached function signatures. Returns platform, loader, architecture tag, and mapped memory blocks. |
pip install ghidra-bizhawk-mcp
Or from source:
git clone https://github.com/getanirao/ghidra-bizhawk-mcp.git
cd ghidra-bizhawk-mcp
pip install -e .
# Required: point to your Ghidra installation
export GHIDRA_INSTALL_DIR=/opt/ghidra_11.2 # Linux / macOS
set GHIDRA_INSTALL_DIR=C:\Program Files\ghidra_11.2 # Windows
# Optional: enable live BizHawk emulation
export BIZHAWK_EXE_PATH=/path/to/EmuHawk.exe
# Optional: run in mock mode (no Ghidra/BizHawk needed)
export MOCK_MODE=1
ghidra-bizhawk-mcp
The server listens on stdin/stdout — pipe it to any MCP-compatible client.
docker build -t ghidra-bizhawk-mcp .
docker run -i --rm -v /path/to/binaries:/data ghidra-bizhawk-mcp
The container bundles JDK 17, Ghidra 11.2, and the server — no host dependencies beyond Docker.
| Variable | Required | Default | Description |
|---|---|---|---|
GHIDRA_INSTALL_DIR | Yes | — | Path to Ghidra installation (e.g. /opt/ghidra_11.2) |
BIZHAWK_EXE_PATH | No | — | Path to EmuHawk.exe for live emulation features |
MOCK_MODE | No | 0 | Set to 1 to run without Ghidra/BizHawk (for testing/CI) |
Add to your claude_desktop_config.json:
{
"mcpServers": {
"ghidra-bizhawk": {
"command": "ghidra-bizhawk-mcp",
"env": {
"GHIDRA_INSTALL_DIR": "/opt/ghidra_11.2"
}
}
}
}
Add to your Cursor MCP configuration:
{
"mcpServers": {
"ghidra-bizhawk": {
"command": "ghidra-bizhawk-mcp",
"env": {
"GHIDRA_INSTALL_DIR": "/opt/ghidra_11.2"
}
}
}
}
| Tool | Description |
|---|---|
analyze_binary | Import + analyze a binary, returns a session_id. Reuses the ID if provided, otherwise auto-generates. |
list_sessions | List all active workspaces with their session IDs, binary paths, and load times. |
close_session | Close a session and free its Ghidra project resources. |
Most tools accept an optional session_id parameter — omit it to use the most recently loaded session.
| Tool | Description |
|---|---|
decompile_function | Decompile a function by name or address. |
decompile_function_paginated | Decompile with line_start, line_end, max_tokens (token-budget truncation), and summarize (strips boilerplate locals + collapsing blank lines). Prevents context-window exhaustion. |
get_data_types | List all data types defined in the program. |
get_cross_references | Cross-references to/from an address. |
get_call_graph | Recursive call graph + callers for a function. |
analyze_and_decompile_entrypoints | Composite — bulk decompile all entry points (program entry, exports, main, _start, etc.) in one call. |
generate_workspace_report | Produce a Markdown summary of the active workspace — entry points, function count, custom symbols, recovered structures, renamed functions, comments. Replaces a GUI CodeBrowser window. |
| Tool | Description |
|---|---|
rename_symbol | Rename a function or label. Stored in the Ghidra project DB. |
add_comment | Attach a comment (plate, pre, post, eol, repeatable). |
create_struct | Create a custom structured data type from a JSON member layout [{offset, name, type}, ...]. Offsets are optional. |
retype_variable | Re-type a local variable or function parameter (e.g. undefined4* → MyStruct*). |
| Tool | Description |
|---|---|
disassemble_range | Disassemble N raw instructions at an address — returns mnemonic, operands, hex bytes, and length for precise lower-level inspection. |
get_listing_range | Raw hex + ASCII dump for a byte range, equivalent to Ghidra's Listing panel. Complements disassemble_range for data regions. |
| Tool | Description |
|---|---|
search_bytes | Search the entire binary for a hex byte pattern (e.g. 09 08 00 01 or F86D0003). Returns matching addresses with context bytes and any string label at the hit. |
| Tool | Description |
|---|---|
diff_binaries | Compare two loaded sessions by function name and body size. Returns functions unique to each side and changed functions. |
Each analyze_binary call creates a named session. Sessions keep their Ghidra project open independently, so multiple binaries can be loaded concurrently:
# Load two binaries into separate sessions
s1 = analyze_binary(binary_path="/bin/a.out") # auto session_id
s2 = analyze_binary(binary_path="/bin/b.out", session_id="my_session")
# Operate on a specific session
decompile_function(function_name="main", session_id=s1.session_id)
# Diff them
diff_binaries(session_a=s1.session_id, session_b="my_session")
docker build -t ghidra-bizhawk-mcp .
# Run as an MCP subprocess
docker run -i --rm \
-v /data/binaries:/data \
ghidra-bizhawk-mcp \
--ghidra-dir /opt/ghidra
The Dockerfile bundles Ghidra 11.2 and JDK 17 in a slim Python 3.11 image. Bind-mount your binaries directory at runtime.
Package as a portable .mcpb bundle for one-click install in Claude Desktop or publishing on Smithery.
Prerequisites: Install the MCPB CLI:
npm install -g @anthropic-ai/mcpb
Build the bundle:
# From the repo root
scripts/build-mcpb.ps1
Or manually with mcpb:
mcpb pack
The output ghidra-bizhawk-mcp.mcpb wraps the server with a manifest.json that prompts for GHIDRA_INSTALL_DIR (required) and optionally BIZHAWK_EXE_PATH at install time — no manual JSON editing.
Publishing to Smithery:
smithery mcp publish ./dist/ghidra-bizhawk-mcp.mcpb -n getanirao/ghidra-bizhawk-mcp
| Tool | Description |
|---|---|
emulate_slice | Headlessly execute N instructions. Seed register state and get a step-by-step trace of register mutations. |
emulate_slice_with_taint | Same as emulate_slice but with automated taint tracking — specify a taint register (e.g. r0) and the tool flags exactly when its value is modified or propagates to other registers. |
emulate_slice_with_breakpoints | Execute until a condition is met or the count expires. Condition syntax: R0==0, R1>0xFF, R2!=R3, PC==0x1234. Stops before or after the matching instruction. |
All run inside the pyhidra process via Ghidra's EmulatorHelper — no GDB/LLDB, no network ports, no debugger stubs. Works on ARM, x86, MIPS, and any Ghidra-supported architecture.
Suppose you're reversing a GBA ROM and want to find the first time r0 becomes zero inside a loop at 0x08000100:
# Step until r0 == 0, stop before the matching instruction
result = emulate_slice_with_breakpoints(
session_id="gba_v1",
start_address="0x08000100",
max_instructions=5000,
stop_condition="R0==0",
stop_mode="before"
)
# result.exit_reason → "R0==0"
# result.instructions_executed → 312
# result.trace → [step 311: r0 goes 4→2, step 312: r0 goes 2→0]
# Check if a specific address was reached after a branch
result = emulate_slice_with_breakpoints(
session_id="gba_v1",
start_address="0x08000100",
max_instructions=5000,
stop_condition="PC==0x08001234"
)
# result.exit_reason → "PC==0x08001234"
# Use inequalities to catch bounds checks
result = emulate_slice_with_breakpoints(
session_id="gba_v1",
start_address="0x08000100",
max_instructions=5000,
stop_condition="R1>0xFF"
)
# result.exit_reason → "R1>0xFF"
# result.last_step["r1"] → 0x100
This is especially powerful for identifying copy-loop bounds (R3 >= R4), null-pointer paths (R0==0), or switch-table targets (PC==0x).
| Tool | Description |
|---|---|
calculate_function_fingerprint | Generate a structural hash for a function (vars, params, body size, branches, called funcs, embedded strings, numeric constants). Survives compiler reordering. |
export_signature_map | Build a complete {hash → name} map for every function in the current binary. Save this JSON to reuse across versions. |
apply_signature_map | Pass a previously exported signature map; the server sweeps the binary and renames every matching function automatically. |
| Tool | Description |
|---|---|
save_active_binary_signature | Fingerprint all functions and stash the map under a lineage_group_id (e.g. "my_firmware_v1"). Stored in ~/.ghidra_bizhawk_mcp/signatures/ — no JSON files to manage. |
auto_restore_signatures_from_stash | Load a stashed map by lineage_group_id and auto-rename every matching function. |
auto_stash_current_binary | Zero-input auto-stash — hashes the binary's first 4 KB, saves a map under that hash. Just analyze and call. |
auto_restore_current_binary | Zero-input auto-restore — hashes the binary, looks up a previous stash, renames matches. No group ID needed. |
list_stashed_signature_groups | List all stashed groups currently in the local cache. |
Workflow — fully automated persistence:
# Analyze v1 — stashes automatically under binary content hash
s1 = analyze_binary(binary_path="/bin/v1.bin")
auto_stash_current_binary(session_id=s1.session_id)
# Later, analyze v2 — restores automatically
s2 = analyze_binary(binary_path="/bin/v2.bin")
auto_restore_current_binary(session_id=s2.session_id)
# → 142 functions renamed, zero manual JSON handling
After configuring your MCP client (see Configuration), ask your AI agent:
ghidra-bizhawk-mcp/
├── Dockerfile
├── pyproject.toml
├── README.md
└── src/ghidra_bizhawk_mcp/
├── __init__.py
├── server.py # MCP server, tool registry, stdio transport
├── ghidra_bridge.py # GhidraSession — pyghidra wrapper, all Ghidra logic
├── lua/
│ └── bridge.lua # BizHawk-side Lua bridge for live emulation
└── tools/
├── __init__.py
├── bizhawk_bridge.py # TCP client connecting MCP ↔ BizHawk
└── ...
pyhidra.start() boots Ghidra's JVM once at server startupanalyze_binary call opens a new Ghidra project in its own named sessionsession_id (or the active default)Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
uvx ghidra-retro-mcpMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-getanirao-ghidra-retro-mcp": {
"command": "uvx",
"args": [
"ghidra-retro-mcp"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceghidra-retro-mcppypiio.github.getanirao/ghidra-retro-mcp works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.