# Directory Structure
```
├── .gitattributes
├── .gitignore
├── fastmcp_adapter.py
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── img
│ └── jadx-mcp-running.png
├── plugin
│ ├── bin
│ │ └── main
│ │ └── META-INF
│ │ └── services
│ │ └── jadx.api.plugins.JadxPlugin
│ ├── build.gradle
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── mobilehackinglab
│ │ └── jadxplugin
│ │ ├── McpPlugin.java
│ │ └── McpPluginOptions.java
│ └── resources
│ └── META-INF
│ └── services
│ └── jadx.api.plugins.JadxPlugin
├── README.md
├── requirements.txt
└── settings.gradle
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Gradle build files
.gradle/
build/
# keep wrapper
!gradle/wrapper/gradle-wrapper.jar
# Java class files
*.class
# Python files
__pycache__/
# IDE files
.idea/
.vscode/
*.iml
# Environment files
venv/
.env
# Logs
*.log
# OS-specific files
.DS_Store
Thumbs.db
# Local cache
cache/
*.jar
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
## ⚙️ Jadx MCP Plugin — Decompiler Access for Claude via MCP
This project provides a [Jadx](https://github.com/skylot/jadx) plugin written in **Java**, which exposes the **Jadx API over HTTP** — enabling live interaction through MCP clients like [Claude](https://www.anthropic.com/index/introducing-claude) via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). A lightweight [FastMCP](https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#resources) adapter in Python acts as a bridge between Claude and the plugin. This enables intelligent navigation and automation of reverse engineering workflows, ideal for AI-assisted security analysis of Android apps.
---
### 🧰 Setup Instructions
```bash
# Clone this repository
git clone https://github.com/mobilehackinglab/jadx-mcp-plugin.git
cd jadx-mcp-plugin
# Create and activate a virtual environment
python3 -m venv venv
# Activate:
source venv/bin/activate # Linux/Mac
.\venv\Scripts\activate # Windows
```
### Install Python dependencies
```bash
pip install -r requirements.txt
```
### 🧠 Setup Claude MCP CLient Integration
To use this adapter in Claude Desktop, go to `File` -> `Settings` -> `Developer` -> `Edit Config` -> `claude_desktop_config.json` and add an MCP server pointing to the Python executable in the venv (to prevent depedency issues) and the full adapter path following below examples:
Windows:
```json
{
"mcpServers": {
"Jadx MCP Server": {
"command": "C:\\Workset\\jadx-mcp-plugin\\venv\\Scripts\\python.exe",
"args": ["C:\\Workset\\jadx-mcp-plugin\\fastmcp_adapter.py"]
}
}
}
```
MacOS / Linux:
```json
{
"mcpServers": {
"Jadx MCP Server": {
"command": "/Users/yourname/jadx-mcp-plugin/venv/bin/python",
"args": ["/Users/yourname/jadx-mcp-plugin/fastmcp_adapter.py"]
}
}
}
```
Make sure to restart (Quit) Claude after editing the config.
After restart it should look like this:

### ✅ Usage Flow
1. Open **Jadx** with the latest plugin JAR from [the releases](https://github.com/mobilehackinglab/jadx-mcp-plugin/releases) placed in its `plugins/` folder or load it via `Plugins` -> `install plugin`.
2. Load an APK or DEX file
3. (Optional) You can specify the HTTP interface address by launching Jadx with:
```bash
jadx-gui -Pjadx-mcp.http-interface=http://localhost:8085
```
This is useful if you want to change the default host/port (`http://localhost:8085`).
> **Note:** If you change the interface address here, make sure to also update the corresponding URL in `fastmcp_adapter.py` to match.
4. Claude will detect and activate the Jadx MCP Server tools.
5. You can now list classes, fetch source, inspect methods/fields, and extract code live.
---
## 🧪 Tools Provided
| Tool | Description |
|-----------------------|---------------------------------------|
| `list_all_classes` | Get all decompiled class names |
| `search_class_by_name` | Find classes matching a string |
| `get_class_source` | Get full source of a given class |
| `search_method_by_name` | Find methods matching a string |
| `get_methods_of_class` | List all method names in a class |
| `get_fields_of_class` | List all field names in a class |
| `get_method_code` | Extract decompiled code for a method |
---
## 🛠 Development
### ☕ Java Plugin
The Java plugin is located at:
```
plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPlugin.java
```
It uses the `JadxPlugin` API (`jadx.api.*`) to:
- Load decompiled classes and methods
- Serve structured data via an embedded HTTP server
- Respond to `/invoke` and `/tools` endpoints
#### 🚀 Automated Installation with Gradle Tasks
You can use the following Gradle task to build and install the plugin in one step:
```bash
./gradlew installPlugin
```
> This uses the `jadx plugins` CLI. Make sure Jadx is installed and available in your `PATH`.
For other plugin-related tasks (uninstall, enable/disable), see the task definitions in [`plugin/build.gradle`](./plugin/build.gradle).
#### 🔧 Manual Installation
To build the plugin:
```bash
./gradlew build
# Output: plugin/build/libs/jadx-mcp-plugin-<version>.jar
```
Install the plugin JAR using the `jadx plugins` CLI:
```bash
jadx plugins --install-jar path/to/jadx-mcp-plugin-<version>.jar
```
Alternatively, place the built `.jar` file into your Jadx `plugins/` folder, typically located at: `~/.jadx/plugins/`
If you place the JAR manually, you’ll also need to enable the plugin through the Jadx GUI or by running:
```bash
jadx plugins --enable jadx-mcp
```
---
### Python FastMCP Adapter
The adapter file is:
```
fastmcp_adapter.py
```
It translates Claude’s MCP tool calls into HTTP POSTs to the running Jadx plugin server. Make sure Jadx is open **before** starting Claude.
---
## 🤝 Contributing
PRs, feature requests, and tool extensions are welcome!
This project is maintained by [Mobile Hacking Lab](https://github.com/mobilehackinglab).
---
## 🧩 Credits
- [Jadx](https://github.com/skylot/jadx)
- [FastMCP](https://github.com/modelcontextprotocol/python-sdk)
- [Claude by Anthropic](https://www.anthropic.com)
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
mcp
requests
```
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
```
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
```
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
```toml
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
guava = "33.0.0-jre"
junit-jupiter = "5.10.2"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
```
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
```
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.8/userguide/multi_project_builds.html in the Gradle documentation.
*/
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
rootProject.name = 'jadx-mcp-plugin'
include('plugin')
```
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPluginOptions.java:
--------------------------------------------------------------------------------
```java
package com.mobilehackinglab.jadxplugin;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
import static com.mobilehackinglab.jadxplugin.McpPlugin.PLUGIN_ID;
public class McpPluginOptions extends BasePluginOptionsBuilder {
private String httpInterface;
@Override
public void registerOptions() {
strOption(PLUGIN_ID + ".http-interface")
.description("interface to run mcp server on")
.defaultValue("http://localhost:8085")
.flags(OptionFlag.PER_PROJECT)
.setter(v -> httpInterface = v);
}
public String getHttpInterface() {
return httpInterface;
}
}
```
--------------------------------------------------------------------------------
/plugin/build.gradle:
--------------------------------------------------------------------------------
```
plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'java'
}
group = 'jadx.plugins'
version = '1.3'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.json:json:20231013'
compileOnly("io.github.skylot:jadx-core:1.5.2")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17) // Also supported older Java versions
}
}
jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveBaseName = 'jadx-mcp-plugin'
manifest {
attributes(
'Jadx-Plugin': 'true',
'Jadx-Plugin-Class': 'com.mobilehackinglab.jadxplugin.McpPlugin',
'Jadx-Plugin-Version': '1.2.0',
'Jadx-Plugin-Id': 'jadx-mcp-plugin',
'Jadx-Plugin-Description': 'Exposes Jadx info over HTTP',
'Jadx-Plugin-Author': 'Mobile Hacking Lab'
)
}
// This ensures all dependencies are included
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
// Exclude problematic files from dependencies
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/MANIFEST.MF'
}
shadowJar {
archiveBaseName = 'jadx-mcp-plugin'
archiveVersion = '1.3.0'
mergeServiceFiles()
}
// Installs the plugin JAR into Jadx
tasks.register("installPlugin", Exec) {
group = "plugin"
description = "Installs the jadx-mcp plugin."
dependsOn jar
doFirst {
def pluginJar = layout.buildDirectory.file("libs/jadx-mcp-plugin-${project.version}.jar").get().asFile.absolutePath
commandLine "sh", "-c", """
jadx plugins --uninstall jadx-mcp >/dev/null
jadx plugins --install-jar \"${pluginJar}\"
"""
}
}
// Uninstalls the plugin from Jadx
tasks.register("uninstallPlugin", Exec) {
group = "plugin"
description = "Uninstalls the jadx-mcp plugin."
commandLine "sh", "-c", "jadx plugins --uninstall jadx-mcp"
}
// Enables the plugin after it's installed
tasks.register("enablePlugin", Exec) {
group = "plugin"
description = "Enables the jadx-mcp plugin."
commandLine "sh", "-c", "jadx plugins --enable jadx-mcp"
}
// Disables the plugin
tasks.register("disablePlugin", Exec) {
group = "plugin"
description = "Disables the jadx-mcp plugin."
commandLine "sh", "-c", "jadx plugins --disable jadx-mcp"
}
```
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
```
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
```
--------------------------------------------------------------------------------
/fastmcp_adapter.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import FastMCP
import requests
from requests.exceptions import ConnectionError
import sys
# Create the MCP adapter with a human-readable name
mcp = FastMCP("Jadx MCP Server")
# Address of the Jadx MCP plugin's HTTP server
DEFAULT_MCP_SERVER = "http://localhost:8085"
mcp_server = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MCP_SERVER
def invoke_jadx(tool: str, parameters: dict = {}) -> dict:
"""
Internal helper to send a tool request to the Jadx MCP HTTP server.
"""
try:
resp = requests.post(f"{mcp_server}/invoke", json={"tool": tool, "parameters": parameters})
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise ValueError(data["error"])
return data.get("result", data)
except ConnectionError:
raise ConnectionError("Jadx MCP server is not running. Please start Jadx and try again.")
except Exception as e:
raise RuntimeError(f"Unexpected error: {str(e)}")
@mcp.tool()
def list_all_classes(limit: int = 250, offset: int = 0) -> dict:
"""
Returns a paginated list of class names.
Params:
- limit: Max number of classes to return (default 250)
- offset: Starting index of class list
"""
return invoke_jadx("list_all_classes", {"limit": limit, "offset": offset})
@mcp.tool()
def search_class_by_name(query: str) -> dict:
"""Search for class names that contain the given query string (case-insensitive)."""
return invoke_jadx("search_class_by_name", {"query": query})
@mcp.tool()
def get_class_source(class_name: str) -> str:
"""
Returns the full decompiled source code of a given class.
"""
return invoke_jadx("get_class_source", {"class_name": class_name})
@mcp.tool()
def search_method_by_name(method_name: str) -> str:
"""
Searches for all methods matching the provided name.
Returns class and method pairs as string.
"""
return invoke_jadx("search_method_by_name", {"method_name": method_name})
@mcp.tool()
def get_methods_of_class(class_name: str) -> list:
"""
Returns all method names declared in the specified class.
"""
return invoke_jadx("get_methods_of_class", {"class_name": class_name})
@mcp.tool()
def get_fields_of_class(class_name: str) -> list:
"""
Returns all field names declared in the specified class.
"""
return invoke_jadx("get_fields_of_class", {"class_name": class_name})
@mcp.tool()
def get_method_code(class_name: str, method_name: str) -> str:
"""
Returns only the source code block of a specific method within a class.
"""
return invoke_jadx("get_method_code", {
"class_name": class_name,
"method_name": method_name
})
@mcp.tool()
def get_android_manifest() -> str:
"""
Returns the content of AndroidManifest.xml
"""
return invoke_jadx("get_android_manifest")
@mcp.resource("jadx://tools")
def get_tools_resource() -> dict:
"""
Fetches the list of all available tools and their descriptions from the Jadx plugin.
Used for dynamic tool discovery.
"""
try:
resp = requests.get(f"{mcp_server}/tools")
resp.raise_for_status()
return resp.json()
except ConnectionError:
raise ConnectionError("Jadx MCP server is not running. Please start Jadx and try again.")
except Exception as e:
raise RuntimeError(f"Unexpected error: {str(e)}")
if __name__ == "__main__":
mcp.run(transport="stdio")
print("Adapter started", file=sys.stderr)
```
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPlugin.java:
--------------------------------------------------------------------------------
```java
package com.mobilehackinglab.jadxplugin;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginContext;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.xmlgen.ResContainer;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class McpPlugin implements JadxPlugin {
public static final String PLUGIN_ID="jadx-mcp";
private ServerSocket serverSocket;
private ExecutorService executor;
private JadxPluginContext context;
private McpPluginOptions pluginOptions;
private boolean running = false;
public McpPlugin() {
}
/**
* Called by Jadx to initialize the plugin.
*/
@Override
public void init(JadxPluginContext context) {
this.context = context;
this.pluginOptions = new McpPluginOptions();
this.context.registerOptions(this.pluginOptions);
new Thread(this::safePluginStartup).start();
}
/**
* Starts the HTTP server if Jadx is ready.
*/
private void safePluginStartup() {
if (!waitForJadxLoad()) {
System.err.println("[MCP] Jadx initialization failed. Not starting server.");
return;
}
try {
URL httpInterface = parseHttpInterface(pluginOptions.getHttpInterface());
startServer(httpInterface);
System.out.println("[MCP] Server started successfully at " + httpInterface.getProtocol() + "://" + httpInterface.getHost() + ":" + httpInterface.getPort());
} catch (IOException | IllegalArgumentException e) {
System.err.println("[MCP] Failed to start server: " + e.getMessage());
}
}
/**
* Waits for the Jadx decompiler to finish loading classes.
*/
private boolean waitForJadxLoad() {
int retries = 0;
while (retries < 30) {
if (isDecompilerValid()) {
int count = context.getDecompiler().getClassesWithInners().size();
System.out.println("[MCP] Jadx fully loaded. Classes found: " + count);
return true;
}
System.out.println("[MCP] Waiting for Jadx to finish loading classes...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
retries++;
}
System.err.println("[MCP] Jadx failed to load classes within expected time.");
return false;
}
/**
* Provides metadata for the plugin to Jadx.
*/
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo(
PLUGIN_ID,
"JADX MCP Plugin",
"Exposes Jadx info over HTTP",
"https://github.com/mobilehackinglab/jadx-mcp-plugin",
"1.0.0");
}
/**
* Cleanly shuts down the server and executor.
*/
public void destroy() {
running = false;
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
if (executor != null) {
executor.shutdown();
}
}
/**
* Starts the TCP server and accepts incoming connections.
*/
private void startServer(URL httpInterface) throws IOException {
String host = httpInterface.getHost();
int port = httpInterface.getPort();
InetAddress bindAddr = InetAddress.getByName(host);
serverSocket = new ServerSocket(port, 50, bindAddr);
executor = Executors.newFixedThreadPool(5);
running = true;
new Thread(() -> {
while (running) {
try {
Socket clientSocket = serverSocket.accept();
executor.submit(() -> handleConnection(clientSocket));
} catch (IOException e) {
if (running)
System.err.println("[MCP] Error accepting connection: " + e.getMessage());
}
}
}).start();
}
/**
* Handles incoming HTTP requests to the plugin.
*/
private void handleConnection(Socket socket) {
try (socket;
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
OutputStream outStream = socket.getOutputStream()) {
String requestLine = in.readLine();
if (requestLine == null)
return;
String method = requestLine.split(" ")[0];
String path = requestLine.split(" ")[1];
int contentLength = 0;
String header;
while (!(header = in.readLine()).isEmpty()) {
if (header.toLowerCase().startsWith("content-length:")) {
contentLength = Integer.parseInt(header.substring("content-length:".length()).trim());
}
}
char[] buffer = new char[contentLength];
int bytesRead = in.read(buffer);
String body = new String(buffer, 0, bytesRead);
JSONObject responseJson;
if ("/invoke".equals(path) && "POST".equalsIgnoreCase(method)) {
try {
String result = processInvokeRequest(body);
if (result.trim().startsWith("{")) {
responseJson = new JSONObject(result);
} else {
responseJson = new JSONObject().put("result", result);
}
} catch (Exception e) {
responseJson = new JSONObject().put("error", "Failed to process tool: " + e.getMessage());
}
} else if ("/tools".equals(path)) {
responseJson = new JSONObject(getToolsJson());
} else {
responseJson = new JSONObject().put("error", "Not found");
}
byte[] respBytes = responseJson.toString(2).getBytes(StandardCharsets.UTF_8);
PrintWriter out = new PrintWriter(outStream, true);
out.printf(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
respBytes.length);
out.flush();
outStream.write(respBytes);
outStream.flush();
} catch (Exception e) {
System.err.println("[MCP] Error handling connection: " + e.getMessage());
}
}
/**
* Return available tools for MCP server in JSON
*/
private String getToolsJson() {
return """
{
"tools": [
{ "name": "get_class_source", "description": "Returns the decompiled source of a class.", "parameters": { "class_name": "string" } },
{ "name": "search_method_by_name", "description": "Search methods by name.", "parameters": { "method_name": "string" } },
{ "name": "search_class_by_name", "description": "Search class names containing a keyword.", "parameters": { "query": "string" } },
{ "name": "get_android_manifest", "description": "Returns the content of AndroidManifest.xml if available.", "parameters": {} },
{ "name": "list_all_classes", "description": "Returns a list of all class names.", "parameters": { "offset": "int", "limit": "int" } },
{ "name": "get_methods_of_class", "description": "Returns all method names of a class.", "parameters": { "class_name": "string" } },
{ "name": "get_fields_of_class", "description": "Returns all field names of a class.", "parameters": { "class_name": "string" } },
{ "name": "get_method_code", "description": "Returns the code for a specific method.", "parameters": { "class_name": "string", "method_name": "string" } }
]
}
""";
}
/**
* Handles tool invocation from the client, routing to the correct handler.
*
* @param requestBody JSON request with tool and parameters
* @return JSON response string
* @throws JSONException if the input JSON is malformed
*/
private String processInvokeRequest(String requestBody) throws JSONException {
JSONObject requestJson = new JSONObject(requestBody);
String toolName = requestJson.getString("tool");
JSONObject params = requestJson.optJSONObject("parameters");
if (params == null)
params = new JSONObject();
return switch (toolName) {
case "get_class_source" -> handleGetClassSource(params);
case "search_method_by_name" -> handleSearchMethodByName(params);
case "search_class_by_name" -> handleSearchClassByName(params);
case "get_android_manifest" -> handleGetAndroidManifest();
case "list_all_classes" -> handleListAllClasses(params);
case "get_methods_of_class" -> handleGetMethodsOfClass(params);
case "get_fields_of_class" -> handleGetFieldsOfClass(params);
case "get_method_code" -> handleGetMethodCode(params);
default -> new JSONObject().put("error", "Unknown tool: " + toolName).toString();
};
}
/**
* Search class names based on a partial query string and return matches.
*
* @param params JSON object with key "query"
* @return JSON object with array of matched class names under "results"
*/
private String handleSearchClassByName(JSONObject params) {
String query = params.optString("query", "").toLowerCase();
JSONArray array = new JSONArray();
for (JavaClass cls : context.getDecompiler().getClassesWithInners()) {
String fullName = cls.getFullName();
if (fullName.toLowerCase().contains(query)) {
array.put(fullName);
}
}
return new JSONObject().put("results", array).toString();
}
/**
* Retrieves the content of AndroidManifest.xml
*
* This extracts the decoded XML from the ResContainer returned by loadContent().
*
* @return The manifest XML as a string or an error message.
*/
private String handleGetAndroidManifest() {
try {
for (ResourceFile resFile : context.getDecompiler().getResources()) {
if (resFile.getType() == ResourceType.MANIFEST) {
ResContainer container = resFile.loadContent();
if (container.getText() != null) {
return container.getText().getCodeStr(); // decoded manifest
} else {
return "Manifest content is empty or could not be decoded.";
}
}
}
return "AndroidManifest.xml not found.";
} catch (Exception e) {
return "Error retrieving AndroidManifest.xml: " + e.getMessage();
}
}
/**
* Retrieves the full decompiled source code of a specific Java class.
*
* @param params A JSON object containing the required parameter:
* - "class_name": The fully qualified name of the class to
* retrieve.
* @return The decompiled source code of the class, or an error message if not
* found.
*/
private String handleGetClassSource(JSONObject params) {
try {
String className = params.getString("class_name");
for (JavaClass cls : context.getDecompiler().getClasses()) {
if (cls.getFullName().equals(className)) {
return cls.getCode();
}
}
return "Class not found: " + className;
} catch (Exception e) {
return "Error fetching class: " + e.getMessage();
}
}
/**
* Searches all decompiled classes for methods whose names match or contain the
* provided string.
*
* @param params A JSON object containing the required parameter:
* - "method_name": A case-insensitive string to match method
* names.
* @return A newline-separated list of matched methods with their class names,
* or a message if no match is found.
*/
private String handleSearchMethodByName(JSONObject params) {
try {
String methodName = params.getString("method_name");
StringBuilder result = new StringBuilder();
for (JavaClass cls : context.getDecompiler().getClasses()) {
cls.decompile();
for (JavaMethod method : cls.getMethods()) {
if (method.getName().toLowerCase().contains(methodName.toLowerCase())) {
result.append(cls.getFullName()).append(" -> ").append(method.getName()).append("\n");
}
}
}
return result.length() > 0 ? result.toString() : "No methods found for: " + methodName;
} catch (Exception e) {
return "Error searching methods: " + e.getMessage();
}
}
/**
* Lists all classes with optional pagination.
*
* @param params JSON with optional offset and limit
* @return JSON response with class list and metadata
*/
private String handleListAllClasses(JSONObject params) {
int offset = params.optInt("offset", 0);
int limit = params.optInt("limit", 250);
int maxLimit = 500;
if (limit > maxLimit)
limit = maxLimit;
List<JavaClass> allClasses = context.getDecompiler().getClassesWithInners();
int total = allClasses.size();
JSONArray array = new JSONArray();
for (int i = offset; i < Math.min(offset + limit, total); i++) {
JavaClass cls = allClasses.get(i);
array.put(cls.getFullName());
}
JSONObject response = new JSONObject();
response.put("total", total);
response.put("offset", offset);
response.put("limit", limit);
response.put("classes", array);
return response.toString();
}
/**
* Retrieves a list of all method names declared in the specified Java class.
*
* @param params A JSON object containing the required parameter:
* - "class_name": The fully qualified name of the class.
* @return A formatted JSON array of method names, or an error message if the
* class is not found.
*/
private String handleGetMethodsOfClass(JSONObject params) {
try {
String className = params.getString("class_name");
for (JavaClass cls : context.getDecompiler().getClasses()) {
if (cls.getFullName().equals(className)) {
JSONArray array = new JSONArray();
for (JavaMethod method : cls.getMethods()) {
array.put(method.getName());
}
return array.toString(2);
}
}
return "Class not found: " + className;
} catch (Exception e) {
return "Error fetching methods: " + e.getMessage();
}
}
/**
* Retrieves all field names defined in the specified Java class.
*
* @param params A JSON object containing the required parameter:
* - "class_name": The fully qualified name of the class.
* @return A formatted JSON array of field names, or an error message if the
* class is not found.
*/
private String handleGetFieldsOfClass(JSONObject params) {
try {
String className = params.getString("class_name");
for (JavaClass cls : context.getDecompiler().getClasses()) {
if (cls.getFullName().equals(className)) {
JSONArray array = new JSONArray();
for (JavaField field : cls.getFields()) {
array.put(field.getName());
}
return array.toString(2);
}
}
return "Class not found: " + className;
} catch (Exception e) {
return "Error fetching fields: " + e.getMessage();
}
}
/**
* Extracts the decompiled source code of a specific method within a given
* class.
*
* @param params A JSON object containing the required parameters:
* - "class_name": The fully qualified name of the class.
* - "method_name": The name of the method to extract.
* @return A string containing the method's source code block (if found),
* or a descriptive error message if the method or class is not found.
*/
private String handleGetMethodCode(JSONObject params) {
try {
String className = params.getString("class_name");
String methodName = params.getString("method_name");
for (JavaClass cls : context.getDecompiler().getClasses()) {
if (cls.getFullName().equals(className)) {
cls.decompile();
for (JavaMethod method : cls.getMethods()) {
if (method.getName().equals(methodName)) {
String classCode = cls.getCode();
int methodIndex = classCode.indexOf(method.getName());
if (methodIndex != -1) {
int openBracket = classCode.indexOf('{', methodIndex);
if (openBracket != -1) {
int closeBracket = findMatchingBracket(classCode, openBracket);
if (closeBracket != -1) {
String methodCode = classCode.substring(openBracket, closeBracket + 1);
return methodCode;
}
}
}
return "Could not extract method code from class source.";
}
}
return "Method '" + methodName + "' not found in class '" + className + "'";
}
}
return "Class '" + className + "' not found";
} catch (Exception e) {
return "Error fetching method code: " + e.getMessage();
}
}
// Helper method to find matching closing bracket
private int findMatchingBracket(String code, int openPos) {
int depth = 1;
for (int i = openPos + 1; i < code.length(); i++) {
char c = code.charAt(i);
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
if (depth == 0) {
return i;
}
}
}
return -1; // No matching bracket found
}
/**
* Validates that the decompiler is loaded and usable.
* This is needed because: When you use "File → Open" to load a new file,
* Jadx replaces the internal decompiler instance, but your plugin still holds a
* stale reference to the old one.
*/
private boolean isDecompilerValid() {
try {
return context != null
&& context.getDecompiler() != null
&& context.getDecompiler().getRoot() != null
&& !context.getDecompiler().getClassesWithInners().isEmpty();
} catch (Exception e) {
return false;
}
}
/**
* Parses and validates the given HTTP interface string.
*
* <p>This method strictly enforces that the input must be a complete HTTP URL
* with a protocol of {@code http}, a non-empty host, and an explicit port.</p>
*
* <p>Examples of valid input: {@code http://127.0.0.1:8080}, {@code http://localhost:3000}</p>
*
* @param httpInterface A string representing the HTTP interface.
* @return A {@link URL} object representing the parsed interface.
* @throws IllegalArgumentException if the URL is malformed or missing required components.
*/
private URL parseHttpInterface(String httpInterface) throws IllegalArgumentException {
try {
URL url = new URL(httpInterface);
if (!"http".equalsIgnoreCase(url.getProtocol())) {
throw new IllegalArgumentException("Invalid protocol: " + url.getProtocol() + ". Only 'http' is supported.");
}
if (url.getHost() == null || url.getHost().isEmpty()) {
throw new IllegalArgumentException("Missing or invalid host in HTTP interface: " + httpInterface);
}
if (url.getPort() == -1) {
throw new IllegalArgumentException("Port must be explicitly specified in HTTP interface: " + httpInterface);
}
if (url.getPath() != null && !url.getPath().isEmpty() && !url.getPath().equals("/")) {
throw new IllegalArgumentException("Path is not allowed in HTTP interface: " + httpInterface);
}
if (url.getQuery() != null || url.getRef() != null || url.getUserInfo() != null) {
throw new IllegalArgumentException("HTTP interface must not contain query, fragment, or user info: " + httpInterface);
}
return url;
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Malformed HTTP interface URL: " + httpInterface, e);
}
}
}
```