diff --git a/.gitignore b/.gitignore index 0701e63..c745086 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ mvnw.cmd hs_err_pid* replay_pid* +/__pycache__ diff --git a/README.md b/README.md index 45b0a84..5462eb3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ GhydraMCP combines a Ghidra plugin with an MCP server to provide a comprehensive - Run multiple Ghidra instances simultaneously - Analyze different binaries in parallel - Connect to specific instances using port numbers +- Auto-discovery of running Ghidra instances +- Instance metadata with project and file information ## Program Navigation @@ -86,6 +88,8 @@ First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/rel > (HydraMCPPlugin) Plugin loaded on port 8193 > (HydraMCPPlugin) HydraMCP HTTP server started on port 8193 > ``` +> +> GhydraMCP now includes auto-discovery of running Ghidra instances, so manually registering each instance is typically not necessary. The MCP bridge will automatically discover and register instances on startup and periodically check for new ones. Video Installation Guide: @@ -116,6 +120,7 @@ Theoretically, any MCP client should work with GhydraMCP. Two examples are given - `list_instances`: List active Ghidra instances (no params) - `register_instance`: Register new instance (params: port, url) - `unregister_instance`: Remove instance (params: port) +- `discover_instances`: Auto-discover running instances (params: host [optional]) **Example Usage**: ```python @@ -125,6 +130,10 @@ client.use_tool("ghydra", "decompile_function", {"name": "main"}) # Instance management client.use_tool("ghydra", "register_instance", {"port": 8192, "url": "http://localhost:8192/"}) client.use_tool("ghydra", "register_instance", {"port": 8193}) + +# Auto-discover instances +client.use_tool("ghydra", "discover_instances") # Default host +client.use_tool("ghydra", "discover_instances", {"host": "192.168.1.10"}) # Custom host ``` ## Client Setup @@ -160,26 +169,53 @@ Below is an example of how an AI assistant might interact with multiple Ghidra i **User:** ``` -Register the following instances with the ghydra mcp tool: -port 8192 = application.exe -port 8193 = firmware.bin +Check which Ghidra instances are currently running. ``` **Assistant:** ``` -I'll help you analyze these binaries. Let me register the instances with the ghydra mcp tool as requested. +I'll check which Ghidra instances are currently running by discovering any active instances. -View result from register_instance from ghydra (local){ - `port`: 8192 +View result from discover_instances from ghydra (local) +{ + "found": 2, + "instances": [ + { + "port": 8192, + "url": "http://localhost:8192", + "result": "Registered instance on port 8192 at http://localhost:8192" + }, + { + "port": 8193, + "url": "http://localhost:8193", + "result": "Registered instance on port 8193 at http://localhost:8193" + } + ] } -Registered instance on port 8192 at http://localhost:8192 -View result from register_instance from ghydra (local){ - `port`: 8193 +View result from list_instances from ghydra (local) +{ + "instances": [ + { + "port": 8192, + "url": "http://localhost:8192", + "project": "TestProject", + "file": "application.exe" + }, + { + "port": 8193, + "url": "http://localhost:8193", + "project": "FirmwareAnalysis", + "file": "firmware.bin" + } + ] } -Registered instance on port 8193 at http://localhost:8193 -Now that I've registered both instances, I'm ready to analyze the decompiled code. +I found two active Ghidra instances: +1. Port 8192: Analyzing "application.exe" in project "TestProject" +2. Port 8193: Analyzing "firmware.bin" in project "FirmwareAnalysis" + +I'm ready to analyze these binaries. What would you like me to examine? ``` **User:** diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 92227ad..b376960 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -7,35 +7,38 @@ # /// import os import sys +import time import requests +import threading from typing import Dict from threading import Lock from mcp.server.fastmcp import FastMCP -# Track active Ghidra instances (port -> url) -active_instances: Dict[int, str] = {} +# Track active Ghidra instances (port -> info dict) +active_instances: Dict[int, dict] = {} instances_lock = Lock() DEFAULT_GHIDRA_PORT = 8192 DEFAULT_GHIDRA_HOST = "localhost" +# Port ranges for scanning +QUICK_DISCOVERY_RANGE = range(8192, 8202) # Limited range for interactive/triggered discovery (10 ports) +FULL_DISCOVERY_RANGE = range(8192, 8212) # Wider range for background discovery (20 ports) mcp = FastMCP("hydra-mcp") -# Get host from environment variable, command line, or use default ghidra_host = os.environ.get("GHIDRA_HYDRA_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] + return active_instances[port]["url"] # Auto-register if not found but port is valid if 8192 <= port <= 65535: register_instance(port) - return active_instances[port] + if port in active_instances: + return active_instances[port]["url"] return f"http://{ghidra_host}:{port}" @@ -116,8 +119,13 @@ def list_instances() -> dict: with instances_lock: return { "instances": [ - {"port": port, "url": url} - for port, url in active_instances.items() + { + "port": port, + "url": info["url"], + "project": info.get("project", ""), + "file": info.get("file", "") + } + for port, info in active_instances.items() ] } @@ -133,12 +141,41 @@ def register_instance(port: int, url: str = None) -> str: response = requests.get(test_url, timeout=2) if not response.ok: return f"Error: Instance at {url} is not responding properly" + + # Try to get project info + project_info = {"url": url} + + try: + info_url = f"{url}/info" + info_response = requests.get(info_url, timeout=2) + if info_response.ok: + try: + # Parse JSON response + info_data = info_response.json() + + # Extract relevant information + project_info["project"] = info_data.get("project", "Unknown") + + # Handle file information which is nested + file_info = info_data.get("file", {}) + if file_info: + project_info["file"] = file_info.get("name", "") + project_info["path"] = file_info.get("path", "") + project_info["architecture"] = file_info.get("architecture", "") + project_info["endian"] = file_info.get("endian", "") + except ValueError: + # Not valid JSON + pass + except Exception: + # Non-critical, continue with registration even if project info fails + pass + + with instances_lock: + active_instances[port] = project_info + + return f"Registered instance on port {port} at {url}" 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 unregister_instance(port: int) -> str: @@ -149,6 +186,40 @@ def unregister_instance(port: int) -> str: return f"Unregistered instance on port {port}" return f"No instance found on port {port}" +@mcp.tool() +def discover_instances(host: str = None) -> dict: + """Auto-discover Ghidra instances by scanning ports (quick discovery with limited range) + + Args: + host: Optional host to scan (defaults to configured ghidra_host) + """ + return _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5) + +def _discover_instances(port_range, host=None, timeout=0.5) -> dict: + """Internal function to discover Ghidra instances by scanning ports""" + found_instances = [] + scan_host = host if host is not None else ghidra_host + + for port in port_range: + if port in active_instances: + continue + + url = f"http://{scan_host}:{port}" + try: + test_url = f"{url}/instances" + response = requests.get(test_url, timeout=timeout) # Short timeout for scanning + if response.ok: + result = register_instance(port, url) + found_instances.append({"port": port, "url": url, "result": result}) + except requests.exceptions.RequestException: + # Instance not available, just continue + continue + + return { + "found": len(found_instances), + "instances": found_instances + } + # Updated tool implementations with port parameter from urllib.parse import quote @@ -210,9 +281,49 @@ import os def handle_sigint(signum, frame): os._exit(0) +def periodic_discovery(): + """Periodically discover new instances""" + while True: + try: + # Use the full discovery range + _discover_instances(FULL_DISCOVERY_RANGE, timeout=0.5) + + # Also check if any existing instances are down + with instances_lock: + ports_to_remove = [] + for port, info in active_instances.items(): + url = info["url"] + try: + response = requests.get(f"{url}/instances", timeout=1) + if not response.ok: + ports_to_remove.append(port) + except requests.exceptions.RequestException: + ports_to_remove.append(port) + + # Remove any instances that are down + for port in ports_to_remove: + del active_instances[port] + print(f"Removed unreachable instance on port {port}") + except Exception as e: + print(f"Error in periodic discovery: {e}") + + # Sleep for 30 seconds before next scan + time.sleep(30) + if __name__ == "__main__": # Auto-register default instance register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}") + # Auto-discover other instances + discover_instances() + + # Start periodic discovery in background thread + discovery_thread = threading.Thread( + target=periodic_discovery, + daemon=True, + name="GhydraMCP-Discovery" + ) + discovery_thread.start() + signal.signal(signal.SIGINT, handle_sigint) mcp.run() diff --git a/pom.xml b/pom.xml index aa8a9cb..6f18125 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,13 @@ system ${ghidra.jar.location}/Base.jar + + + + com.googlecode.json-simple + json-simple + 1.1.1 + diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index bc07381..72dc84a 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -11,6 +11,8 @@ import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileResults; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.ProgramManager; +import ghidra.framework.model.Project; +import ghidra.framework.model.DomainFile; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.util.Msg; @@ -30,6 +32,9 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; +// For JSON response handling +import org.json.simple.JSONObject; + @PluginInfo( status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, @@ -207,6 +212,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { } sendResponse(exchange, sb.toString()); }); + + // Info endpoints - both root and /info for flexibility + server.createContext("/info", exchange -> { + if ("GET".equals(exchange.getRequestMethod())) { + sendJsonResponse(exchange, getProjectInfo()); + } else { + exchange.sendResponseHeaders(405, -1); // Method Not Allowed + } + }); + + // Root endpoint also returns project info + server.createContext("/", exchange -> { + if ("GET".equals(exchange.getRequestMethod())) { + sendJsonResponse(exchange, getProjectInfo()); + } else { + exchange.sendResponseHeaders(405, -1); // Method Not Allowed + } + }); server.createContext("/registerInstance", exchange -> { Map params = parsePostParams(exchange); @@ -550,6 +573,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { ProgramManager pm = tool.getService(ProgramManager.class); return pm != null ? pm.getCurrentProgram() : null; } + + /** + * Get information about the current project and open file in JSON format + */ + private JSONObject getProjectInfo() { + JSONObject info = new JSONObject(); + Program program = getCurrentProgram(); + + // Get project information if available + Project project = tool.getProject(); + if (project != null) { + info.put("project", project.getName()); + } else { + info.put("project", "Unknown"); + } + + // Create file information object + JSONObject fileInfo = new JSONObject(); + + // Get current file information if available + if (program != null) { + // Basic info + fileInfo.put("name", program.getName()); + + // Try to get more detailed info + DomainFile domainFile = program.getDomainFile(); + if (domainFile != null) { + fileInfo.put("path", domainFile.getPathname()); + } + + // Add any additional file info we might want + fileInfo.put("architecture", program.getLanguage().getProcessor().toString()); + fileInfo.put("endian", program.getLanguage().isBigEndian() ? "big" : "little"); + + info.put("file", fileInfo); + } else { + info.put("file", null); + info.put("status", "No file open"); + } + + // Add server metadata + info.put("port", port); + info.put("isBaseInstance", isBaseInstance); + + return info; + } private void sendResponse(HttpExchange exchange, String response) throws IOException { byte[] bytes = response.getBytes(StandardCharsets.UTF_8); @@ -559,6 +628,19 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { os.write(bytes); } } + + /** + * Send a JSON response to the client + */ + private void sendJsonResponse(HttpExchange exchange, JSONObject json) throws IOException { + String jsonString = json.toJSONString(); + byte[] bytes = jsonString.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } private int findAvailablePort() { int basePort = 8192;