#
tokens: 8968/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
![](img/jadx-mcp-running.png)

### ✅ 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);
        }
    }
}
```