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 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"
except Exception as e:
return f"Error: Could not connect to instance at {url}: {str(e)}"
# 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] = url
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)}"
@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()

View File

@ -70,6 +70,13 @@
<systemPath>${ghidra.jar.location}/Base.jar</systemPath>
</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 -->
<dependency>
<groupId>junit</groupId>

View File

@ -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,
@ -208,6 +213,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<String, String> params = parsePostParams(exchange);
int port = parseIntOrDefault(params.get("port"), 0);
@ -551,6 +574,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
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);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
@ -560,6 +629,19 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
}
/**
* 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;
int maxAttempts = 10;