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;