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:
parent
c4744afa58
commit
86d04860bf
@ -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()
|
||||
|
||||
7
pom.xml
7
pom.xml
@ -69,6 +69,13 @@
|
||||
<scope>system</scope>
|
||||
<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>
|
||||
|
||||
@ -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<String, String> 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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user