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 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,13 +142,42 @@ 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:
|
||||||
"""Unregister a Ghidra instance"""
|
"""Unregister a Ghidra instance"""
|
||||||
@ -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()
|
||||||
|
|||||||
7
pom.xml
7
pom.xml
@ -70,6 +70,13 @@
|
|||||||
<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>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
|
|||||||
@ -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,
|
||||||
@ -208,6 +213,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);
|
||||||
int port = parseIntOrDefault(params.get("port"), 0);
|
int port = parseIntOrDefault(params.get("port"), 0);
|
||||||
@ -551,6 +574,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
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);
|
||||||
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=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() {
|
private int findAvailablePort() {
|
||||||
int basePort = 8192;
|
int basePort = 8192;
|
||||||
int maxAttempts = 10;
|
int maxAttempts = 10;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user