#
tokens: 11812/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | #
 2 | # https://help.github.com/articles/dealing-with-line-endings/
 3 | #
 4 | # Linux start script should use lf
 5 | /gradlew        text eol=lf
 6 | 
 7 | # These are Windows script files and should use crlf
 8 | *.bat           text eol=crlf
 9 | 
10 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Gradle build files
 2 | .gradle/
 3 | build/
 4 | # keep wrapper
 5 | !gradle/wrapper/gradle-wrapper.jar
 6 | 
 7 | # Java class files
 8 | *.class
 9 | 
10 | # Python files
11 | __pycache__/
12 | 
13 | # IDE files
14 | .idea/
15 | .vscode/
16 | *.iml
17 | 
18 | # Environment files
19 | venv/
20 | .env
21 | 
22 | # Logs
23 | *.log
24 | 
25 | # OS-specific files
26 | .DS_Store
27 | Thumbs.db
28 | 
29 | # Local cache
30 | cache/
31 | *.jar
32 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## ⚙️ Jadx MCP Plugin — Decompiler Access for Claude via MCP
  2 | 
  3 | 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.
  4 | 
  5 | ---
  6 | 
  7 | ### 🧰 Setup Instructions
  8 | 
  9 | ```bash
 10 | # Clone this repository
 11 | git clone https://github.com/mobilehackinglab/jadx-mcp-plugin.git
 12 | cd jadx-mcp-plugin
 13 | 
 14 | # Create and activate a virtual environment
 15 | python3 -m venv venv
 16 | 
 17 | # Activate:
 18 | source venv/bin/activate      # Linux/Mac
 19 | .\venv\Scripts\activate       # Windows
 20 | ```
 21 | 
 22 | ### Install Python dependencies
 23 | ```bash
 24 | pip install -r requirements.txt 
 25 | ```
 26 | 
 27 | ### 🧠 Setup Claude MCP CLient Integration
 28 | 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:
 29 | 
 30 | Windows:
 31 | 
 32 | ```json
 33 | {
 34 |   "mcpServers": {
 35 |     "Jadx MCP Server": {
 36 |       "command": "C:\\Workset\\jadx-mcp-plugin\\venv\\Scripts\\python.exe",
 37 |       "args": ["C:\\Workset\\jadx-mcp-plugin\\fastmcp_adapter.py"]
 38 |     }
 39 |   }
 40 | }
 41 | ```
 42 | 
 43 | MacOS / Linux:
 44 | ```json
 45 | {
 46 |   "mcpServers": {
 47 |     "Jadx MCP Server": {
 48 |       "command": "/Users/yourname/jadx-mcp-plugin/venv/bin/python",
 49 |       "args": ["/Users/yourname/jadx-mcp-plugin/fastmcp_adapter.py"]
 50 |     }
 51 |   }
 52 | }
 53 | ```
 54 | 
 55 | Make sure to restart (Quit) Claude after editing the config.
 56 | After restart it should look like this:
 57 | ![](img/jadx-mcp-running.png)
 58 | 
 59 | ### ✅ Usage Flow
 60 | 
 61 | 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`.
 62 | 2. Load an APK or DEX file
 63 | 3. (Optional) You can specify the HTTP interface address by launching Jadx with:
 64 | 
 65 |     ```bash
 66 |     jadx-gui -Pjadx-mcp.http-interface=http://localhost:8085
 67 |     ```
 68 | 
 69 |    This is useful if you want to change the default host/port (`http://localhost:8085`).
 70 | 
 71 |    > **Note:** If you change the interface address here, make sure to also update the corresponding URL in `fastmcp_adapter.py` to match.
 72 | 
 73 | 4. Claude will detect and activate the Jadx MCP Server tools.
 74 | 5. You can now list classes, fetch source, inspect methods/fields, and extract code live.
 75 | 
 76 | ---
 77 | 
 78 | ## 🧪 Tools Provided
 79 | 
 80 | | Tool                  | Description                           |
 81 | |-----------------------|---------------------------------------|
 82 | | `list_all_classes`    | Get all decompiled class names        |
 83 | | `search_class_by_name` | Find classes matching a string       |
 84 | | `get_class_source`    | Get full source of a given class      |
 85 | | `search_method_by_name` | Find methods matching a string      |
 86 | | `get_methods_of_class` | List all method names in a class     |
 87 | | `get_fields_of_class`  | List all field names in a class      |
 88 | | `get_method_code`     | Extract decompiled code for a method  |
 89 | 
 90 | ---
 91 | 
 92 | ## 🛠 Development
 93 | 
 94 | ### ☕ Java Plugin
 95 | 
 96 | The Java plugin is located at:
 97 | 
 98 | ```
 99 | plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPlugin.java
100 | ```
101 | 
102 | It uses the `JadxPlugin` API (`jadx.api.*`) to:
103 | - Load decompiled classes and methods
104 | - Serve structured data via an embedded HTTP server
105 | - Respond to `/invoke` and `/tools` endpoints
106 | 
107 | #### 🚀 Automated Installation with Gradle Tasks
108 | 
109 | You can use the following Gradle task to build and install the plugin in one step:
110 | 
111 | ```bash
112 | ./gradlew installPlugin
113 | ```
114 | 
115 | > This uses the `jadx plugins` CLI. Make sure Jadx is installed and available in your `PATH`.
116 | 
117 | For other plugin-related tasks (uninstall, enable/disable), see the task definitions in [`plugin/build.gradle`](./plugin/build.gradle).
118 | 
119 | #### 🔧 Manual Installation
120 | 
121 | To build the plugin:
122 | 
123 | ```bash
124 | ./gradlew build
125 | # Output: plugin/build/libs/jadx-mcp-plugin-<version>.jar
126 | ```
127 | 
128 | Install the plugin JAR using the `jadx plugins` CLI:
129 | 
130 | ```bash
131 | jadx plugins --install-jar path/to/jadx-mcp-plugin-<version>.jar
132 | ```
133 | 
134 | Alternatively, place the built `.jar` file into your Jadx `plugins/` folder, typically located at: `~/.jadx/plugins/`
135 | 
136 | If you place the JAR manually, you’ll also need to enable the plugin through the Jadx GUI or by running:
137 | 
138 | ```bash
139 | jadx plugins --enable jadx-mcp
140 | ```
141 | 
142 | ---
143 | 
144 | ### Python FastMCP Adapter
145 | 
146 | The adapter file is:
147 | 
148 | ```
149 | fastmcp_adapter.py
150 | ```
151 | 
152 | It translates Claude’s MCP tool calls into HTTP POSTs to the running Jadx plugin server. Make sure Jadx is open **before** starting Claude.
153 | 
154 | ---
155 | 
156 | ## 🤝 Contributing
157 | 
158 | PRs, feature requests, and tool extensions are welcome!  
159 | This project is maintained by [Mobile Hacking Lab](https://github.com/mobilehackinglab).
160 | 
161 | ---
162 | 
163 | ## 🧩 Credits
164 | 
165 | - [Jadx](https://github.com/skylot/jadx)
166 | - [FastMCP](https://github.com/modelcontextprotocol/python-sdk)
167 | - [Claude by Anthropic](https://www.anthropic.com)
168 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | mcp
2 | requests
```

--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------

```
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 | 
```

--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------

```toml
 1 | # This file was generated by the Gradle 'init' task.
 2 | # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
 3 | 
 4 | [versions]
 5 | guava = "33.0.0-jre"
 6 | junit-jupiter = "5.10.2"
 7 | 
 8 | [libraries]
 9 | guava = { module = "com.google.guava:guava", version.ref = "guava" }
10 | junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
11 | 
```

--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------

```
 1 | /*
 2 |  * This file was generated by the Gradle 'init' task.
 3 |  *
 4 |  * The settings file is used to specify which projects to include in your build.
 5 |  * 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.
 6 |  */
 7 | 
 8 | plugins {
 9 |     // Apply the foojay-resolver plugin to allow automatic download of JDKs
10 |     id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
11 | }
12 | 
13 | rootProject.name = 'jadx-mcp-plugin'
14 | include('plugin')
15 | 
```

--------------------------------------------------------------------------------
/plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPluginOptions.java:
--------------------------------------------------------------------------------

```java
 1 | package com.mobilehackinglab.jadxplugin;
 2 | 
 3 | import jadx.api.plugins.options.OptionFlag;
 4 | import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
 5 | 
 6 | import static com.mobilehackinglab.jadxplugin.McpPlugin.PLUGIN_ID;
 7 | 
 8 | public class McpPluginOptions extends BasePluginOptionsBuilder {
 9 | 
10 |     private String httpInterface;
11 | 
12 |     @Override
13 |     public void registerOptions() {
14 |         strOption(PLUGIN_ID + ".http-interface")
15 |                 .description("interface to run mcp server on")
16 |                 .defaultValue("http://localhost:8085")
17 |                 .flags(OptionFlag.PER_PROJECT)
18 |                 .setter(v -> httpInterface = v);
19 |     }
20 | 
21 |     public String getHttpInterface() {
22 |         return httpInterface;
23 |     }
24 | 
25 | }
26 | 
```

--------------------------------------------------------------------------------
/plugin/build.gradle:
--------------------------------------------------------------------------------

```
 1 | plugins {
 2 |     id 'com.github.johnrengelman.shadow' version '8.1.1'
 3 |     id 'java'
 4 | }
 5 | 
 6 | group = 'jadx.plugins'
 7 | version = '1.3'
 8 | 
 9 | repositories {
10 |     mavenCentral()
11 | }
12 | 
13 | dependencies {
14 |     implementation 'org.json:json:20231013'
15 |     compileOnly("io.github.skylot:jadx-core:1.5.2")
16 | }
17 | 
18 | java {
19 |     toolchain {
20 |         languageVersion = JavaLanguageVersion.of(17) // Also supported older Java versions
21 |     }
22 | }
23 | 
24 | jar {
25 |     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
26 |     archiveBaseName = 'jadx-mcp-plugin' 
27 |     
28 |     manifest {
29 |         attributes(
30 |             'Jadx-Plugin': 'true',
31 |             'Jadx-Plugin-Class': 'com.mobilehackinglab.jadxplugin.McpPlugin',
32 |             'Jadx-Plugin-Version': '1.2.0',
33 |             'Jadx-Plugin-Id': 'jadx-mcp-plugin',
34 |             'Jadx-Plugin-Description': 'Exposes Jadx info over HTTP',
35 |             'Jadx-Plugin-Author': 'Mobile Hacking Lab'
36 |         )
37 |     }
38 |     
39 |     // This ensures all dependencies are included
40 |     from {
41 |         configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
42 |     }
43 |     
44 |     // Exclude problematic files from dependencies
45 |     exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/MANIFEST.MF'
46 | }
47 | 
48 | shadowJar {
49 |     archiveBaseName = 'jadx-mcp-plugin'
50 |     archiveVersion = '1.3.0'
51 |     mergeServiceFiles()
52 | }
53 | 
54 | // Installs the plugin JAR into Jadx
55 | tasks.register("installPlugin", Exec) {
56 |     group = "plugin"
57 |     description = "Installs the jadx-mcp plugin."
58 | 
59 |     dependsOn jar
60 | 
61 |     doFirst {
62 |         def pluginJar = layout.buildDirectory.file("libs/jadx-mcp-plugin-${project.version}.jar").get().asFile.absolutePath
63 |         commandLine "sh", "-c", """
64 |             jadx plugins --uninstall jadx-mcp >/dev/null
65 |             jadx plugins --install-jar \"${pluginJar}\"
66 |         """
67 |     }
68 | }
69 | 
70 | // Uninstalls the plugin from Jadx
71 | tasks.register("uninstallPlugin", Exec) {
72 |     group = "plugin"
73 |     description = "Uninstalls the jadx-mcp plugin."
74 |     commandLine "sh", "-c", "jadx plugins --uninstall jadx-mcp"
75 | }
76 | 
77 | // Enables the plugin after it's installed
78 | tasks.register("enablePlugin", Exec) {
79 |     group = "plugin"
80 |     description = "Enables the jadx-mcp plugin."
81 |     commandLine "sh", "-c", "jadx plugins --enable jadx-mcp"
82 | }
83 | 
84 | // Disables the plugin
85 | tasks.register("disablePlugin", Exec) {
86 |     group = "plugin"
87 |     description = "Disables the jadx-mcp plugin."
88 |     commandLine "sh", "-c", "jadx plugins --disable jadx-mcp"
89 | }
90 | 
```

--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------

```
 1 | @rem
 2 | @rem Copyright 2015 the original author or authors.
 3 | @rem
 4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
 5 | @rem you may not use this file except in compliance with the License.
 6 | @rem You may obtain a copy of the License at
 7 | @rem
 8 | @rem      https://www.apache.org/licenses/LICENSE-2.0
 9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | 
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem  Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 | 
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 | 
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 | 
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 | 
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 | 
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 | 
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 | 
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 | 
52 | goto fail
53 | 
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 | 
58 | if exist "%JAVA_EXE%" goto execute
59 | 
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 | 
66 | goto fail
67 | 
68 | :execute
69 | @rem Setup the command line
70 | 
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 | 
73 | 
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 | 
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 | 
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 | 
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 | 
92 | :omega
93 | 
```

--------------------------------------------------------------------------------
/fastmcp_adapter.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | import requests
  3 | from requests.exceptions import ConnectionError
  4 | import sys
  5 | 
  6 | # Create the MCP adapter with a human-readable name
  7 | mcp = FastMCP("Jadx MCP Server")
  8 | 
  9 | # Address of the Jadx MCP plugin's HTTP server
 10 | DEFAULT_MCP_SERVER = "http://localhost:8085"
 11 | mcp_server = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MCP_SERVER
 12 | 
 13 | def invoke_jadx(tool: str, parameters: dict = {}) -> dict:
 14 |     """
 15 |     Internal helper to send a tool request to the Jadx MCP HTTP server.
 16 |     """
 17 |     try:
 18 |         resp = requests.post(f"{mcp_server}/invoke", json={"tool": tool, "parameters": parameters})
 19 |         resp.raise_for_status()
 20 |         data = resp.json()
 21 |         if "error" in data:
 22 |             raise ValueError(data["error"])
 23 |         return data.get("result", data)
 24 |     except ConnectionError:
 25 |         raise ConnectionError("Jadx MCP server is not running. Please start Jadx and try again.")
 26 |     except Exception as e:
 27 |         raise RuntimeError(f"Unexpected error: {str(e)}")
 28 | 
 29 | 
 30 | @mcp.tool()
 31 | def list_all_classes(limit: int = 250, offset: int = 0) -> dict:
 32 |     """
 33 |     Returns a paginated list of class names.
 34 | 
 35 |     Params:
 36 |     - limit: Max number of classes to return (default 250)
 37 |     - offset: Starting index of class list
 38 |     """
 39 |     return invoke_jadx("list_all_classes", {"limit": limit, "offset": offset})
 40 | 
 41 | 
 42 | @mcp.tool()
 43 | def search_class_by_name(query: str) -> dict:
 44 |     """Search for class names that contain the given query string (case-insensitive)."""
 45 |     return invoke_jadx("search_class_by_name", {"query": query})
 46 | 
 47 | 
 48 | @mcp.tool()
 49 | def get_class_source(class_name: str) -> str:
 50 |     """
 51 |    Returns the full decompiled source code of a given class.
 52 |     """
 53 |     return invoke_jadx("get_class_source", {"class_name": class_name})
 54 | 
 55 | 
 56 | @mcp.tool()
 57 | def search_method_by_name(method_name: str) -> str:
 58 |     """
 59 |    Searches for all methods matching the provided name.
 60 |    Returns class and method pairs as string.
 61 |     """
 62 |     return invoke_jadx("search_method_by_name", {"method_name": method_name})
 63 | 
 64 | 
 65 | @mcp.tool()
 66 | def get_methods_of_class(class_name: str) -> list:
 67 |     """
 68 |    Returns all method names declared in the specified class.
 69 |     """
 70 |     return invoke_jadx("get_methods_of_class", {"class_name": class_name})
 71 | 
 72 | 
 73 | @mcp.tool()
 74 | def get_fields_of_class(class_name: str) -> list:
 75 |     """
 76 |    Returns all field names declared in the specified class.
 77 |     """
 78 |     return invoke_jadx("get_fields_of_class", {"class_name": class_name})
 79 | 
 80 | 
 81 | @mcp.tool()
 82 | def get_method_code(class_name: str, method_name: str) -> str:
 83 |     """
 84 |    Returns only the source code block of a specific method within a class.
 85 |     """
 86 |     return invoke_jadx("get_method_code", {
 87 |         "class_name": class_name,
 88 |         "method_name": method_name
 89 |     })
 90 | 
 91 | @mcp.tool()
 92 | def get_android_manifest() -> str:
 93 |     """
 94 |    Returns the content of AndroidManifest.xml
 95 |     """
 96 |     return invoke_jadx("get_android_manifest")
 97 | 
 98 | @mcp.resource("jadx://tools")
 99 | def get_tools_resource() -> dict:
100 |     """
101 |    Fetches the list of all available tools and their descriptions from the Jadx plugin.
102 |     Used for dynamic tool discovery.
103 |     """
104 |     try:
105 |         resp = requests.get(f"{mcp_server}/tools")
106 |         resp.raise_for_status()
107 |         return resp.json()
108 |     except ConnectionError:
109 |         raise ConnectionError("Jadx MCP server is not running. Please start Jadx and try again.")
110 |     except Exception as e:
111 |         raise RuntimeError(f"Unexpected error: {str(e)}")
112 | 
113 | if __name__ == "__main__":
114 |     mcp.run(transport="stdio")
115 |     print("Adapter started", file=sys.stderr)
116 | 
```

--------------------------------------------------------------------------------
/plugin/src/main/java/com/mobilehackinglab/jadxplugin/McpPlugin.java:
--------------------------------------------------------------------------------

```java
  1 | package com.mobilehackinglab.jadxplugin;
  2 | 
  3 | import jadx.api.JavaClass;
  4 | import jadx.api.JavaField;
  5 | import jadx.api.JavaMethod;
  6 | import jadx.api.plugins.JadxPlugin;
  7 | import jadx.api.plugins.JadxPluginContext;
  8 | import jadx.api.plugins.JadxPluginInfo;
  9 | import jadx.api.ResourceFile;
 10 | import jadx.api.ResourceType;
 11 | import jadx.core.xmlgen.ResContainer;
 12 | 
 13 | import java.io.IOException;
 14 | import java.io.BufferedReader;
 15 | import java.io.InputStreamReader;
 16 | import java.io.OutputStream;
 17 | import java.io.PrintWriter;
 18 | import java.net.*;
 19 | import java.nio.charset.StandardCharsets;
 20 | import java.util.List;
 21 | import java.util.concurrent.ExecutorService;
 22 | import java.util.concurrent.Executors;
 23 | 
 24 | import org.json.JSONArray;
 25 | import org.json.JSONException;
 26 | import org.json.JSONObject;
 27 | 
 28 | public class McpPlugin implements JadxPlugin {
 29 |     public static final String PLUGIN_ID="jadx-mcp";
 30 | 
 31 |     private ServerSocket serverSocket;
 32 |     private ExecutorService executor;
 33 |     private JadxPluginContext context;
 34 |     private McpPluginOptions pluginOptions;
 35 |     private boolean running = false;
 36 | 
 37 |     public McpPlugin() {
 38 |     }
 39 | 
 40 |     /**
 41 |      * Called by Jadx to initialize the plugin.
 42 |      */
 43 |     @Override
 44 |     public void init(JadxPluginContext context) {
 45 |         this.context = context;
 46 | 
 47 |         this.pluginOptions = new McpPluginOptions();
 48 |         this.context.registerOptions(this.pluginOptions);
 49 | 
 50 |         new Thread(this::safePluginStartup).start();
 51 |     }
 52 | 
 53 |     /**
 54 |      * Starts the HTTP server if Jadx is ready.
 55 |      */
 56 |     private void safePluginStartup() {
 57 |         if (!waitForJadxLoad()) {
 58 |             System.err.println("[MCP] Jadx initialization failed. Not starting server.");
 59 |             return;
 60 |         }
 61 | 
 62 |         try {
 63 |             URL httpInterface = parseHttpInterface(pluginOptions.getHttpInterface());
 64 |             startServer(httpInterface);
 65 |             System.out.println("[MCP] Server started successfully at " + httpInterface.getProtocol() + "://" + httpInterface.getHost() + ":" + httpInterface.getPort());
 66 |         } catch (IOException | IllegalArgumentException e) {
 67 |             System.err.println("[MCP] Failed to start server: " + e.getMessage());
 68 |         }
 69 |     }
 70 | 
 71 |     /**
 72 |      * Waits for the Jadx decompiler to finish loading classes.
 73 |      */
 74 |     private boolean waitForJadxLoad() {
 75 |         int retries = 0;
 76 |         while (retries < 30) {
 77 |             if (isDecompilerValid()) {
 78 |                 int count = context.getDecompiler().getClassesWithInners().size();
 79 |                 System.out.println("[MCP] Jadx fully loaded. Classes found: " + count);
 80 |                 return true;
 81 |             }
 82 | 
 83 |             System.out.println("[MCP] Waiting for Jadx to finish loading classes...");
 84 |             try {
 85 |                 Thread.sleep(1000);
 86 |             } catch (InterruptedException ignored) {
 87 |             }
 88 | 
 89 |             retries++;
 90 |         }
 91 | 
 92 |         System.err.println("[MCP] Jadx failed to load classes within expected time.");
 93 |         return false;
 94 |     }
 95 | 
 96 |     /**
 97 |      * Provides metadata for the plugin to Jadx.
 98 |      */
 99 |     @Override
100 |     public JadxPluginInfo getPluginInfo() {
101 |         return new JadxPluginInfo(
102 |                 PLUGIN_ID,
103 |                 "JADX MCP Plugin",
104 |                 "Exposes Jadx info over HTTP",
105 |                 "https://github.com/mobilehackinglab/jadx-mcp-plugin",
106 |                 "1.0.0");
107 |     }
108 | 
109 |     /**
110 |      * Cleanly shuts down the server and executor.
111 |      */
112 |     public void destroy() {
113 |         running = false;
114 |         if (serverSocket != null && !serverSocket.isClosed()) {
115 |             try {
116 |                 serverSocket.close();
117 |             } catch (IOException e) {
118 |                 // Ignore
119 |             }
120 |         }
121 | 
122 |         if (executor != null) {
123 |             executor.shutdown();
124 |         }
125 |     }
126 | 
127 |     /**
128 |      * Starts the TCP server and accepts incoming connections.
129 |      */
130 |     private void startServer(URL httpInterface) throws IOException {
131 | 
132 |         String host = httpInterface.getHost();
133 |         int port = httpInterface.getPort();
134 |         InetAddress bindAddr = InetAddress.getByName(host);
135 | 
136 |         serverSocket = new ServerSocket(port, 50, bindAddr);
137 |         executor = Executors.newFixedThreadPool(5);
138 |         running = true;
139 |         new Thread(() -> {
140 |             while (running) {
141 |                 try {
142 |                     Socket clientSocket = serverSocket.accept();
143 |                     executor.submit(() -> handleConnection(clientSocket));
144 |                 } catch (IOException e) {
145 |                     if (running)
146 |                         System.err.println("[MCP] Error accepting connection: " + e.getMessage());
147 |                 }
148 |             }
149 |         }).start();
150 |     }
151 | 
152 |     /**
153 |      * Handles incoming HTTP requests to the plugin.
154 |      */
155 |     private void handleConnection(Socket socket) {
156 |         try (socket;
157 |              BufferedReader in = new BufferedReader(
158 |                  new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
159 |              OutputStream outStream = socket.getOutputStream()) {
160 | 
161 |             String requestLine = in.readLine();
162 |             if (requestLine == null)
163 |                 return;
164 | 
165 |             String method = requestLine.split(" ")[0];
166 |             String path = requestLine.split(" ")[1];
167 | 
168 |             int contentLength = 0;
169 |             String header;
170 |             while (!(header = in.readLine()).isEmpty()) {
171 |                 if (header.toLowerCase().startsWith("content-length:")) {
172 |                     contentLength = Integer.parseInt(header.substring("content-length:".length()).trim());
173 |                 }
174 |             }
175 | 
176 |             char[] buffer = new char[contentLength];
177 |             int bytesRead = in.read(buffer);
178 |             String body = new String(buffer, 0, bytesRead);
179 | 
180 |             JSONObject responseJson;
181 | 
182 |             if ("/invoke".equals(path) && "POST".equalsIgnoreCase(method)) {
183 |                 try {
184 |                     String result = processInvokeRequest(body);
185 |                     if (result.trim().startsWith("{")) {
186 |                         responseJson = new JSONObject(result);
187 |                     } else {
188 |                         responseJson = new JSONObject().put("result", result);
189 |                     }
190 |                 } catch (Exception e) {
191 |                     responseJson = new JSONObject().put("error", "Failed to process tool: " + e.getMessage());
192 |                 }
193 |             } else if ("/tools".equals(path)) {
194 |                 responseJson = new JSONObject(getToolsJson());
195 |             } else {
196 |                 responseJson = new JSONObject().put("error", "Not found");
197 |             }
198 | 
199 |             byte[] respBytes = responseJson.toString(2).getBytes(StandardCharsets.UTF_8);
200 | 
201 |             PrintWriter out = new PrintWriter(outStream, true);
202 |             out.printf(
203 |                     "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",
204 |                     respBytes.length);
205 |             out.flush();
206 | 
207 |             outStream.write(respBytes);
208 |             outStream.flush();
209 | 
210 |         } catch (Exception e) {
211 |             System.err.println("[MCP] Error handling connection: " + e.getMessage());
212 |         }
213 |     }
214 | 
215 |     /**
216 |      * Return available tools for MCP server in JSON
217 |      */
218 |     private String getToolsJson() {
219 |         return """
220 |                 {
221 |                     "tools": [
222 |                         { "name": "get_class_source", "description": "Returns the decompiled source of a class.", "parameters": { "class_name": "string" } },
223 |                         { "name": "search_method_by_name", "description": "Search methods by name.", "parameters": { "method_name": "string" } },
224 |                         { "name": "search_class_by_name", "description": "Search class names containing a keyword.", "parameters": { "query": "string" } },
225 |                         { "name": "get_android_manifest", "description": "Returns the content of AndroidManifest.xml if available.", "parameters": {} },
226 |                         { "name": "list_all_classes", "description": "Returns a list of all class names.", "parameters": { "offset": "int", "limit": "int" } },
227 |                         { "name": "get_methods_of_class", "description": "Returns all method names of a class.", "parameters": { "class_name": "string" } },
228 |                         { "name": "get_fields_of_class", "description": "Returns all field names of a class.", "parameters": { "class_name": "string" } },
229 |                         { "name": "get_method_code", "description": "Returns the code for a specific method.", "parameters": { "class_name": "string", "method_name": "string" } }
230 |                     ]
231 |                 }
232 |                 """;
233 |     }
234 | 
235 |     /**
236 |      * Handles tool invocation from the client, routing to the correct handler.
237 |      *
238 |      * @param requestBody JSON request with tool and parameters
239 |      * @return JSON response string
240 |      * @throws JSONException if the input JSON is malformed
241 |      */
242 |     private String processInvokeRequest(String requestBody) throws JSONException {
243 |         JSONObject requestJson = new JSONObject(requestBody);
244 |         String toolName = requestJson.getString("tool");
245 |         JSONObject params = requestJson.optJSONObject("parameters");
246 |         if (params == null)
247 |             params = new JSONObject();
248 | 
249 |         return switch (toolName) {
250 |             case "get_class_source" -> handleGetClassSource(params);
251 |             case "search_method_by_name" -> handleSearchMethodByName(params);
252 |             case "search_class_by_name" -> handleSearchClassByName(params);
253 |             case "get_android_manifest" -> handleGetAndroidManifest();
254 |             case "list_all_classes" -> handleListAllClasses(params);
255 |             case "get_methods_of_class" -> handleGetMethodsOfClass(params);
256 |             case "get_fields_of_class" -> handleGetFieldsOfClass(params);
257 |             case "get_method_code" -> handleGetMethodCode(params);
258 |             default -> new JSONObject().put("error", "Unknown tool: " + toolName).toString();
259 |         };
260 |     }
261 | 
262 |     /**
263 |      * Search class names based on a partial query string and return matches.
264 |      *
265 |      * @param params JSON object with key "query"
266 |      * @return JSON object with array of matched class names under "results"
267 |      */
268 |     private String handleSearchClassByName(JSONObject params) {
269 |         String query = params.optString("query", "").toLowerCase();
270 |         JSONArray array = new JSONArray();
271 |         for (JavaClass cls : context.getDecompiler().getClassesWithInners()) {
272 |             String fullName = cls.getFullName();
273 |             if (fullName.toLowerCase().contains(query)) {
274 |                 array.put(fullName);
275 |             }
276 |         }
277 |         return new JSONObject().put("results", array).toString();
278 |     }
279 | 
280 |     /**
281 |      * Retrieves the content of AndroidManifest.xml
282 |      *
283 |      * This extracts the decoded XML from the ResContainer returned by loadContent().
284 |      *
285 |      * @return The manifest XML as a string or an error message.
286 |      */
287 |     private String handleGetAndroidManifest() {
288 |         try {
289 |             for (ResourceFile resFile : context.getDecompiler().getResources()) {
290 |                 if (resFile.getType() == ResourceType.MANIFEST) {
291 |                     ResContainer container = resFile.loadContent();
292 |                     if (container.getText() != null) {
293 |                         return container.getText().getCodeStr(); // decoded manifest
294 |                     } else {
295 |                         return "Manifest content is empty or could not be decoded.";
296 |                     }
297 |                 }
298 |             }
299 |             return "AndroidManifest.xml not found.";
300 |         } catch (Exception e) {
301 |             return "Error retrieving AndroidManifest.xml: " + e.getMessage();
302 |         }
303 |     }
304 | 
305 |     /**
306 |      * Retrieves the full decompiled source code of a specific Java class.
307 |      *
308 |      * @param params A JSON object containing the required parameter:
309 |      *               - "class_name": The fully qualified name of the class to
310 |      *               retrieve.
311 |      * @return The decompiled source code of the class, or an error message if not
312 |      *         found.
313 |      */
314 |     private String handleGetClassSource(JSONObject params) {
315 |         try {
316 |             String className = params.getString("class_name");
317 |             for (JavaClass cls : context.getDecompiler().getClasses()) {
318 |                 if (cls.getFullName().equals(className)) {
319 |                     return cls.getCode();
320 |                 }
321 |             }
322 |             return "Class not found: " + className;
323 |         } catch (Exception e) {
324 |             return "Error fetching class: " + e.getMessage();
325 |         }
326 |     }
327 | 
328 |     /**
329 |      * Searches all decompiled classes for methods whose names match or contain the
330 |      * provided string.
331 |      *
332 |      * @param params A JSON object containing the required parameter:
333 |      *               - "method_name": A case-insensitive string to match method
334 |      *               names.
335 |      * @return A newline-separated list of matched methods with their class names,
336 |      *         or a message if no match is found.
337 |      */
338 |     private String handleSearchMethodByName(JSONObject params) {
339 |         try {
340 |             String methodName = params.getString("method_name");
341 |             StringBuilder result = new StringBuilder();
342 |             for (JavaClass cls : context.getDecompiler().getClasses()) {
343 |                 cls.decompile();
344 |                 for (JavaMethod method : cls.getMethods()) {
345 |                     if (method.getName().toLowerCase().contains(methodName.toLowerCase())) {
346 |                         result.append(cls.getFullName()).append(" -> ").append(method.getName()).append("\n");
347 |                     }
348 |                 }
349 |             }
350 |             return result.length() > 0 ? result.toString() : "No methods found for: " + methodName;
351 |         } catch (Exception e) {
352 |             return "Error searching methods: " + e.getMessage();
353 |         }
354 |     }
355 | 
356 |     /**
357 |      * Lists all classes with optional pagination.
358 |      *
359 |      * @param params JSON with optional offset and limit
360 |      * @return JSON response with class list and metadata
361 |      */
362 |     private String handleListAllClasses(JSONObject params) {
363 |         int offset = params.optInt("offset", 0);
364 |         int limit = params.optInt("limit", 250);
365 |         int maxLimit = 500;
366 |         if (limit > maxLimit)
367 |             limit = maxLimit;
368 | 
369 |         List<JavaClass> allClasses = context.getDecompiler().getClassesWithInners();
370 |         int total = allClasses.size();
371 | 
372 |         JSONArray array = new JSONArray();
373 |         for (int i = offset; i < Math.min(offset + limit, total); i++) {
374 |             JavaClass cls = allClasses.get(i);
375 |             array.put(cls.getFullName());
376 |         }
377 | 
378 |         JSONObject response = new JSONObject();
379 |         response.put("total", total);
380 |         response.put("offset", offset);
381 |         response.put("limit", limit);
382 |         response.put("classes", array);
383 | 
384 |         return response.toString();
385 |     }
386 | 
387 |     /**
388 |      * Retrieves a list of all method names declared in the specified Java class.
389 |      *
390 |      * @param params A JSON object containing the required parameter:
391 |      *               - "class_name": The fully qualified name of the class.
392 |      * @return A formatted JSON array of method names, or an error message if the
393 |      *         class is not found.
394 |      */
395 |     private String handleGetMethodsOfClass(JSONObject params) {
396 |         try {
397 |             String className = params.getString("class_name");
398 |             for (JavaClass cls : context.getDecompiler().getClasses()) {
399 |                 if (cls.getFullName().equals(className)) {
400 |                     JSONArray array = new JSONArray();
401 |                     for (JavaMethod method : cls.getMethods()) {
402 |                         array.put(method.getName());
403 |                     }
404 |                     return array.toString(2);
405 |                 }
406 |             }
407 |             return "Class not found: " + className;
408 |         } catch (Exception e) {
409 |             return "Error fetching methods: " + e.getMessage();
410 |         }
411 |     }
412 | 
413 |     /**
414 |      * Retrieves all field names defined in the specified Java class.
415 |      *
416 |      * @param params A JSON object containing the required parameter:
417 |      *               - "class_name": The fully qualified name of the class.
418 |      * @return A formatted JSON array of field names, or an error message if the
419 |      *         class is not found.
420 |      */
421 |     private String handleGetFieldsOfClass(JSONObject params) {
422 |         try {
423 |             String className = params.getString("class_name");
424 |             for (JavaClass cls : context.getDecompiler().getClasses()) {
425 |                 if (cls.getFullName().equals(className)) {
426 |                     JSONArray array = new JSONArray();
427 |                     for (JavaField field : cls.getFields()) {
428 |                         array.put(field.getName());
429 |                     }
430 |                     return array.toString(2);
431 |                 }
432 |             }
433 |             return "Class not found: " + className;
434 |         } catch (Exception e) {
435 |             return "Error fetching fields: " + e.getMessage();
436 |         }
437 |     }
438 | 
439 |     /**
440 |      * Extracts the decompiled source code of a specific method within a given
441 |      * class.
442 |      *
443 |      * @param params A JSON object containing the required parameters:
444 |      *               - "class_name": The fully qualified name of the class.
445 |      *               - "method_name": The name of the method to extract.
446 |      * @return A string containing the method's source code block (if found),
447 |      *         or a descriptive error message if the method or class is not found.
448 |      */
449 |     private String handleGetMethodCode(JSONObject params) {
450 |         try {
451 |             String className = params.getString("class_name");
452 |             String methodName = params.getString("method_name");
453 |             for (JavaClass cls : context.getDecompiler().getClasses()) {
454 |                 if (cls.getFullName().equals(className)) {
455 |                     cls.decompile();
456 |                     for (JavaMethod method : cls.getMethods()) {
457 |                         if (method.getName().equals(methodName)) {
458 |                             String classCode = cls.getCode();
459 |                             int methodIndex = classCode.indexOf(method.getName());
460 |                             if (methodIndex != -1) {
461 |                                 int openBracket = classCode.indexOf('{', methodIndex);
462 |                                 if (openBracket != -1) {
463 |                                     int closeBracket = findMatchingBracket(classCode, openBracket);
464 |                                     if (closeBracket != -1) {
465 |                                         String methodCode = classCode.substring(openBracket, closeBracket + 1);
466 |                                         return methodCode;
467 |                                     }
468 |                                 }
469 |                             }
470 | 
471 |                             return "Could not extract method code from class source.";
472 |                         }
473 |                     }
474 |                     return "Method '" + methodName + "' not found in class '" + className + "'";
475 |                 }
476 |             }
477 |             return "Class '" + className + "' not found";
478 |         } catch (Exception e) {
479 |             return "Error fetching method code: " + e.getMessage();
480 |         }
481 |     }
482 | 
483 |     // Helper method to find matching closing bracket
484 |     private int findMatchingBracket(String code, int openPos) {
485 |         int depth = 1;
486 |         for (int i = openPos + 1; i < code.length(); i++) {
487 |             char c = code.charAt(i);
488 |             if (c == '{') {
489 |                 depth++;
490 |             } else if (c == '}') {
491 |                 depth--;
492 |                 if (depth == 0) {
493 |                     return i;
494 |                 }
495 |             }
496 |         }
497 |         return -1; // No matching bracket found
498 |     }
499 | 
500 |     /**
501 |      * Validates that the decompiler is loaded and usable.
502 |      * This is needed because: When you use "File → Open" to load a new file,
503 |      * Jadx replaces the internal decompiler instance, but your plugin still holds a
504 |      * stale reference to the old one.
505 |      */
506 |     private boolean isDecompilerValid() {
507 |         try {
508 |             return context != null
509 |                     && context.getDecompiler() != null
510 |                     && context.getDecompiler().getRoot() != null
511 |                     && !context.getDecompiler().getClassesWithInners().isEmpty();
512 |         } catch (Exception e) {
513 |             return false;
514 |         }
515 |     }
516 | 
517 |     /**
518 |      * Parses and validates the given HTTP interface string.
519 |      *
520 |      * <p>This method strictly enforces that the input must be a complete HTTP URL
521 |      * with a protocol of {@code http}, a non-empty host, and an explicit port.</p>
522 |      *
523 |      * <p>Examples of valid input: {@code http://127.0.0.1:8080}, {@code http://localhost:3000}</p>
524 |      *
525 |      * @param httpInterface A string representing the HTTP interface.
526 |      * @return A {@link URL} object representing the parsed interface.
527 |      * @throws IllegalArgumentException if the URL is malformed or missing required components.
528 |      */
529 |     private URL parseHttpInterface(String httpInterface) throws IllegalArgumentException {
530 |         try {
531 |             URL url = new URL(httpInterface);
532 | 
533 |             if (!"http".equalsIgnoreCase(url.getProtocol())) {
534 |                 throw new IllegalArgumentException("Invalid protocol: " + url.getProtocol() + ". Only 'http' is supported.");
535 |             }
536 | 
537 |             if (url.getHost() == null || url.getHost().isEmpty()) {
538 |                 throw new IllegalArgumentException("Missing or invalid host in HTTP interface: " + httpInterface);
539 |             }
540 | 
541 |             if (url.getPort() == -1) {
542 |                 throw new IllegalArgumentException("Port must be explicitly specified in HTTP interface: " + httpInterface);
543 |             }
544 | 
545 |             if (url.getPath() != null && !url.getPath().isEmpty() && !url.getPath().equals("/")) {
546 |                 throw new IllegalArgumentException("Path is not allowed in HTTP interface: " + httpInterface);
547 |             }
548 | 
549 |             if (url.getQuery() != null || url.getRef() != null || url.getUserInfo() != null) {
550 |                 throw new IllegalArgumentException("HTTP interface must not contain query, fragment, or user info: " + httpInterface);
551 |             }
552 | 
553 |             return url;
554 |         } catch (MalformedURLException e) {
555 |             throw new IllegalArgumentException("Malformed HTTP interface URL: " + httpInterface, e);
556 |         }
557 |     }
558 | }
```