Add auto-discovery of Ghidra instances and JSON project info endpoint

- Modified bridge_mcp_hydra.py to auto-discover GhydraMCP plugin instances on ports 8192-8299
- Added periodic background thread to maintain discovered instances list
- Added project and binary file information to instance reporting
- Added JSON-based info endpoint in GhydraMCP plugin
- Added json-simple dependency to support JSON responses
This commit is contained in:
Teal Bauer 2025-03-30 01:06:04 +01:00
parent c4744afa58
commit 86d04860bf
3 changed files with 200 additions and 10 deletions

View File

@ -7,16 +7,19 @@
# /// # ///
import os import os
import sys import sys
import time
import requests import requests
import threading
from typing import Dict from typing import Dict
from threading import Lock from threading import Lock
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
# Track active Ghidra instances (port -> url) # Track active Ghidra instances (port -> info dict)
active_instances: Dict[int, str] = {} active_instances: Dict[int, dict] = {}
instances_lock = Lock() instances_lock = Lock()
DEFAULT_GHIDRA_PORT = 8192 DEFAULT_GHIDRA_PORT = 8192
DEFAULT_GHIDRA_HOST = "localhost" DEFAULT_GHIDRA_HOST = "localhost"
DISCOVERY_PORT_RANGE = range(8192, 8300) # Port range to scan for Ghidra instances
mcp = FastMCP("hydra-mcp") mcp = FastMCP("hydra-mcp")
@ -30,12 +33,13 @@ def get_instance_url(port: int) -> str:
"""Get URL for a Ghidra instance by port""" """Get URL for a Ghidra instance by port"""
with instances_lock: with instances_lock:
if port in active_instances: if port in active_instances:
return active_instances[port] return active_instances[port]["url"]
# Auto-register if not found but port is valid # Auto-register if not found but port is valid
if 8192 <= port <= 65535: if 8192 <= port <= 65535:
register_instance(port) register_instance(port)
return active_instances[port] if port in active_instances:
return active_instances[port]["url"]
return f"http://{ghidra_host}:{port}" return f"http://{ghidra_host}:{port}"
@ -116,8 +120,13 @@ def list_instances() -> dict:
with instances_lock: with instances_lock:
return { return {
"instances": [ "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) response = requests.get(test_url, timeout=2)
if not response.ok: if not response.ok:
return f"Error: Instance at {url} is not responding properly" 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: except Exception as e:
return f"Error: Could not connect to instance at {url}: {str(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 unregister_instance(port: int) -> str: 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"Unregistered instance on port {port}"
return f"No instance found 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 # Updated tool implementations with port parameter
from urllib.parse import quote from urllib.parse import quote
@ -210,9 +273,47 @@ import os
def handle_sigint(signum, frame): def handle_sigint(signum, frame):
os._exit(0) 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__": if __name__ == "__main__":
# Auto-register default instance # Auto-register default instance
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}") 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) signal.signal(signal.SIGINT, handle_sigint)
mcp.run() mcp.run()

View File

@ -69,6 +69,13 @@
<scope>system</scope> <scope>system</scope>
<systemPath>${ghidra.jar.location}/Base.jar</systemPath> <systemPath>${ghidra.jar.location}/Base.jar</systemPath>
</dependency> </dependency>
<!-- JSON Simple for JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Test dependencies --> <!-- Test dependencies -->
<dependency> <dependency>

View File

@ -11,6 +11,8 @@ import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults; import ghidra.app.decompiler.DecompileResults;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.ProgramManager; import ghidra.app.services.ProgramManager;
import ghidra.framework.model.Project;
import ghidra.framework.model.DomainFile;
import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -30,6 +32,9 @@ import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.*; import java.util.concurrent.atomic.*;
// For JSON response handling
import org.json.simple.JSONObject;
@PluginInfo( @PluginInfo(
status = PluginStatus.RELEASED, status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME, packageName = ghidra.app.DeveloperPluginPackage.NAME,
@ -207,6 +212,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
} }
sendResponse(exchange, sb.toString()); 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 -> { server.createContext("/registerInstance", exchange -> {
Map<String, String> params = parsePostParams(exchange); Map<String, String> params = parsePostParams(exchange);
@ -550,6 +573,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
ProgramManager pm = tool.getService(ProgramManager.class); ProgramManager pm = tool.getService(ProgramManager.class);
return pm != null ? pm.getCurrentProgram() : null; 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 { private void sendResponse(HttpExchange exchange, String response) throws IOException {
byte[] bytes = response.getBytes(StandardCharsets.UTF_8); byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
@ -559,6 +628,19 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
os.write(bytes); 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() { private int findAvailablePort() {
int basePort = 8192; int basePort = 8192;