diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 92227ad..8ca95cc 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -7,16 +7,19 @@ # /// 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" +DISCOVERY_PORT_RANGE = range(8192, 8300) # Port range to scan for Ghidra instances mcp = FastMCP("hydra-mcp") @@ -30,12 +33,13 @@ 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 +120,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 +142,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 +187,31 @@ 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() -> dict: + """Auto-discover Ghidra instances by scanning ports""" + found_instances = [] + + for port in DISCOVERY_PORT_RANGE: + if port in active_instances: + continue + + url = f"http://{ghidra_host}:{port}" + try: + test_url = f"{url}/instances" + response = requests.get(test_url, timeout=1) # 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 +273,47 @@ import os def handle_sigint(signum, frame): os._exit(0) +def periodic_discovery(): + """Periodically discover new instances""" + while True: + try: + discover_instances() + # 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;