JADX MCP Server for Claude Code and Codex: Headless Setup and a Worked APK Example
On this page
To let Claude Code or Codex read decompiled APK code, run JADX headless behind an MCP server and give the agent tools that take an APK path. Here that server is REA (a third-party open-source tool) with the jadx-headless-mcp 0.7.1 JAR: set REA_JADX_MCP_JAR (and optionally JAVA_HOME) in the server’s env block, restart the client, and the agent gets five Android tools. No jadx-gui window has to stay open.
Everything below was run on macOS with REA 6.7.0 from npm, Temurin JDK 17.0.18 and the 0.7.1 JAR, against fresh Claude Code and Codex configs. Use APKs you are allowed to analyze; both examples here are open source.
Headless JADX vs jadx-ai-mcp
The other well-known option, jadx-ai-mcp, is a plugin for the JADX GUI paired with a separate Python MCP server. You open the APK in jadx-gui and the agent works with what the GUI has loaded. That suits an interactive pairing session.
| REA + jadx-headless-mcp | jadx-ai-mcp | |
|---|---|---|
| GUI required | No | Yes, jadx-gui with the plugin |
| How the APK is chosen | Each tool call names the APK path | Whatever is open in the GUI |
| Output | Evidence with APK and engine JAR digests | Plugin responses |
| Other targets in the same server | Native, JavaScript, .NET, web, firmware | Android only |
The headless route gives repeatable answers that also work from CI or the plain rea CLI.
What you need
- A full JDK 17 or newer. REA compiles a small Java metadata bridge at startup and needs the
jdk.compilermodule, which a JRE lacks. - jadx-headless-mcp 0.7.1, the
-all.jarfrom the v0.7.1 release. SHA-2566e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75. - REA 6.7.0 registered with your agent (install guide).
- Linux, macOS or Windows x64. Other hosts (for example Windows arm64) get
unsupported_host.
REA installs none of these. On each JADX call it checks the host, resolves the JAR and java, runs java --list-modules, and reads the JAR’s ZIP directory for the classes the bridge needs. Any failure returns capability_unavailable and JADX never starts.
Step 1: Check the JDK
java --list-modules | grep -E '^(java\.base|jdk\.compiler)@'
You need a jdk.compiler line and java.base@17 or higher. No jdk.compiler means you are on a JRE. If several Javas are installed, pin one with JAVA_HOME; it must be an absolute path.
Step 2: Download and verify the JAR
shasum -a 256 jadx-headless-mcp-0.7.1-all.jar
# Windows PowerShell: Get-FileHash .\jadx-headless-mcp-0.7.1-all.jar -Algorithm SHA256
Use the -all JAR. REA checks that it contains the jadx-headless-mcp session classes, JADX’s JadxDecompiler, Gson and the Kotlin MCP SDK server class, so a plain JADX distribution or a thin JAR fails. Put it somewhere stable, such as ~/tools/jadx/.
Step 3: Smoke-test from the shell
Before touching agent config, confirm Java and the JAR work. With the F-Droid client APK:
export REA_JADX_MCP_JAR="$HOME/tools/jadx/jadx-headless-mcp-0.7.1-all.jar"
export JAVA_HOME=/absolute/path/to/jdk-21 # optional
rea doctor --provider jadx --json
rea inspect-android-package /absolute/path/F-Droid.apk --json
The doctor check, trimmed:
"name": "jadx", "ok": true, "classification": "healthy",
"detail": "JADX MCP bridge JAR validated at /…/jadx-headless-mcp-0.7.1-all.jar; its path is recorded for agent registrations. This checks configuration, not whether a selected APK fits in the worker's memory."
Our F-Droid download (2.0.2, SHA-256 19d51fad…7b52, fetched 2026-10-20) took 4.5 seconds and reported org.fdroid.fdroid, min SDK 24, target SDK 37, 31 permissions and 11,960 classes. If this works, any later MCP problem is agent config, not Java or the JAR.
Step 4: Add the env block to Claude Code
Run setup in a shell where REA_JADX_MCP_JAR is set and setup copies it into the registration (along with JAVA_HOME, REA_JADX_HEAP_MIB, REA_JADX_ACTIVE_PROCESSOR_COUNT and REA_JADX_OPERATION_TIMEOUT_MS if set):
export REA_JADX_MCP_JAR="$HOME/tools/jadx/jadx-headless-mcp-0.7.1-all.jar"
npx [email protected] setup --client claude_code --client codex --yes
--dry-run prints Detected provider environment: JAVA_HOME=…, REA_JADX_MCP_JAR=… before writing anything. The MCP client starts the server with the environment from its config file, not your shell, so check the file. For Claude Code it is ~/.claude.json:
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "[email protected]", "mcp"],
"env": {
"JAVA_HOME": "/absolute/path/to/jdk-17",
"REA_JADX_MCP_JAR": "/Users/you/tools/jadx/jadx-headless-mcp-0.7.1-all.jar"
}
}
}
}
If you ran setup without the variable, add the env block by hand. Then quit Claude Code completely and start a new session; a running client keeps the old server process and environment. Check /mcp shows rea connected. Rerunning setup keeps your existing env values.
Step 5: Add the env block to Codex
Codex reads ~/.codex/config.toml (or $CODEX_HOME/config.toml). Setup wrote:
[mcp_servers.rea]
command = "npx"
args = [ "-y", "[email protected]", "mcp" ]
startup_timeout_sec = 30
tool_timeout_sec = 450
[mcp_servers.rea.env]
JAVA_HOME = "/absolute/path/to/jdk-17"
REA_JADX_MCP_JAR = "/home/you/tools/jadx/jadx-headless-mcp-0.7.1-all.jar"
On Windows, write paths in a TOML literal string, for example 'D:\tools\jadx-headless-mcp-0.7.1-all.jar', and leave whatever command setup wrote. Start a new Codex session after editing.
Step 6: Optional limits for big APKs
| Variable | Effect | Example |
|---|---|---|
REA_JADX_HEAP_MIB |
-Xmx<value>m |
8192 |
REA_JADX_ACTIVE_PROCESSOR_COUNT |
-XX:ActiveProcessorCount=<value> |
4 |
REA_JADX_OPERATION_TIMEOUT_MS |
per-operation deadline (default 120 s) | 300000 |
All three take plain integers. 8g for the heap and 5m for the timeout were both rejected. A bigger heap is usually a better fix than retrying.
The five tools and how to prompt
| MCP tool | CLI command | Purpose |
|---|---|---|
inspect_android_package |
rea inspect-android-package |
Manifest, SDKs, permissions, counts |
search_android_classes |
rea search-android-classes |
Case-sensitive class-name substring search |
inspect_android_class |
rea inspect-android-class |
Fields, methods, overload indices |
inspect_android_method |
rea inspect-android-method |
Decompile one method (smali fallback) |
trace_android_references |
rea trace-android-references |
Incoming static references |
Every call takes the absolute APK path, so there is no “open” step. State the goal and the evidence you want, not the tool order:
Using REA's Android tools, inspect /absolute/path/F-Droid.apk.
1. Report the package name, minSdk/targetSdk and the declared permissions.
2. Find the class registered as android:name on <application> in the manifest,
list its methods, and decompile onCreate.
3. Tell me which statements come from decompiled code and which are your
inference. List anything you could not determine.
Starting from the manifest gives the agent real class names. Release builds are often shrunk and renamed, so a name search alone can miss things. Asking for unknowns makes the agent separate observed facts from inference.
Worked example: how Aegis computes its six-digit code
Aegis Authenticator v3.4.3 (GPL-3.0) from its GitHub release. Pin the digest first, since every result embeds it:
shasum -a 256 aegis-v3.4.3.apk
# fa091c91912c0d56ec3553394fac4158f9a9d178e93bcbb35419601d788cb528 (6,881,897 bytes)
The F-Droid build is signed by F-Droid, so its bytes and digest differ. These are the same calls the agent makes over MCP, run from the CLI.
Package map. rea inspect-android-package /absolute/path/aegis-v3.4.3.apk --json finished in 3.8 seconds. Trimmed from normalized_result:
"package_name": "com.beemdevelopment.aegis",
"version_name": "3.4.3",
"version_code": "82",
"min_sdk": "23",
"target_sdk": "36",
"class_count": 3077,
"resource_count": 1106,
"signature_verification": "not_performed"
version_code and the SDK levels are strings. The signature is not verified and the app is never run.
Over MCP, pass detail: "summary" on big APKs: for Aegis it cut the response from 26,991 to 4,634 characters by dropping the decoded manifest text. To read the manifest later, pass the summary’s parent_evidence_id to inspect_analysis_view with {"source": {"kind": "retained-evidence", "evidence_id": "<parent_evidence_id>"}, "view": {"kind": "android", "facet": "manifest", "offset": 0, "limit": 4000}} and page through (offsets are UTF-16 code units; the Aegis manifest is 10,988). The ID only works in the session that produced it; reusing one from an earlier session returned evidence_integrity_mismatch.
Search class names.
rea search-android-classes /absolute/path/aegis-v3.4.3.apk Totp
Three matches: otp.TotpInfo, ui.views.TotpProgressBar and importers.TotpAuthenticatorImporter (package prefix shortened). Only the first looks like a calculation.
List members, then read one overload.
rea inspect-android-class /absolute/path/aegis-v3.4.3.apk com.beemdevelopment.aegis.otp.TotpInfo
rea inspect-android-method /absolute/path/aegis-v3.4.3.apk \
com.beemdevelopment.aegis.otp.TotpInfo getOtp --overload-index 1
getOtp() is overload 0 and getOtp(long) is overload 1. Without an index REA refuses instead of guessing. Indices belong to this APK and engine; don’t reuse them across builds.
Find callers.
rea trace-android-references /absolute/path/aegis-v3.4.3.apk \
com.beemdevelopment.aegis.otp.OtpInfo --method-name getOtp
Two callers: ui.views.EntryHolder.getOtp (displays the code) and ui.MainActivity.copyEntryCode (copies it). Each reference carries a direct_invokes list with the opcode (such as INVOKE_VIRTUAL), DEX method index and code-unit offset. OtpInfo.getOtp has one overload; tracing TotpInfo.getOtp without --overload-index is refused with Select one of 2 overloads: 0: getOtp(): java.lang.String; 1: getOtp(long): java.lang.String.
How to read it.
- Decompiled Java is a derived view; a complete text result is not proof of complete semantics. Static initializers (
<clinit>) come back as smali. - References are static incoming relationships, not observed runtime calls. Reflection and dynamic loading stay unknown.
- Names lie in shrunk builds: in Aegis the OTP generator surfaced under
kotlin.ExceptionsKt, so follow calls from a known entry point.
For resources, devices and Flutter in the same toolset, see Android APK analysis with REA.
When it doesn’t connect
These are the failures we hit ourselves:
REA_JADX_MCP_JARnot seen. It is missing from the server’senvblock, or relative. Setting it in your shell does not reach a server started by the client.JAVA_HOMErelative or using~. WithJAVA_HOME=jdk17the 6.7.0 server printedREA configuration is invalid: JAVA_HOME must be absolute.and never answeredinitialize, so/mcpshowsreaas failed. Use a literal absolute path.- JRE instead of JDK. The bridge fails to compile; point
JAVA_HOMEat a full JDK. - Wrong JAR. The non-
allartifact or a JADX release fails the class check. - Heap value with a unit.
REA_JADX_HEAP_MIB must be a positive safe integer; received 8g. - Timeouts on big APKs. Raise the heap, narrow the request, and avoid an empty class search on very large apps.
- An
.aabfile. App Bundles are refused; use a standalone APK (unpacking APK and AAB files).
We did not test Windows. On Windows x64 without PATH access to Java, set JAVA_HOME in the env block; WSL with a Linux JDK is an alternative.