English | 简体中文
Headless MCP server for Android APK static analysis, built directly on jadx-core. No JADX GUI required, no Python adapter, no plugin to install — just one JVM process speaking MCP over stdio.
jadx is two parts: the jadx-core decompiler engine, and a GUI wrapped around it for a human to click through by hand.
Give a capable LLM direct access to jadx-core and the GUI loses its reason to exist. The model walks the same class tree, resolves the same xrefs, reads the same source — headless and scriptable. As models get stronger, Java-layer reverse engineering increasingly runs end to end from the model, without a reverse engineer driving it by hand.
So this server drops the GUI and exposes jadx-core as ~26 MCP tools over stdio.
| Tool | Description |
|---|---|
status |
Whether an APK is loaded; path, class/resource counts, load duration |
load_apk |
Load an APK / DEX / JAR (replaces any current one) |
unload_apk |
Release the loaded APK and free memory |
| Tool | Description |
|---|---|
get_app_info |
Package name, version, minSdk/targetSdk, permissions |
get_android_manifest |
Full decoded AndroidManifest.xml |
get_main_activity_class |
LAUNCHER activity FQN |
get_main_application_classes_names |
FQNs of classes under the Manifest package — a lightweight "is the right APK loaded" probe |
list_classes |
Paginated class FQN list |
search_classes_by_keyword |
Substring search over class FQNs |
get_class_source |
Decompiled Java source. Auto-falls back to smali when jadx hits anti-decompile stubs (marker // [jadx java-decompile failed → smali]; smali_fallback=false to opt out). Pageable via offset (UTF-8 byte offset into full source); header reports total_bytes/next_offset. Resolves inner/$Companion names. |
get_class_summary |
Class skeleton (method signatures, fields, inner-class names) — no method bodies |
get_smali_of_class |
Smali disassembly. Pageable via offset (byte offset into full smali); header reports total_bytes/next_offset |
get_methods_of_class |
Method signatures |
get_method_by_name |
Java source of a single method; same smali fallback as get_class_source |
get_method_body |
One method as Java-if-possible-else-smali, in a JSON envelope (mode, fell_back, markers); handles overloads |
get_method_smali |
Smali of a single method (all overloads), sliced from the class disassembly |
get_inner_classes |
Inner/nested classes with both dotted full_name and raw $-joined raw_name |
resolve_class |
Resolve an inner/$Companion/fuzzy class name to its exact FQN, or list candidates |
get_fields_of_class |
Field list with types |
search_method_by_name |
Global method search (includes inner-class methods) |
get_xrefs_to_class |
References to a class |
get_xrefs_to_method |
References to a method |
get_xrefs_to_field |
References to a field |
get_strings |
String resources with optional filter |
list_resource_files |
Resource file names |
get_resource_file |
Resource file content (text or base64) |
Requires JDK 17+.
./gradlew installDist
Output: build/install/jadx-headless-mcp/bin/jadx-headless-mcp (and .bat on Windows).
For a single portable fat jar:
./gradlew shadowJar
# build/libs/jadx-headless-mcp-<version>-all.jar
No build needed? Prebuilt artifacts (fat jar + distribution zip) are attached to every GitHub Release — download one and point your MCP config at it directly.
Empty start (load an APK later via load_apk tool):
./build/install/jadx-headless-mcp/bin/jadx-headless-mcp
Or eager-load on startup:
./build/install/jadx-headless-mcp/bin/jadx-headless-mcp \
--apk /path/to/app.apk \
--max-source-bytes 200000
The server speaks MCP over stdio. Logs go to stderr.
Pass these as args alongside --apk / --max-source-bytes (all optional; load_apk can override the per-APK ones at runtime):
| Flag | Default | Meaning |
|---|---|---|
--threads <n> |
min(CPU, 4) |
jadx pre-decompile worker count (caps peak RAM). 0 = every core. |
--include-packages <list> |
— | comma-separated package/FQN prefixes to KEEP (e.g. com.gcash,com.mynt); repeatable. Dropped classes are unloaded. |
--exclude-packages <list> |
— | comma-separated package prefixes to DROP (e.g. androidx,kotlin); repeatable. Combined with include: must match include AND not exclude. |
--code-cache-size <n> |
64 |
max decompiled top-level classes kept in RAM (LRU-evicted via JavaClass.unload()). 0 = unlimited. |
--resources <full\|lite\|none> |
full |
full: all resource files; lite: only Manifest + values/strings XML + resources.arsc; none: hide non-manifest resources (get_strings unavailable). |
--max-scan <n> |
tiered | cap on classes scanned by search_classes_by_keyword code scope (0 = built-in defaults: 20000 with a package prefix, 5000 without). |
--decompile-timeout-ms <n> |
90000 |
hard wall-clock budget per full-class decompile; prevents one fat obfuscated class from wedging the process. Counts a task's own execution time, not time spent queued behind another request; queue wait is bounded separately at min(this, 30s). |
Run --help for the full list (including the --transport http remote flags below).
This repo is also a Claude Code plugin — the easiest way
to install for Claude Code, no JSON editing and no manual build. It needs JDK 17+ on PATH; on
first start it downloads the released fat jar (SHA-256 verified) and caches it.
/plugin marketplace add 1013503897/jadx-headless-mcp
/plugin install jadx-headless@jadx-headless-mcp
This bundles the MCP server plus the /jadx-headless:open-apk skill. See PLUGIN.md for
how it works, local development (claude --plugin-dir .), releasing, and community-marketplace
submission. If you'd rather wire the server up by hand (or use another client), use the manual
configuration below.
jadx-headless is a standard stdio MCP server, so any MCP-capable client can drive it — Claude Code, Claude Desktop, Cursor, Windsurf, Cline, Roo Code, VS Code / Copilot, LM Studio, Zed, Codex, Gemini CLI, and others. Claude Code has the smoothest path; the rest take one small config entry — see Other MCP clients.
One command, no JSON editing. -s user writes to the global ~/.claude.json; the default local scope only applies to the current project:
# installDist launcher
claude mcp add jadx-headless -s user -- C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat
# ...or the portable fat jar (single file; needs java on PATH)
claude mcp add jadx-headless -s user -- java -jar C:/tools/jadx-headless-mcp-<version>-all.jar
Then ask the AI to call load_apk with the path you want to analyze. Use unload_apk to free memory or switch to a different one — no config edit needed.
Register one entry, swap APKs at runtime:
{
"mcpServers": {
"jadx-headless": {
"command": "C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat"
}
}
}
To point at the portable fat jar instead:
{
"mcpServers": {
"jadx-headless": {
"command": "java",
"args": ["-jar", "C:/tools/jadx-headless-mcp-<version>-all.jar"]
}
}
}
If you only ever analyze a single APK and want it ready immediately, append the APK path via args:
{
"mcpServers": {
"jadx-headless": {
"command": "C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat",
"args": ["--apk", "C:/work/current.apk"]
}
}
}
Note:
java -jarskips the launcher's default JVM args (e.g.-XX:MaxRAMPercentage=60). stdout stays clean either way — slf4j-simple logs to stderr andMain.ktredirectsSystem.outto stderr — but if you need a memory ceiling, pass it yourself viaJAVA_OPTS/-XX. The installDist launcher keeps those defaults.
Point the client at the same launcher (or java -jar the fat jar); only the surrounding config format differs. In every case command is the launcher (or java), and for the fat jar args is ["-jar", "<path-to>-all.jar"]. --apk / --max-source-bytes and the load_apk / unload_apk tools behave identically no matter which client drives it.
mcpServers JSON — the de-facto standard, accepted by Claude Desktop, Cursor, Windsurf, Cline, Roo Code, LM Studio, Gemini CLI, and most others:
{
"mcpServers": {
"jadx-headless": {
"command": "C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat"
}
}
}
Only the file it goes in differs — e.g. Claude Desktop claude_desktop_config.json, Cursor .cursor/mcp.json, Windsurf ~/.codeium/windsurf/mcp_config.json. Check your client's MCP docs for the exact path.
VS Code / GitHub Copilot uses servers + type (not mcpServers), in .vscode/mcp.json:
{
"servers": {
"jadx-headless": {
"type": "stdio",
"command": "C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat"
}
}
}
TOML-based CLIs (Codex, Grok, …) use an mcp_servers table:
[mcp_servers.jadx-headless]
command = "C:/tools/jadx-headless-mcp/bin/jadx-headless-mcp.bat"
args = []
--transport http)By default the server speaks stdio, so the MCP client spawns it as a local child process — client and jadx live on the same machine. To split them (client on machine B, jadx and the APK on machine A), run the server with the Streamable HTTP transport and point a remote MCP client at the URL:
# on machine A (where jadx runs and the APK lives)
./bin/jadx-headless-mcp --transport http --host 0.0.0.0 --port 8080 --allowed-host a.example.com
| Flag | Default | Meaning |
|---|---|---|
--transport <stdio\|http> |
stdio |
serve Streamable HTTP instead of stdio |
--host <addr> |
127.0.0.1 |
bind address; use 0.0.0.0 to accept connections from other machines |
--port <n> |
8080 |
listen port |
--path <p> |
/mcp |
endpoint path (serves POST/GET/DELETE) |
--allowed-host <h> |
— | extra Host header value accepted by the DNS-rebinding check (repeatable). Pass the hostname/IP clients use to reach machine A |
--no-dns-rebinding-protection |
off | disable Host/Origin validation (trusted networks / behind a reverse proxy only) |
The endpoint is http://<host>:<port><path> (default http://127.0.0.1:8080/mcp). Point any Streamable-HTTP-capable MCP client at it — e.g. Claude Code:
claude mcp add --transport http jadx-headless http://a.example.com:8080/mcp
The APK always lives on the jadx host. load_apk resolves its path with File(path) on the machine running the server (A) — a remote client only sends a path string that must be valid on A. Copy the APK to A first, then call load_apk with the A-side path. It's still one APK per process; multiple HTTP sessions share the single loaded APK.
Security:
Host headers, so a remote client reaching a.example.com is rejected with 403 unless you add --allowed-host a.example.com (or disable the check). This deliberately stops a malicious web page from driving your server.127.0.0.1 and reaching it over an SSH tunnel, or put it behind a reverse proxy / VPN that adds auth. Don't expose 0.0.0.0 on an untrusted network.Claude Code ──stdio | Streamable-HTTP JSON-RPC──> jadx-headless-mcp (JVM)
│
├── MCP Kotlin SDK (stdio / Ktor CIO HTTP server)
│
└── SessionHolder
│ (Mutex-serialized load/unload)
└── JadxSession
└── JadxDecompiler (jadx-core)
Lazy decompilation: only get_class_source and get_smali_of_class (and get_method_by_name / code search) trigger per-class decompilation. Index building at startup is metadata-only.
Important: max_bytes truncates the response after jadx finishes materializing the class — it does not stop a slow decompile early. Fat obfuscated classes (e.g. large ViewModels) previously could block the whole MCP process for tens of minutes until the client’s multi-hour tool ceiling. As of the decompile-timeout fix:
--decompile-timeout-ms (default 90000)// ERROR: ... timed out ... banner immediatelyget_class_summary + get_method_by_name for large classesThe launcher sets -XX:MaxRAMPercentage=60.0 by default. Override via JAVA_OPTS:
JAVA_OPTS="-Xmx8g" ./bin/jadx-headless-mcp
| Dependency | Mechanism |
|---|---|
io.github.skylot:jadx-core and friends |
Dependabot weekly (opens PRs grouped under "jadx"); CI verifies the build still passes |
io.modelcontextprotocol:kotlin-sdk |
Dependabot weekly (grouped under "mcp-kotlin-sdk") |
org.jetbrains.kotlinx:* |
Dependabot weekly (grouped under "kotlinx") |
| GitHub Actions versions | Dependabot monthly |
Related but not auto-synced (different architecture; manual watch only):
zinja-coder/jadx-ai-mcp — JADX GUI pluginzinja-coder/jadx-mcp-server — Python adapter for the abovemobilehackinglab/jadx-mcp-plugin — alternative GUI pluginIf those projects ship a new tool worth porting, file an issue here.
morluto/rea) integrates this project as an upstream analysis engine for its static Android APK analysis. third_party/README.md .Pinning this engine by digest in your own project? Each release's jadx-headless-mcp.sha256 asset (and the current release's .mcp.json pin) lets you verify a download the same way — see below.
Every GitHub Release attaches the portable fat jar jadx-headless-mcp-<version>-all.jar, a distribution zip, and (since v0.7.1) a jadx-headless-mcp.sha256 asset carrying that fat jar's SHA-256. The .sha256 asset is the authoritative digest for each release; the current release's digest is also pinned in the Claude Code plugin's .mcp.json.
To verify a download, compare its digest against the release's jadx-headless-mcp.sha256 asset (or the .mcp.json pin):
# macOS / Linux
sha256sum jadx-headless-mcp-<version>-all.jar
# Windows PowerShell
(Get-FileHash -Algorithm SHA256 jadx-headless-mcp-<version>-all.jar).Hash.ToLowerInvariant()
Maintainer note: the release's
jadx-headless-mcp.sha256asset and the.mcp.jsonpin carry the same digest — keep the two in sync when releasing.
skylot/jadx — the underlying decompilermodelcontextprotocol/kotlin-sdk — MCP server frameworkApache-2.0.
| 日期 | 榜单 | 排名 | 周期新增 | 总 STAR |
|---|