diff --git a/bridge_mcp_ghidra.py b/bridge_mcp_ghidra.py index 1c4477d..e9043e9 100644 --- a/bridge_mcp_ghidra.py +++ b/bridge_mcp_ghidra.py @@ -7,134 +7,184 @@ # /// import sys import requests - +from typing import Dict +from threading import Lock from mcp.server.fastmcp import FastMCP -DEFAULT_GHIDRA_SERVER = "http://127.0.0.1:8080/" -ghidra_server_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_GHIDRA_SERVER +# Track active Ghidra instances (port -> url) +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: - """ - Perform a GET request with optional query parameters. - """ +# Get host from command line or use default +ghidra_host = DEFAULT_GHIDRA_HOST +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: params = {} - url = f"{ghidra_server_url}/{endpoint}" + url = f"{get_instance_url(port)}/{endpoint}" try: response = requests.get(url, params=params, timeout=5) response.encoding = 'utf-8' if response.ok: 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: 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: 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: + url = f"{get_instance_url(port)}/{endpoint}" 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: - 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' if response.ok: 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: 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: return f"Request failed: {str(e)}" +# Instance management tools @mcp.tool() -def list_methods(offset: int = 0, limit: int = 100) -> list: - """ - List all function names in the program with pagination. - """ - return safe_get("methods", {"offset": offset, "limit": limit}) +def list_instances() -> dict: + """List all active Ghidra instances""" + with instances_lock: + return { + "instances": [ + {"port": port, "url": url} + for port, url in active_instances.items() + ] + } @mcp.tool() -def list_classes(offset: int = 0, limit: int = 100) -> list: - """ - List all namespace/class names in the program with pagination. - """ - return safe_get("classes", {"offset": offset, "limit": limit}) +def register_instance(port: int, url: str = None) -> str: + """Register a new Ghidra instance""" + if url is None: + url = f"http://{ghidra_host}:{port}" + + # 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() -def decompile_function(name: str) -> str: - """ - Decompile a specific function by name and return the decompiled C code. - """ - return safe_post("decompile", name) +def unregister_instance(port: int) -> str: + """Unregister a Ghidra instance""" + with instances_lock: + if port in active_instances: + 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() -def rename_function(old_name: str, new_name: str) -> str: - """ - Rename a function by its current name to a new user-defined name. - """ - return safe_post("renameFunction", {"oldName": old_name, "newName": new_name}) +def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + return safe_get(port, "classes", {"offset": offset, "limit": limit}) @mcp.tool() -def rename_data(address: str, new_name: str) -> str: - """ - Rename a data label at the specified address. - """ - return safe_post("renameData", {"address": address, "newName": new_name}) +def decompile_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "") -> str: + return safe_post(port, "decompile", name) @mcp.tool() -def list_segments(offset: int = 0, limit: int = 100) -> list: - """ - List all memory segments in the program with pagination. - """ - return safe_get("segments", {"offset": offset, "limit": limit}) +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}) @mcp.tool() -def list_imports(offset: int = 0, limit: int = 100) -> list: - """ - List imported symbols in the program with pagination. - """ - return safe_get("imports", {"offset": offset, "limit": limit}) +def rename_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str: + return safe_post(port, "renameData", {"address": address, "newName": new_name}) @mcp.tool() -def list_exports(offset: int = 0, limit: int = 100) -> list: - """ - List exported functions/symbols with pagination. - """ - return safe_get("exports", {"offset": offset, "limit": limit}) +def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + return safe_get(port, "segments", {"offset": offset, "limit": limit}) @mcp.tool() -def list_namespaces(offset: int = 0, limit: int = 100) -> list: - """ - List all non-global namespaces in the program with pagination. - """ - return safe_get("namespaces", {"offset": offset, "limit": limit}) +def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + return safe_get(port, "imports", {"offset": offset, "limit": limit}) @mcp.tool() -def list_data_items(offset: int = 0, limit: int = 100) -> list: - """ - List defined data labels and their values with pagination. - """ - return safe_get("data", {"offset": offset, "limit": limit}) +def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + return safe_get(port, "exports", {"offset": offset, "limit": limit}) @mcp.tool() -def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list: - """ - Search for functions whose name contains the given substring. - """ +def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + return safe_get(port, "namespaces", {"offset": offset, "limit": limit}) + +@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: 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 os def handle_sigint(signum, frame): os._exit(0) 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) mcp.run() diff --git a/lib/Base.jar b/lib/Base.jar new file mode 100755 index 0000000..d347dbd Binary files /dev/null and b/lib/Base.jar differ diff --git a/lib/Decompiler.jar b/lib/Decompiler.jar new file mode 100755 index 0000000..6da4136 Binary files /dev/null and b/lib/Decompiler.jar differ diff --git a/lib/Docking.jar b/lib/Docking.jar new file mode 100755 index 0000000..2e58f2e Binary files /dev/null and b/lib/Docking.jar differ diff --git a/lib/Generic.jar b/lib/Generic.jar new file mode 100755 index 0000000..1755e14 Binary files /dev/null and b/lib/Generic.jar differ diff --git a/lib/Project.jar b/lib/Project.jar new file mode 100755 index 0000000..fb93904 Binary files /dev/null and b/lib/Project.jar differ diff --git a/lib/SoftwareModeling.jar b/lib/SoftwareModeling.jar new file mode 100755 index 0000000..b9ed858 Binary files /dev/null and b/lib/SoftwareModeling.jar differ diff --git a/lib/Utility.jar b/lib/Utility.jar new file mode 100755 index 0000000..4fe9673 Binary files /dev/null and b/lib/Utility.jar differ diff --git a/pom.xml b/pom.xml index ec29081..f7f5dc4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,11 +4,11 @@ 4.0.0 com.lauriewired - GhidraMCP + HydraMCP jar - 1.0-SNAPSHOT - GhidraMCP - http://maven.apache.org + 1.1 + HydraMCP + https://github.com/LaurieWired/GhidraMCP @@ -71,18 +71,29 @@ - - - - - maven-jar-plugin - 3.2.2 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + + + maven-jar-plugin + 3.2.2 src/main/resources/META-INF/MANIFEST.MF - GhidraMCP + HydraMCP **/App.class @@ -104,7 +115,7 @@ - GhidraMCP-${project.version} + HydraMCP-${project.version} false diff --git a/src/assembly/ghidra-extension.xml b/src/assembly/ghidra-extension.xml index 5941ca0..24cc449 100644 --- a/src/assembly/ghidra-extension.xml +++ b/src/assembly/ghidra-extension.xml @@ -24,17 +24,17 @@ extension.properties Module.manifest - GhidraMCP + HydraMCP - + ${project.build.directory} - GhidraMCP.jar + HydraMCP.jar - GhidraMCP/lib + HydraMCP/lib diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/HydraMCPPlugin.java similarity index 80% rename from src/main/java/com/lauriewired/GhidraMCPPlugin.java rename to src/main/java/com/lauriewired/HydraMCPPlugin.java index 78aba8e..bd28383 100644 --- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java +++ b/src/main/java/com/lauriewired/HydraMCPPlugin.java @@ -1,7 +1,8 @@ package com.lauriewired; -import ghidra.framework.plugintool.Plugin; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.*; +import ghidra.framework.plugintool.util.*; +import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.program.model.address.Address; import ghidra.program.model.address.GlobalNamespace; import ghidra.program.model.listing.*; @@ -24,9 +25,11 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; @PluginInfo( status = PluginStatus.RELEASED, @@ -35,23 +38,47 @@ import java.util.concurrent.atomic.AtomicBoolean; shortDescription = "HTTP server plugin", 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 activeInstances = new ConcurrentHashMap<>(); + private static final AtomicInteger nextPort = new AtomicInteger(8192); + private static final Object baseInstanceLock = new Object(); + private HttpServer server; + private int port; + private boolean isBaseInstance = false; - public GhidraMCPPlugin(PluginTool tool) { + public HydraMCPPlugin(PluginTool 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 { startServer(); - } - catch (IOException e) { - Msg.error(this, "Failed to start HTTP server", e); + } catch (IOException e) { + Msg.error(this, "Failed to start HTTP server on port " + port, 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 { - int port = 8080; server = HttpServer.create(new InetSocketAddress(port), 0); // Each listing endpoint uses offset & limit from query params: @@ -130,11 +157,44 @@ public class GhidraMCPPlugin extends Plugin { sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit)); }); + // Instance management endpoints + server.createContext("/instances", exchange -> { + StringBuilder sb = new StringBuilder(); + for (Map.Entry 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 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 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); new Thread(() -> { server.start(); - Msg.info(this, "GhidraMCP HTTP server started on port " + port); - }, "GhidraMCP-HTTP-Server").start(); + Msg.info(this, "HydraMCP HTTP server started on port " + port); + 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(); if (program == null) return "No program loaded"; DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); + try { + if (!decomp.openProgram(program)) { + return "Failed to initialize decompiler"; + } for (Function func : program.getFunctionManager().getFunctions(true)) { if (func.getName().equals(name)) { DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); if (result != null && result.decompileCompleted()) { return result.getDecompiledFunction().getC(); - } else { - return "Decompilation failed"; } + return "Decompilation failed"; } } return "Function not found"; + } finally { + decomp.dispose(); + } } 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 public void dispose() { if (server != null) { 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(); } } diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties index b0c5ea9..409fa12 100644 --- a/src/main/resources/extension.properties +++ b/src/main/resources/extension.properties @@ -1,4 +1,4 @@ -name=GhidraMCP +name=HydraMCP description=A plugin that runs an embedded HTTP server to expose program data. author=LaurieWired createdOn=2025-03-22