
Wraps Ghidra's reverse engineering toolkit in an MCP interface optimized for token efficiency. Exposes eight tools covering the full RE workflow: load binaries (ELF, Mach-O, PE), decompile functions, trace cross-references, search for strings or bytes, and extract metadata. Auto-detects Swift, Objective-C, and Hermes runtimes. Runs as a lightweight stdio proxy that forwards to a shared JVM backend, so multiple Claude sessions don't each spawn a 500MB process. Defaults to a "fast" analysis profile that skips slow analyzers to stay under MCP timeouts. Includes bootstrap mode for version-to-version diffing with stable synthetic labels. Reach for this when you need programmatic access to Ghidra's decompiler without managing headless scripts or parsing XML output yourself.
Token-efficient MCP server for Ghidra-based reverse engineering. Analyze ELF, Mach-O, and PE binaries with Swift, Objective-C, and Hermes support — decompile, disassemble, trace calls and value flow, recover data types, and map addresses back to file offsets. Runs on Linux, macOS, and Windows; ships as a Claude Code plugin with a bundled reverse-engineering skill.
Bottom line: a lean, security-first Ghidra MCP. It is read-only by default — analysis tools never mutate your binaries or the server's configuration (which is frozen for the life of the process). The one tool that writes, annotate (rename / comment / prototype), is opt-in (--allow-write) and human-confirmed: every change is approved by you through an MCP elicitation prompt before it's committed, and it fails closed if your client can't ask. You get an analyst-agent that can persist its findings — under supervision — without giving up the read-only safety story.
1. Prerequisites
A 64-bit JDK 21+ and Ghidra 11.x or 12.x are required (tested with Ghidra 12.1.2 and JDK 21).
# macOS
brew install openjdk@21
brew install ghidra
# Ubuntu/Debian
sudo apt install openjdk-21-jdk
# Download Ghidra from https://ghidra-sre.org
# Arch Linux
sudo pacman -S jdk21-openjdk
yay -S ghidra
Ghidra installed via Homebrew (brew install ghidra) or to /opt/ghidra or ~/ghidra is found automatically. Elsewhere — including any Windows install — point GHIDRA_INSTALL_DIR at the extracted ghidra_<version>_PUBLIC folder.
2. Install — pick one
PATH./plugin install pyghidra-lite --marketplace johnzfitch/pyghidra-lite
.mcp.json in your project or ~/.claude.json; other clients: see MCP Configuration).pip install pyghidra-lite
{
"mcpServers": {
"pyghidra-lite": {
"command": "pyghidra-lite"
}
}
}
3. Check the environment
pyghidra-lite doctor # Ghidra, JDK, decompiler, project dir, locks; exits 1 on problems
[!TIP] Plugin users without a shell install can run the same check with
uvx pyghidra-lite doctor.
4. Use it
You: Analyze the binary at /path/to/binaries/app
Claude: [calls load, info, code...]
Installs the MCP server (run with uvx, so uv must be on
PATH) together with the pyghidra-lite:reverse-engineering workflow skill, and asks
for your Ghidra directory, cache directory, optional path restriction and whether to
allow annotate:
/plugin install pyghidra-lite --marketplace johnzfitch/pyghidra-lite
/plugin marketplace add johnzfitch/pyghidra-lite
first, then /plugin install pyghidra-lite@pyghidra-lite./reload-plugins (or start a new session) to switch to it./config.[!WARNING] If you also have a manual
pyghidra-liteentry in.mcp.jsonor~/.claude.json, remove it — otherwise every tool appears twice.
pip install pyghidra-lite
yay -S python-pyghidra-lite
git clone https://github.com/johnzfitch/pyghidra-lite
cd pyghidra-lite
pip install -e .
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"pyghidra-lite": {
"command": "uvx",
"args": ["pyghidra-lite"]
}
}
}
uvx auto-installs pyghidra-lite from PyPI on first run. Ghidra is auto-detected; set GHIDRA_INSTALL_DIR in env if needed:
{
"mcpServers": {
"pyghidra-lite": {
"command": "uvx",
"args": ["pyghidra-lite"],
"env": {
"GHIDRA_INSTALL_DIR": "/path/to/ghidra"
}
}
}
}
Create .mcp.json in your project (or ~/.claude.json for global):
{
"mcpServers": {
"pyghidra-lite": {
"command": "pyghidra-lite"
}
}
}
For single-session use or debugging, run the server directly:
{
"mcpServers": {
"pyghidra-lite": {
"command": "pyghidra-lite",
"args": ["serve"]
}
}
}
{
"mcpServers": {
"pyghidra-lite": {
"command": "pyghidra-lite",
"args": [
"serve",
"--ghidra-dir", "/path/to/ghidra"
]
}
}
}
By default, pyghidra-lite can load binaries from any path (the MCP client handles permissions). Use --restrict-path to lock down access:
{
"mcpServers": {
"pyghidra-lite": {
"command": "pyghidra-lite",
"args": [
"serve",
"--restrict-path", "/home/user/binaries",
"--restrict-path", "/opt/targets"
]
}
}
}
The HTTP/SSE transports are shared and apply DNS-rebinding protection (Host/Origin
validation). Binding to a non-loopback address additionally requires both
--restrict-path and a bearer token:
pyghidra-lite serve -t streamable-http --host 0.0.0.0 \
--restrict-path /opt/targets \
--auth-token "$PYGHIDRA_LITE_AUTH_TOKEN" \
--allowed-host re.example.com:8000 # if fronted under another hostname
Clients then send Authorization: Bearer <token> on every request. Terminate TLS
at a reverse proxy for remote access.
pyghidra-lite provides 8 read-only analysis tools plus 1 opt-in write tool, all auto-detecting format (ELF/Mach-O/PE) and language (Swift/ObjC/Hermes):
| Tool | Purpose | Key Parameters |
|---|---|---|
load | Import and analyze binary | path, profile?, fresh?, bootstrap?, bootstrap_mode? |
delete | Remove binary and cancel jobs | name |
binaries | List binaries + job status | jobs?, rank_sources? |
info | Binary overview | binary, detail? (summary/full/format/sections/entropy) |
functions | List/search functions, imports, exports | binary, query?, type? (all/swift/objc/imports/exports/types/got/dylibs), limit? |
code | Decompile, disassemble, p-code, bytes | binary, target, what? (decompile/asm/pcode/bytes/string), cfg? |
xrefs | References and call graphs | binary, target, direction?, depth?, diff? |
search | Find strings, bytes, symbols, data types | binary, query, type? (strings/symbols/bytes/all/blob/extract/types), mode?, bg? |
annotate 🔒 | Rename / comment / set prototype | binary, target, action, name?/comment?/prototype? |
🔒 annotate is the only tool that writes. It is disabled unless the server is
started with --allow-write, and every change requires interactive
confirmation (MCP elicitation) before it is committed — clients that can't
confirm get a preview only. See Writing back.
# Import and analyze
load("/path/to/binary", profile="fast")
# Version-track from a prior build, including synthetic IDs for unnamed code
load("/path/to/new.bin", profile="deep", bootstrap="old.bin", bootstrap_mode="all")
# Get overview with full triage
info("mybinary", detail="full")
# List Swift functions
functions("mybinary", type="swift")
# Decompile with CFG
code("mybinary", "main", cfg=True)
# Search strings in background
search("mybinary", ["password", "api_key"], bg=True)
# Get cross-references
xrefs("mybinary", "malloc", depth=2)
# Value flow: SSA p-code, grouped by basic block; each value is defined once
code("mybinary", "parse_header", what="pcode")
# ...or just where one argument's value goes (forward slice; ~10x smaller on real code)
code("mybinary", "parse_header,param_2", what="pcode")
# Raw bytes plus where they live in the file (for patching)
code("mybinary", "0x4011a0,64", what="bytes") # -> hex, ascii, file_offset, block
# Recovered struct layouts / enums / typedefs
search("mybinary", "sockaddr", type="types")
All tools automatically detect:
Use the type and detail parameters to access format/language-specific features.
bootstrap_mode="named": transfer only meaningful source names (default).bootstrap_mode="all": also assign stable synthetic labels to source FUN_* functions during transfer, which is useful for large version-to-version bootstrap workflows where uniqueness matters more than semantics.By default pyghidra-lite is read-only — no tool mutates your binaries. To let
an agent persist findings (rename a function, attach a comment, fix a prototype),
start the server with --allow-write:
pyghidra-lite serve --allow-write # or PYGHIDRA_LITE_ALLOW_WRITE=1
Then the annotate tool becomes usable:
annotate("mybinary", target="FUN_00401000", action="rename", name="parse_header")
annotate("mybinary", target="parse_header", action="comment", comment="validates the v2 header")
annotate("mybinary", target="parse_header", action="prototype", prototype="int parse_header(char *buf, int len)")
Every call is human-confirmed: the server sends an MCP elicitation prompt
showing the exact old -> new change, and only commits if you accept. If the
server was started without --allow-write, the tool refuses; if your MCP client
doesn't support elicitation, the tool returns a preview with applied: false
and writes nothing (fail closed). Confirmed changes are written in a single
Ghidra transaction and saved to the on-disk project.
Audit journal. Because MCP elicitation ultimately trusts the client (an
autonomous "auto-approve" client can self-confirm), every write is recorded in
annotate_audit.jsonl next to the projects — and every declined or failed
attempt is logged too. Each line records old -> new, so the journal is both an
accountability trail and an undo log; a flood of entries is your signal that an
auto-agent is churning, and the server also nudges (ctx.warning) as write
volume climbs. The journal is fail-closed and hardened: a write is recorded
before it's applied (if it can't be journaled, it isn't committed), the file
is created 0o600 and opened with O_NOFOLLOW (a symlinked journal is
refused), and it rotates by size so it can't grow without bound.
| Profile | Use Case |
|---|---|
fast | Quick triage, disables 20 slow analyzers (default) |
default | Balanced, full Ghidra analysis |
deep | Thorough analysis for obfuscated code |
The server defaults to fast to stay within MCP timeout limits. Use load(fresh=True) to run deeper analysis when needed:
# Default import uses fast profile
load("/path/to/binary")
# Re-analyze with deep profile
load("/path/to/binary", profile="deep", fresh=True)
pyghidra-lite is designed for minimal token usage:
code("func,var", what="pcode") returns only the ops a value flows into
(on ws2_32.dll WSAStartup: ~1.9K characters vs ~22K for the full listing)functions(binary, type="all") returns minimal {name, addr} pairsinfo(detail="full"), code(cfg=True), or richer type/what modes only when neededBy default, pyghidra-lite runs as a lightweight stdio proxy (~10MB) that forwards to a persistent shared HTTP backend (~500MB JVM). Multiple sessions share a single JVM instead of each spawning their own.
Claude Code session 1 ──stdio──> proxy ──┐
Claude Code session 2 ──stdio──> proxy ──┼──HTTP──> shared backend (1 JVM)
Claude Code session 3 ──stdio──> proxy ──┘ localhost:19101
The proxy auto-starts the backend on first use, and a file lock plus a port check stop concurrent proxies from spawning duplicates. The backend keeps running until pyghidra-lite stop; binaries idle for 30 minutes are unloaded from memory (the 2 most recent stay loaded) and reload transparently from disk.
[!IMPORTANT] The proxy reuses whatever backend is already listening, whatever its version. After upgrading pyghidra-lite, run
pyghidra-lite stopso the next session starts the new code. (serveand the Claude Code plugin run one server per session and don't have this issue.)
| Command | What it does |
|---|---|
pyghidra-lite | Stdio proxy (default) -- auto-starts backend |
pyghidra-lite serve | Direct stdio server (1 JVM per session) |
pyghidra-lite serve -t streamable-http | Start persistent HTTP backend manually |
pyghidra-lite stop | Stop the shared backend |
pyghidra-lite doctor [--json] | Diagnose the environment (read-only; exit 1 if analysis would fail) |
Set PYGHIDRA_LITE_NO_AUTOSTART=1 to disable auto-start (useful with systemd).
Every setting is a serve flag or an environment variable (handy in an MCP env block):
| Variable | Flag | Default | Purpose |
|---|---|---|---|
GHIDRA_INSTALL_DIR | --ghidra-dir | auto-detect | Ghidra installation |
PYGHIDRA_LITE_PROJECT_DIR | --project-dir | see below | Analysis cache |
PYGHIDRA_LITE_RESTRICT_PATHS | --restrict-path (repeatable) | unrestricted | Only load binaries under these dirs (env: OS path separator) |
PYGHIDRA_LITE_ALLOW_WRITE | --allow-write | off | Enable annotate (still human-confirmed) |
PYGHIDRA_LITE_DEFAULT_PROFILE | --profile | fast | Default analysis profile |
PYGHIDRA_LITE_TOOL_TIMEOUT | — | 300 seconds | Per-call budget so a wedged JVM call can't hang the client; 0 disables |
PYGHIDRA_LITE_AUTH_TOKEN | --auth-token | none | Bearer token for HTTP transports (required off-loopback) |
PYGHIDRA_LITE_NO_AUTOSTART | — | off | Proxy never spawns a backend |
| — | --evict-after / --min-loaded | 30 min / 2 | Memory eviction of idle binaries |
| — | --autopurge-days | off | Delete projects not opened in N days |
Each binary gets its own Ghidra project, enabling:
Projects are stored in ~/pyghidra-lite/projects/ (Windows: %LOCALAPPDATA%\pyghidra-lite\projects\),
or in the pre-0.8 location ~/.local/share/pyghidra-lite/projects/ if that already exists.
Override with --project-dir or PYGHIDRA_LITE_PROJECT_DIR. Ghidra 12+ rejects project paths with a
component starting with ., so a legacy ~/.local directory only works with Ghidra 11.x —
pyghidra-lite doctor flags this.
MIT
GHIDRA_INSTALL_DIRPath to Ghidra installation directory