Move to Hydra
This commit is contained in:
parent
bb10207b84
commit
76e8355252
@ -7,134 +7,184 @@
|
|||||||
# ///
|
# ///
|
||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
|
from typing import Dict
|
||||||
|
from threading import Lock
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
DEFAULT_GHIDRA_SERVER = "http://127.0.0.1:8080/"
|
# Track active Ghidra instances (port -> url)
|
||||||
ghidra_server_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_GHIDRA_SERVER
|
active_instances: Dict[int, str] = {}
|
||||||
|
instances_lock = Lock()
|
||||||
|
DEFAULT_GHIDRA_PORT = 8192
|
||||||
|
DEFAULT_GHIDRA_HOST = "localhost"
|
||||||
|
|
||||||
mcp = FastMCP("ghidra-mcp")
|
mcp = FastMCP("hydra-mcp")
|
||||||
|
|
||||||
def safe_get(endpoint: str, params: dict = None) -> list:
|
# Get host from command line or use default
|
||||||
"""
|
ghidra_host = DEFAULT_GHIDRA_HOST
|
||||||
Perform a GET request with optional query parameters.
|
if len(sys.argv) > 1:
|
||||||
"""
|
ghidra_host = sys.argv[1]
|
||||||
|
print(f"Using Ghidra host: {ghidra_host}")
|
||||||
|
|
||||||
|
def get_instance_url(port: int) -> str:
|
||||||
|
"""Get URL for a Ghidra instance by port"""
|
||||||
|
with instances_lock:
|
||||||
|
if port in active_instances:
|
||||||
|
return active_instances[port]
|
||||||
|
|
||||||
|
# Auto-register if not found but port is valid
|
||||||
|
if 8192 <= port <= 65535:
|
||||||
|
register_instance(port)
|
||||||
|
return active_instances[port]
|
||||||
|
|
||||||
|
return f"http://{ghidra_host}:{port}"
|
||||||
|
|
||||||
|
def safe_get(port: int, endpoint: str, params: dict = None) -> list:
|
||||||
|
"""Perform a GET request to a specific Ghidra instance"""
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
url = f"{ghidra_server_url}/{endpoint}"
|
url = f"{get_instance_url(port)}/{endpoint}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=5)
|
response = requests.get(url, params=params, timeout=5)
|
||||||
response.encoding = 'utf-8'
|
response.encoding = 'utf-8'
|
||||||
if response.ok:
|
if response.ok:
|
||||||
return response.text.splitlines()
|
return response.text.splitlines()
|
||||||
|
elif response.status_code == 404:
|
||||||
|
# Try falling back to default instance if this was a secondary instance
|
||||||
|
if port != DEFAULT_GHIDRA_PORT:
|
||||||
|
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
|
||||||
|
return [f"Error {response.status_code}: {response.text.strip()}"]
|
||||||
else:
|
else:
|
||||||
return [f"Error {response.status_code}: {response.text.strip()}"]
|
return [f"Error {response.status_code}: {response.text.strip()}"]
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
# Instance may be down - try default instance if this was secondary
|
||||||
|
if port != DEFAULT_GHIDRA_PORT:
|
||||||
|
return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
|
||||||
|
return ["Error: Failed to connect to Ghidra instance"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"Request failed: {str(e)}"]
|
return [f"Request failed: {str(e)}"]
|
||||||
|
|
||||||
def safe_post(endpoint: str, data: dict | str) -> str:
|
def safe_post(port: int, endpoint: str, data: dict | str) -> str:
|
||||||
|
"""Perform a POST request to a specific Ghidra instance"""
|
||||||
try:
|
try:
|
||||||
|
url = f"{get_instance_url(port)}/{endpoint}"
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data, timeout=5)
|
response = requests.post(url, data=data, timeout=5)
|
||||||
else:
|
else:
|
||||||
response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data.encode("utf-8"), timeout=5)
|
response = requests.post(url, data=data.encode("utf-8"), timeout=5)
|
||||||
response.encoding = 'utf-8'
|
response.encoding = 'utf-8'
|
||||||
if response.ok:
|
if response.ok:
|
||||||
return response.text.strip()
|
return response.text.strip()
|
||||||
|
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT:
|
||||||
|
# Try falling back to default instance
|
||||||
|
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||||
else:
|
else:
|
||||||
return f"Error {response.status_code}: {response.text.strip()}"
|
return f"Error {response.status_code}: {response.text.strip()}"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
if port != DEFAULT_GHIDRA_PORT:
|
||||||
|
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||||
|
return "Error: Failed to connect to Ghidra instance"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Request failed: {str(e)}"
|
return f"Request failed: {str(e)}"
|
||||||
|
|
||||||
|
# Instance management tools
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_methods(offset: int = 0, limit: int = 100) -> list:
|
def list_instances() -> dict:
|
||||||
"""
|
"""List all active Ghidra instances"""
|
||||||
List all function names in the program with pagination.
|
with instances_lock:
|
||||||
"""
|
return {
|
||||||
return safe_get("methods", {"offset": offset, "limit": limit})
|
"instances": [
|
||||||
|
{"port": port, "url": url}
|
||||||
|
for port, url in active_instances.items()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_classes(offset: int = 0, limit: int = 100) -> list:
|
def register_instance(port: int, url: str = None) -> str:
|
||||||
"""
|
"""Register a new Ghidra instance"""
|
||||||
List all namespace/class names in the program with pagination.
|
if url is None:
|
||||||
"""
|
url = f"http://{ghidra_host}:{port}"
|
||||||
return safe_get("classes", {"offset": offset, "limit": limit})
|
|
||||||
|
# Verify instance is reachable before registering
|
||||||
|
try:
|
||||||
|
test_url = f"{url}/instances"
|
||||||
|
response = requests.get(test_url, timeout=2)
|
||||||
|
if not response.ok:
|
||||||
|
return f"Error: Instance at {url} is not responding properly"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: Could not connect to instance at {url}: {str(e)}"
|
||||||
|
|
||||||
|
with instances_lock:
|
||||||
|
active_instances[port] = url
|
||||||
|
return f"Registered instance on port {port} at {url}"
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def decompile_function(name: str) -> str:
|
def unregister_instance(port: int) -> str:
|
||||||
"""
|
"""Unregister a Ghidra instance"""
|
||||||
Decompile a specific function by name and return the decompiled C code.
|
with instances_lock:
|
||||||
"""
|
if port in active_instances:
|
||||||
return safe_post("decompile", name)
|
del active_instances[port]
|
||||||
|
return f"Unregistered instance on port {port}"
|
||||||
|
return f"No instance found on port {port}"
|
||||||
|
|
||||||
|
# Updated tool implementations with port parameter
|
||||||
|
@mcp.tool()
|
||||||
|
def list_methods(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
|
return safe_get(port, "methods", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_function(old_name: str, new_name: str) -> str:
|
def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
"""
|
return safe_get(port, "classes", {"offset": offset, "limit": limit})
|
||||||
Rename a function by its current name to a new user-defined name.
|
|
||||||
"""
|
|
||||||
return safe_post("renameFunction", {"oldName": old_name, "newName": new_name})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_data(address: str, new_name: str) -> str:
|
def decompile_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "") -> str:
|
||||||
"""
|
return safe_post(port, "decompile", name)
|
||||||
Rename a data label at the specified address.
|
|
||||||
"""
|
|
||||||
return safe_post("renameData", {"address": address, "newName": new_name})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_segments(offset: int = 0, limit: int = 100) -> list:
|
def rename_function(port: int = DEFAULT_GHIDRA_PORT, old_name: str = "", new_name: str = "") -> str:
|
||||||
"""
|
return safe_post(port, "renameFunction", {"oldName": old_name, "newName": new_name})
|
||||||
List all memory segments in the program with pagination.
|
|
||||||
"""
|
|
||||||
return safe_get("segments", {"offset": offset, "limit": limit})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_imports(offset: int = 0, limit: int = 100) -> list:
|
def rename_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str:
|
||||||
"""
|
return safe_post(port, "renameData", {"address": address, "newName": new_name})
|
||||||
List imported symbols in the program with pagination.
|
|
||||||
"""
|
|
||||||
return safe_get("imports", {"offset": offset, "limit": limit})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_exports(offset: int = 0, limit: int = 100) -> list:
|
def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
"""
|
return safe_get(port, "segments", {"offset": offset, "limit": limit})
|
||||||
List exported functions/symbols with pagination.
|
|
||||||
"""
|
|
||||||
return safe_get("exports", {"offset": offset, "limit": limit})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_namespaces(offset: int = 0, limit: int = 100) -> list:
|
def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
"""
|
return safe_get(port, "imports", {"offset": offset, "limit": limit})
|
||||||
List all non-global namespaces in the program with pagination.
|
|
||||||
"""
|
|
||||||
return safe_get("namespaces", {"offset": offset, "limit": limit})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_data_items(offset: int = 0, limit: int = 100) -> list:
|
def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
"""
|
return safe_get(port, "exports", {"offset": offset, "limit": limit})
|
||||||
List defined data labels and their values with pagination.
|
|
||||||
"""
|
|
||||||
return safe_get("data", {"offset": offset, "limit": limit})
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list:
|
def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
"""
|
return safe_get(port, "namespaces", {"offset": offset, "limit": limit})
|
||||||
Search for functions whose name contains the given substring.
|
|
||||||
"""
|
@mcp.tool()
|
||||||
|
def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
|
return safe_get(port, "data", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list:
|
||||||
if not query:
|
if not query:
|
||||||
return ["Error: query string is required"]
|
return ["Error: query string is required"]
|
||||||
return safe_get("searchFunctions", {"query": query, "offset": offset, "limit": limit})
|
return safe_get(port, "searchFunctions", {"query": query, "offset": offset, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
|
# Handle graceful shutdown
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def handle_sigint(signum, frame):
|
def handle_sigint(signum, frame):
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Auto-register default instance
|
||||||
|
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, handle_sigint)
|
signal.signal(signal.SIGINT, handle_sigint)
|
||||||
mcp.run()
|
mcp.run()
|
||||||
|
|||||||
BIN
lib/Base.jar
Executable file
BIN
lib/Base.jar
Executable file
Binary file not shown.
BIN
lib/Decompiler.jar
Executable file
BIN
lib/Decompiler.jar
Executable file
Binary file not shown.
BIN
lib/Docking.jar
Executable file
BIN
lib/Docking.jar
Executable file
Binary file not shown.
BIN
lib/Generic.jar
Executable file
BIN
lib/Generic.jar
Executable file
Binary file not shown.
BIN
lib/Project.jar
Executable file
BIN
lib/Project.jar
Executable file
Binary file not shown.
BIN
lib/SoftwareModeling.jar
Executable file
BIN
lib/SoftwareModeling.jar
Executable file
Binary file not shown.
BIN
lib/Utility.jar
Executable file
BIN
lib/Utility.jar
Executable file
Binary file not shown.
35
pom.xml
35
pom.xml
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.lauriewired</groupId>
|
<groupId>com.lauriewired</groupId>
|
||||||
<artifactId>GhidraMCP</artifactId>
|
<artifactId>HydraMCP</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.1</version>
|
||||||
<name>GhidraMCP</name>
|
<name>HydraMCP</name>
|
||||||
<url>http://maven.apache.org</url>
|
<url>https://github.com/LaurieWired/GhidraMCP</url>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||||
@ -71,18 +71,29 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<!-- Use custom MANIFEST.MF -->
|
<!-- Set Java version -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<version>3.2.2</version>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>11</source>
|
||||||
|
<target>11</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Use custom MANIFEST.MF -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.2.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
|
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
|
||||||
</archive>
|
</archive>
|
||||||
<!-- Set a fixed name for the JAR without version -->
|
<!-- Set a fixed name for the JAR without version -->
|
||||||
<finalName>GhidraMCP</finalName>
|
<finalName>HydraMCP</finalName>
|
||||||
<!-- Exclude the App class -->
|
<!-- Exclude the App class -->
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>**/App.class</exclude>
|
<exclude>**/App.class</exclude>
|
||||||
@ -104,7 +115,7 @@
|
|||||||
</descriptors>
|
</descriptors>
|
||||||
|
|
||||||
<!-- The name of the final zip -->
|
<!-- The name of the final zip -->
|
||||||
<finalName>GhidraMCP-${project.version}</finalName>
|
<finalName>HydraMCP-${project.version}</finalName>
|
||||||
|
|
||||||
<!-- Don't append the assembly ID -->
|
<!-- Don't append the assembly ID -->
|
||||||
<appendAssemblyId>false</appendAssemblyId>
|
<appendAssemblyId>false</appendAssemblyId>
|
||||||
|
|||||||
@ -24,17 +24,17 @@
|
|||||||
<include>extension.properties</include>
|
<include>extension.properties</include>
|
||||||
<include>Module.manifest</include>
|
<include>Module.manifest</include>
|
||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>GhidraMCP</outputDirectory>
|
<outputDirectory>HydraMCP</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
|
||||||
<!-- 2) Copy your built plugin JAR into GhidraMCP/lib -->
|
<!-- 2) Copy your built plugin JAR into HydraMCP/lib -->
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>${project.build.directory}</directory>
|
<directory>${project.build.directory}</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<!-- Use the finalized JAR name from the maven-jar-plugin -->
|
<!-- Use the finalized JAR name from the maven-jar-plugin -->
|
||||||
<include>GhidraMCP.jar</include>
|
<include>HydraMCP.jar</include>
|
||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>GhidraMCP/lib</outputDirectory>
|
<outputDirectory>HydraMCP/lib</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
</fileSets>
|
</fileSets>
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
package com.lauriewired;
|
package com.lauriewired;
|
||||||
|
|
||||||
import ghidra.framework.plugintool.Plugin;
|
import ghidra.framework.plugintool.*;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.util.*;
|
||||||
|
import ghidra.framework.main.ApplicationLevelPlugin;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
import ghidra.program.model.address.GlobalNamespace;
|
import ghidra.program.model.address.GlobalNamespace;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
@ -24,9 +25,11 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.*;
|
||||||
|
|
||||||
@PluginInfo(
|
@PluginInfo(
|
||||||
status = PluginStatus.RELEASED,
|
status = PluginStatus.RELEASED,
|
||||||
@ -35,23 +38,47 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
shortDescription = "HTTP server plugin",
|
shortDescription = "HTTP server plugin",
|
||||||
description = "Starts an embedded HTTP server to expose program data."
|
description = "Starts an embedded HTTP server to expose program data."
|
||||||
)
|
)
|
||||||
public class GhidraMCPPlugin extends Plugin {
|
public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||||
|
|
||||||
|
private static final Map<Integer, HydraMCPPlugin> activeInstances = new ConcurrentHashMap<>();
|
||||||
|
private static final AtomicInteger nextPort = new AtomicInteger(8192);
|
||||||
|
private static final Object baseInstanceLock = new Object();
|
||||||
|
|
||||||
private HttpServer server;
|
private HttpServer server;
|
||||||
|
private int port;
|
||||||
|
private boolean isBaseInstance = false;
|
||||||
|
|
||||||
public GhidraMCPPlugin(PluginTool tool) {
|
public HydraMCPPlugin(PluginTool tool) {
|
||||||
super(tool);
|
super(tool);
|
||||||
Msg.info(this, "GhidraMCPPlugin loaded!");
|
|
||||||
|
// Find available port
|
||||||
|
this.port = findAvailablePort();
|
||||||
|
activeInstances.put(port, this);
|
||||||
|
|
||||||
|
// Check if we should be base instance
|
||||||
|
synchronized (baseInstanceLock) {
|
||||||
|
if (port == 8192 || activeInstances.get(8192) == null) {
|
||||||
|
this.isBaseInstance = true;
|
||||||
|
Msg.info(this, "Starting as base instance on port " + port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log to both console and log file
|
||||||
|
Msg.info(this, "HydraMCPPlugin loaded on port " + port);
|
||||||
|
System.out.println("[HydraMCP] Plugin loaded on port " + port);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startServer();
|
startServer();
|
||||||
}
|
} catch (IOException e) {
|
||||||
catch (IOException e) {
|
Msg.error(this, "Failed to start HTTP server on port " + port, e);
|
||||||
Msg.error(this, "Failed to start HTTP server", e);
|
if (e.getMessage().contains("Address already in use")) {
|
||||||
|
Msg.showError(this, null, "Port Conflict",
|
||||||
|
"Port " + port + " is already in use. Please specify a different port with -Dghidra.mcp.port=NEW_PORT");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startServer() throws IOException {
|
private void startServer() throws IOException {
|
||||||
int port = 8080;
|
|
||||||
server = HttpServer.create(new InetSocketAddress(port), 0);
|
server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
|
|
||||||
// Each listing endpoint uses offset & limit from query params:
|
// Each listing endpoint uses offset & limit from query params:
|
||||||
@ -130,11 +157,44 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
|
sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Instance management endpoints
|
||||||
|
server.createContext("/instances", exchange -> {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Map.Entry<Integer, HydraMCPPlugin> entry : activeInstances.entrySet()) {
|
||||||
|
sb.append(entry.getKey()).append(": ")
|
||||||
|
.append(entry.getValue().isBaseInstance ? "base" : "secondary")
|
||||||
|
.append("\n");
|
||||||
|
}
|
||||||
|
sendResponse(exchange, sb.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/registerInstance", exchange -> {
|
||||||
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
|
int port = parseIntOrDefault(params.get("port"), 0);
|
||||||
|
if (port > 0) {
|
||||||
|
sendResponse(exchange, "Instance registered on port " + port);
|
||||||
|
} else {
|
||||||
|
sendResponse(exchange, "Invalid port number");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/unregisterInstance", exchange -> {
|
||||||
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
|
int port = parseIntOrDefault(params.get("port"), 0);
|
||||||
|
if (port > 0 && activeInstances.containsKey(port)) {
|
||||||
|
activeInstances.remove(port);
|
||||||
|
sendResponse(exchange, "Unregistered instance on port " + port);
|
||||||
|
} else {
|
||||||
|
sendResponse(exchange, "No instance found on port " + port);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.setExecutor(null);
|
server.setExecutor(null);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
server.start();
|
server.start();
|
||||||
Msg.info(this, "GhidraMCP HTTP server started on port " + port);
|
Msg.info(this, "HydraMCP HTTP server started on port " + port);
|
||||||
}, "GhidraMCP-HTTP-Server").start();
|
System.out.println("[HydraMCP] HTTP server started on port " + port);
|
||||||
|
}, "HydraMCP-HTTP-Server").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------
|
||||||
@ -278,19 +338,24 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
Program program = getCurrentProgram();
|
Program program = getCurrentProgram();
|
||||||
if (program == null) return "No program loaded";
|
if (program == null) return "No program loaded";
|
||||||
DecompInterface decomp = new DecompInterface();
|
DecompInterface decomp = new DecompInterface();
|
||||||
decomp.openProgram(program);
|
try {
|
||||||
|
if (!decomp.openProgram(program)) {
|
||||||
|
return "Failed to initialize decompiler";
|
||||||
|
}
|
||||||
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
||||||
if (func.getName().equals(name)) {
|
if (func.getName().equals(name)) {
|
||||||
DecompileResults result =
|
DecompileResults result =
|
||||||
decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
|
decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
|
||||||
if (result != null && result.decompileCompleted()) {
|
if (result != null && result.decompileCompleted()) {
|
||||||
return result.getDecompiledFunction().getC();
|
return result.getDecompiledFunction().getC();
|
||||||
} else {
|
|
||||||
return "Decompilation failed";
|
|
||||||
}
|
}
|
||||||
|
return "Decompilation failed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Function not found";
|
return "Function not found";
|
||||||
|
} finally {
|
||||||
|
decomp.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean renameFunction(String oldName, String newName) {
|
private boolean renameFunction(String oldName, String newName) {
|
||||||
@ -455,12 +520,31 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int findAvailablePort() {
|
||||||
|
int basePort = 8192;
|
||||||
|
int maxAttempts = 10;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
int candidate = basePort + attempt;
|
||||||
|
if (!activeInstances.containsKey(candidate)) {
|
||||||
|
try (ServerSocket s = new ServerSocket(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
} catch (IOException e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Could not find available port after " + maxAttempts + " attempts");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
server.stop(0);
|
server.stop(0);
|
||||||
Msg.info(this, "HTTP server stopped.");
|
Msg.info(this, "HTTP server stopped on port " + port);
|
||||||
|
System.out.println("[HydraMCP] HTTP server stopped on port " + port);
|
||||||
}
|
}
|
||||||
|
activeInstances.remove(port);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
name=GhidraMCP
|
name=HydraMCP
|
||||||
description=A plugin that runs an embedded HTTP server to expose program data.
|
description=A plugin that runs an embedded HTTP server to expose program data.
|
||||||
author=LaurieWired
|
author=LaurieWired
|
||||||
createdOn=2025-03-22
|
createdOn=2025-03-22
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user