jadx-mcp-server
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 29 GitHub stars
Code Pass
- Code scan — Scanned 9 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
A Pure-Java MCP Server for JaDX Android Reverse Engineering Tool
JADX MCP Server
A Model Context Protocol (MCP) server that provides Android APK reverse engineering capabilities using JADX (Java Android Decompiler). This server enables AI assistants like Claude to analyze APK files, decompile code, extract components, and perform security assessments on Android applications.
Two processes
Claude ──stdio──► mcp_server/jadx_mcp.py ──HTTP──► JVM daemon (JADX)
the MCP protocol :8765 decompilation only
The MCP layer is Python; the JVM is a long-lived local HTTP service that owns
decompilation and nothing else. mcp_server/daemon.py starts and supervises it,
so a client only ever launches the Python script.
Why not a Java MCP server. It was one, until concurrent tool calls proved
unsurvivable. The official MCP Java SDK emits each response onto a Reactor sink
built with Sinks.many().unicast().onBackpressureBuffer(), which is not safe for
concurrent emission: when several tool calls finish at the same instanttryEmitNext returns FAIL_NON_SERIALIZED, the SDK raisesFailed to enqueue message, nothing handles it, and the transport dies. The JVM
then stays alive with no reader thread, so the client never sees EOF and every
request hangs forever. That is java-sdk#686,
still open, and still present in the SDK's 2.0.0 source. Over HTTP, concurrency
is just concurrent requests — there is no shared sink to race on.
Two things fall out of the split:
- The APK stays loaded between sessions. The JVM is a daemon, not a
per-session subprocess, so re-analysing the same APK is free rather than a
fresh parse every run. - Failure is loud. If the JVM dies, calls fail immediately and the next
start respawns it, instead of blocking indefinitely.
Overview
This project implements an MCP server that wraps the powerful JADX decompiler, making Android APK analysis accessible through standardized MCP tools. It's designed for security researchers, developers, and analysts who need to reverse engineer Android applications programmatically.
Key Features
- APK Loading & Analysis: Load and analyze APK files for detailed inspection
- Code Decompilation: Get decompiled Java source code from DEX bytecode
- Class & Method Inspection: Browse classes, methods, and fields
- Component Extraction: Extract exported components from AndroidManifest.xml
- Method Search: Search for specific methods across all classes
- Cross-References & Hierarchy: Callers, callees, subtypes and overrides read
from the dex, so dispatch is resolvable to the concrete handler - Sink Sweep: Find every framework sink call site across the whole APK with no
decompilation, keyed by impact category - Manifest Analysis: Parse and analyze AndroidManifest.xml content
- Main Activity Detection: Identify the application's entry point
- Resource File Analysis: Access and analyze APK resource files (layouts, strings, etc.)
- Bytecode Analysis: Extract smali (bytecode) representations for low-level analysis
- MCP Integration: Seamless integration with AI assistants through MCP protocol
Architecture
The project follows a layered architecture:
- MCP Layer (
mcp_server/jadx_mcp.py): Exposes the analysis API as MCP tools - Supervisor (
mcp_server/daemon.py): Starts and health-checks the JVM - HTTP Layer (
JadxRestController): The daemon's API, localhost-only - API Layer (
JadxApkAnalyzerAPI): Clean API interface for APK analysis - Registry (
AnalyzerRegistry): Keeps loaded APKs warm across sessions - Core Layer (
JadxAnalyzerCore): Core JADX integration and analysis logic - CLI Layer (
JadxApkAnalyzerCLI): Interactive command-line interface - Model Layer: Data structures for components
Prerequisites
- Java 17 or higher (built and run on 21)
- Maven 3.6+ for building
- Python 3.10+ for the MCP layer — see
mcp_server/requirements.txt - JADX dependencies (automatically handled by Maven)
Installation & Setup
1. Clone the Repository
git clone <repository-url>
cd jadx-mcp-server
2. Build the JVM daemon
Use the provided build script — it builds the jar, runs both test suites, and
prints the MCP client config below filled in with your actual paths:
./build.sh # build + test
./build.sh --skip-tests # build only
Or build manually with Maven:
mvn clean package
Either way the build produces target/jadx-mcp-server-1.0.0.jar. That filename
is what mcp_server/daemon.py looks for, so it is not free to change.
3. Install the Python MCP layer
python -m venv .venv
.venv/bin/pip install -r mcp_server/requirements.txt
Any interpreter with those packages works — the launch command below just has to
point at it.
4. Configure your MCP client
{
"mcpServers": {
"jadx-analyzer": {
"command": "/path/to/jadx-mcp-server/.venv/bin/python",
"args": ["/path/to/jadx-mcp-server/mcp_server/jadx_mcp.py"]
}
}
}
Replace /path/to/jadx-mcp-server with the actual path to your project
directory. Nothing launches the JVM directly — jadx_mcp.py starts it on first
use and reuses it afterwards.
Environment variables, all optional:
| Variable | Default | Purpose |
|---|---|---|
JADX_URL |
http://127.0.0.1:8765 |
Where the daemon listens. The supervisor spawns the JVM on this port |
JADX_PORT |
8765 |
Port the JVM binds (127.0.0.1 only) |
JADX_HEAP |
24g |
JVM max heap |
JADX_TIMEOUT |
600 |
Per-request timeout, seconds |
JADX_ALLOW_STALE |
unset | Set to 1 to keep a running daemon even when the jar on disk is newer |
JADX_LOG_FILE |
per port, see below | The daemon's application log |
JADX_DAEMON_LOG |
per port, see below | Capture of the spawned JVM's stdout/stderr |
jadx.maxLoadedApks |
1 |
APKs kept warm; a large one costs ~26 GB |
One daemon per hunt means one log per daemon
Run one daemon per concurrent hunt — the registry holds one APK warm by default,
and two hunts sharing a daemon will evict each other's APK on every call. Give
each its own port, and its files follow:
| Port | Application log | stdout capture | Start lock |
|---|---|---|---|
8765 (default) |
/tmp/jadx-mcp-server.log |
/tmp/jadx-daemon.log |
/tmp/jadx-daemon.lock |
8791 |
/tmp/jadx-mcp-server-8791.log |
/tmp/jadx-daemon-8791.log |
/tmp/jadx-daemon-8791.lock |
The default port keeps the paths it always had, so anything already grepping/tmp/jadx-mcp-server.log is unaffected. The cache-thrash and oversize warnings
are only discoverable in the application log, and before this they interleaved
from every daemon on the machine into one file.
JADX_PORT=8791 JADX_URL=http://127.0.0.1:8791 ./run-api.sh # prints its own log path
Running the tests
./test.sh # both suites at once
./test.sh java # Maven / JUnit 5 only
./test.sh python # pytest only
Or by hand:
mvn -B test # Java
.venv/bin/pip install -r mcp_server/requirements-dev.txt
.venv/bin/pytest mcp_server/tests/ -q # Python
Neither suite needs a running daemon. The Java tests drive JadxAnalyzerCore
directly; the Python tests mock the HTTP transport, so they do not even need the
jar to be built.
Skips are expected — a green run is not a run with zero skips.
- The committed fixture
misc/DivaApplication.apk(1.5 MB) is what the Java
tests analyse. A few tests additionally want the 141k-class didi/taxi APK for
perf and scale claims; that one is not in the repo, and those tests skip
themselves with a JUnitAssumptionwhen it is missing. With it hidden:
92 run, 4 skipped,BUILD SUCCESS. mcp_server/tests/test_concurrency.pyreplays the failure that motivated the
two-process split — many simultaneous searches and decompilations. It skips
itself unless a daemon is already running with an APK loaded.
Available MCP Tools
Once running, the server provides these MCP tools:
Core Analysis Tools
load_apk- Load and analyze an APK fileget_all_classes- Class names, inner classes included. Capped at 1000
by default — the whole APK is 141,461 names and 7.7 MB on a large one — so
prefersearch_classes_by_keywordwhen you know part of the name. OptionalpackagePrefix(case-insensitive prefix of the full name — 254 names / 14 KB
forcom.didi.onehybrid) andlimitnarrow it further;limit: 0is the
escape hatch back to the whole list.get_class_source- Decompiled source of a class, or its outline if the
class is large. Budgeted at 8,192 bytes by default, and past that budget
this tool does not cut — it returns what the class declares (its fields and
method signatures, led by an@outline:line) so the caller can pick a member
and fetch just that body. About a third of real calls land there. Cutting was
the old behaviour and it is worse: the first 8 KB of a class is its imports and
two of its forty methods, with nothing saying whether the one you wanted is
among them. The budget matters because this tool dominates what a long-running
caller pays — 59% of every byte one 1,076-worker APK audit pulled.maxBytes: 0
(or any negative) is the escape hatch back to the whole file, for when you need
to see how several methods relate through fields and static state; an uncapped
answer over 1 MB leads with@oversized:instead.get_methods_of_class- A class's methods, as fully-qualified
deobfuscated names — the same vocabulary every other tool here speaks. Capped
at 1000 with alimit: a class is usually a narrow scope, but a generated one
is not (kotlin.collections.ArraysKt___ArraysKtdeclares 1,668).get_fields_of_class- Get list of fields in a specific class. Capped at
1000 with alimit— the widest per-class list there is, because generated
resource tables are classes (com.example.R.iddeclares 15,260 fields and
411 KB of them on one production APK).
Method Analysis Tools
Every tool that takes a methodName takes the same names: fully-qualified
(com.x.Y.m30110h) or bare (m30110h), deobfuscated or the raw dex name (h)
that get_smali_of_class prints. So a name any tool returns can be fed back into
any tool that accepts one, which is what discovering a method and then asking
about it requires.
get_method_by_name- Source of a method.<init>and<clinit>are
accepted — they are the names the sink sweep and the xref tools hand out, 161
of the 4,125 call sites in a default sweep of one production APK.<init>
returns every constructor the class declares, and an overloaded name every
overload, separated by a blank line; annotations and javadoc above a
declaration come with it. A method jadx prints no body for (an implicit default
constructor, a<clinit>folded into field initialisers) comes back as a
one-line//note saying so rather than an error.search_method_by_name- Search for methods across all classessearch_classes_by_keyword- Find classes whose name contains a keyword (case-insensitive)
Cross-Reference and Hierarchy Tools
These are what make interface dispatch resolvable — following a router to the
concrete class that actually handles a URI.
get_xrefs_to_class- Every class and method referencing a classget_xrefs_to_method- Every caller of a method, with the enclosing method namedget_xrefs_to_field- Every method reading or writing a fieldAll three are capped at 1000 entries by default, with a
limitto raise or
disable it. Reference counts are heavy-tailed and the heavy end is what a trace
walks into:kotlin.jvm.internal.Intrinsicshas 53,181 references (10.0 MB) on
one production APK,Intrinsics.checkNotNullParameter38,291 callers (7.5 MB).
Truncation arrives as a trailing one-key{"@truncated": "…"}entry naming the
real total, since a list of objects cannot carry a bare string.All three read a reverse index built from the dex, not jadx's usage graph,
which four decompiler passes rewrite as classes are decompiled — so an empty
caller list is a fact about the APK rather than about what some earlier request
happened to touch. The index is built once per loaded APK by whichever call
needs it first (3.1 s and 88 MB on a 141,461-class APK) and reported underreverseIndexon/health. See
§3b of the 1.5.6 assessment.get_type_hierarchy- Supertypes, interfaces, direct subclasses and nesting.
Resolves framework types not present in the APK, and reportsinApkso a caller
can tell "not declared here" from "no supertypes". All four lists are capped at
1000 and this tool takes nolimit; onlysubClasseshas ever reached it
(java.lang.Objectis 100,019 direct subclasses and 5.6 MB).get_overrides- Methods overriding a method, plus the base methods it
overrides. Accepts a declaring class that is not in the APK, which is the
useful direction:android.webkit.WebViewClient+shouldOverrideUrlLoading
returns every concrete implementation in the app.overriddenByandbaseMethodsare capped at 1000 with alimit, and the cap bites on this
tool's own use:java.lang.Runnable+runis 7,188 implementations and
470 KB.get_callees- The forward direction: what a method calls.calleesare
hops into classes this APK declares;externalare calls that leave it, which
is the method's sink list already filtered;callsSelfreports direct
recursion.FusionWebViewClient.openThirdPartyAppWithIntentSchemereturnsIntent.parseUriandContext.startActivityinexternalwithout decompiling
a line.limitcaps each list at 1000, though it has never bitten: this
dedupes call targets, so the widest of one APK's 872,119 methods is 541. Read from the method's dex instructions, so the answer does not depend
on what the daemon has already decompiled — see
§3a of the 1.5.6 assessment
for why that is not automatic.
Sink Analysis Tools
find_sink_call_sites- Every place the APK calls a framework sink, across
the whole APK, without decompiling anything (~150 ms on a 141k-class APK).
Called with no arguments it sweeps the hunter's sink catalogue and keys the
result byimpact_categorytag (webview_loadurl,js_bridge_call,intent_launch,file_read,exec,class_load, … 20 tags). You can also
name your own sinks:"package.Class.method"matches that method on the class
or any subtype (soandroid.webkit.WebView.loadUrlalso finds an in-APK
WebView subclass that inherits it), a bare"method"matches any receiver, and
a catalogue tag expands to its calls. Each call site gives the calling class,
method and signature — feed it toget_class_sourceorget_calleesto see the
arguments.@resolved_in_apknames the app's own types a sink was reached
through, which is a lead in itself.limitcaps each sink's list (default 100,
per sink across the catalogue;0uncapped). Blind to reflection and to
wrappers that rename the operation — follow those withget_callees.
Component Analysis Tools
get_exported_components- Every component whoseandroid:exported
resolves to true. Same record shape asget_deeplink_components; the two
differ only in which components they keep. Capped at 1000 with nolimit; a
real APK declares tens.get_deeplink_components- Components a link or a co-located app can reach,
withscheme/host/pathresolved from resource references to literals.
Every entry has the same keys, and everyintent_filters[].data[]entry
carries all nine attributes, empty where the element declares noneget_android_manifest- Get the AndroidManifest.xml contentget_main_activity_class- Get the main launcher activity class
Resource Analysis Tools
get_all_resource_file_names- Resource file names, including assets and
the manifest. OptionalpathPrefix(case-insensitive prefix of the whole
archive path) is the one to reach for:res/xmlis 24 names and 814 bytes
against 16,559 and 897 KB for the lot, and it holdsnetwork_security_config,
theFileProvider<paths>and the App Links declaration. Capped at 1000 with
alimit.get_resource_file- Get content of a specific resource file (XML layouts, strings, etc.)
Bytecode Analysis Tools
get_smali_of_class- Get smali (bytecode) representation of a specific classget_smali_of_method- Get smali (bytecode) representation of a specific method
Demo
https://github.com/user-attachments/assets/9c93c16a-5f42-4d57-a4a2-87975735bd91
JADX MCP Server in action analyzing Android APKs with Claude
The video streams from GitHub's asset CDN at the link above; it is not a file in
this repository. It used to be misc/poc.mp4, 83 MB of an 79 MB pack, which
every clone paid for. Removed from the working tree — GitHub already hosts the
copy this section renders.
Usage Examples
Basic APK Analysis with Claude
Load an APK:
Please analyze this APK file: /path/to/app.apkNote: A test APK (DIVA) is available in the
misc/directory for testing purposes.Examine Classes:
Show me all the classes in the loaded APKGet Source Code:
Get the source code for the MainActivity classSecurity Analysis:
Check for exported components and potential security issuesResource Analysis:
Show me all resource files in the APK and get the content of strings.xmlBytecode Analysis:
Get the smali bytecode for the MainActivity class to analyze low-level implementation
Driving the daemon by hand
You do not normally start the JVM yourself — jadx_mcp.py does it. To watch its
log on your terminal, or to poke the HTTP API with curl without an MCP client in
the way:
./run-api.sh
It binds 127.0.0.1:8765 (JADX_PORT to change it) and prints example requests.
Loading an APK is a POST with a JSON body; everything else is a GET taking
query parameters, never path segments, because class names contain dots and
resource paths contain slashes:
curl -XPOST http://127.0.0.1:8765/api/jadx/load-apk -H 'Content-Type: application/json' \
-d '{"apkPath":"misc/DivaApplication.apk"}'
curl "http://127.0.0.1:8765/api/jadx/class-source?className=jakhar.aseem.diva.MainActivity"
Project Structure
jadx-mcp-server/
├── mcp_server/ # the MCP server (Python)
│ ├── jadx_mcp.py # the 23 MCP tools, over HTTP
│ ├── daemon.py # spawns and health-checks the JVM
│ ├── requirements.txt # runtime deps
│ ├── requirements-dev.txt # + pytest
│ └── tests/
│ ├── test_tools.py # tool/parameter contract, envelopes
│ ├── test_daemon.py # supervision
│ └── test_concurrency.py # regression for the transport failure
├── src/main/java/com/example/jadxmcpserver/
│ ├── JadxMcpServerApplication.java # Spring Boot entry point (HTTP daemon)
│ ├── JadxRestController.java # the daemon's API, localhost-only
│ ├── JadxApkAnalyzerAPI.java # Clean API interface
│ ├── JadxApkAnalyzer.java # Legacy compatibility wrapper
│ ├── cli/
│ │ └── JadxApkAnalyzerCLI.java # Interactive CLI interface
│ ├── core/
│ │ ├── JadxAnalyzerCore.java # Core JADX integration
│ │ └── AnalyzerRegistry.java # Keeps loaded APKs warm (LRU)
│ └── model/
│ └── ExportedComponent.java # Component data structure
├── src/main/resources/
│ └── application.properties # Spring configuration
├── src/test/ # Java test suite
├── misc/
│ └── DivaApplication.apk # committed test fixture (1.5 MB)
├── build.sh # build the jar, print the client config
├── run-api.sh # start the daemon in the foreground
├── test.sh # run both suites
└── pom.xml # Maven configuration
Configuration
Spring Boot Configuration
The JVM is a plain local HTTP service. Key settings in application.properties:
- Runs as a servlet application on
${JADX_PORT:8765} - Binds
127.0.0.1only — these endpoints read arbitrary classes and resources
out of a loaded APK and must not be reachable from the network - Logs to
${jadx.log.file}—/tmp/jadx-mcp-server.logon the default port,/tmp/jadx-mcp-server-<port>.logon any other.DaemonLogFileresolves it
from--server.port/-Dserver.port/JADX_PORTinmain(), before Spring
opens the first appender - Console logging is off (
logging.pattern.console=is blank) — the daemon is
detached and nothing reads its stdout
JADX Configuration
JADX is configured with:
- Java 11 compatibility
- Full decompilation including resources
- Error handling for corrupted APKs
Development
Adding New Tools
A tool spans both processes, so add it bottom-up:
- Add core logic to
JadxAnalyzerCore - Expose it through
JadxApkAnalyzerAPI - Add an endpoint to
JadxRestController— query parameters, never path
variables: class names and resource paths contain dots and slashes - Add a method to
JadxClientand a@mcp.tool()inmcp_server/jadx_mcp.py - Update both test suites
Tool results are returned as a single JSON string (_json(...)) withstructured_output=False. That is the wire format clients were built against;
letting FastMCP infer it from the return annotation splits a list into one
content block per element and duplicates the payload in structuredContent.
Testing
./test.sh
Both suites. See Running the tests for what
skips and why. ./test.sh java and ./test.sh python narrow it to one side.
Security Considerations
This tool is designed for defensive security analysis only:
- ✅ Vulnerability assessment
- ✅ Security research
- ✅ Code review and analysis
- ✅ Malware analysis (defensive)
- ✅ Bytecode analysis for security auditing
- ✅ Resource file inspection for hardcoded secrets
- ❌ Creating malicious modifications
- ❌ Bypassing security controls
- ❌ Unauthorized application modification
Troubleshooting
Common Issues
- Java Version: Ensure Java 17+ is installed and
JAVA_HOMEis set (built and tested on 21) - Memory Issues: Large APKs may require additional JVM memory:
-Xmx4g - Path Issues: Use absolute paths in Claude Desktop configuration
- Permission Issues: Ensure the APK file is readable
Debug Logging
Enable debug logging by modifying application.properties:
logging.level.root=DEBUG
logging.level.com.example.jadxmcpserver=TRACE
The daemon's own stdout/stderr go to /tmp/jadx-daemon.log when the Python
supervisor starts it; its application log is /tmp/jadx-mcp-server.log. On any
port other than 8765 both names gain a -<port> — see the table above. The two
catch different failures: a JVM that dies on -Xmx or a bad jar only ever
reaches the stdout capture, while "Port 8791 was already in use" is logged by
Spring and, with the console pattern blank, appears nowhere else. ensure_daemon
quotes both when a daemon fails to come up.
License
This project uses JADX library which is licensed under Apache License 2.0. Please refer to JADX documentation for usage terms and conditions.
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
Support
For issues and questions:
- Check the troubleshooting section
- Review JADX documentation
- Submit an issue with detailed information including Java version, APK details, and error messages
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found