Merge branch 'auto-register'

This commit is contained in:
Teal Bauer 2025-03-30 01:21:19 +01:00
commit 07c94b2324
5 changed files with 261 additions and 24 deletions

1
.gitignore vendored
View File

@ -49,3 +49,4 @@ mvnw.cmd
hs_err_pid*
replay_pid*
/__pycache__

View File

@ -57,6 +57,8 @@ GhydraMCP combines a Ghidra plugin with an MCP server to provide a comprehensive
- Run multiple Ghidra instances simultaneously
- Analyze different binaries in parallel
- Connect to specific instances using port numbers
- Auto-discovery of running Ghidra instances
- Instance metadata with project and file information
## Program Navigation
@ -86,6 +88,8 @@ First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/rel
> (HydraMCPPlugin) Plugin loaded on port 8193
> (HydraMCPPlugin) HydraMCP HTTP server started on port 8193
> ```
>
> GhydraMCP now includes auto-discovery of running Ghidra instances, so manually registering each instance is typically not necessary. The MCP bridge will automatically discover and register instances on startup and periodically check for new ones.
Video Installation Guide:
@ -116,6 +120,7 @@ Theoretically, any MCP client should work with GhydraMCP. Two examples are given
- `list_instances`: List active Ghidra instances (no params)
- `register_instance`: Register new instance (params: port, url)
- `unregister_instance`: Remove instance (params: port)
- `discover_instances`: Auto-discover running instances (params: host [optional])
**Example Usage**:
```python
@ -125,6 +130,10 @@ client.use_tool("ghydra", "decompile_function", {"name": "main"})
# Instance management
client.use_tool("ghydra", "register_instance", {"port": 8192, "url": "http://localhost:8192/"})
client.use_tool("ghydra", "register_instance", {"port": 8193})
# Auto-discover instances
client.use_tool("ghydra", "discover_instances") # Default host
client.use_tool("ghydra", "discover_instances", {"host": "192.168.1.10"}) # Custom host
```
## Client Setup
@ -160,26 +169,53 @@ Below is an example of how an AI assistant might interact with multiple Ghidra i
**User:**
```
Register the following instances with the ghydra mcp tool:
port 8192 = application.exe
port 8193 = firmware.bin
Check which Ghidra instances are currently running.
```
**Assistant:**
```
I'll help you analyze these binaries. Let me register the instances with the ghydra mcp tool as requested.
I'll check which Ghidra instances are currently running by discovering any active instances.
View result from register_instance from ghydra (local){
`port`: 8192
View result from discover_instances from ghydra (local)
{
"found": 2,
"instances": [
{
"port": 8192,
"url": "http://localhost:8192",
"result": "Registered instance on port 8192 at http://localhost:8192"
},
{
"port": 8193,
"url": "http://localhost:8193",
"result": "Registered instance on port 8193 at http://localhost:8193"
}
]
}
Registered instance on port 8192 at http://localhost:8192
View result from register_instance from ghydra (local){
`port`: 8193
View result from list_instances from ghydra (local)
{
"instances": [
{
"port": 8192,
"url": "http://localhost:8192",
"project": "TestProject",
"file": "application.exe"
},
{
"port": 8193,
"url": "http://localhost:8193",
"project": "FirmwareAnalysis",
"file": "firmware.bin"
}
]
}
Registered instance on port 8193 at http://localhost:8193
Now that I've registered both instances, I'm ready to analyze the decompiled code.
I found two active Ghidra instances:
1. Port 8192: Analyzing "application.exe" in project "TestProject"
2. Port 8193: Analyzing "firmware.bin" in project "FirmwareAnalysis"
I'm ready to analyze these binaries. What would you like me to examine?
```
**User:**

View File

@ -7,35 +7,38 @@
# ///
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"
# Port ranges for scanning
QUICK_DISCOVERY_RANGE = range(8192, 8202) # Limited range for interactive/triggered discovery (10 ports)
FULL_DISCOVERY_RANGE = range(8192, 8212) # Wider range for background discovery (20 ports)
mcp = FastMCP("hydra-mcp")
# Get host from environment variable, command line, or use default
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
if len(sys.argv) > 1:
ghidra_host = sys.argv[1]
print(f"Using Ghidra host: {ghidra_host}")
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 +119,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 +141,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 +186,40 @@ 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(host: str = None) -> dict:
"""Auto-discover Ghidra instances by scanning ports (quick discovery with limited range)
Args:
host: Optional host to scan (defaults to configured ghidra_host)
"""
return _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5)
def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
"""Internal function to discover Ghidra instances by scanning ports"""
found_instances = []
scan_host = host if host is not None else ghidra_host
for port in port_range:
if port in active_instances:
continue
url = f"http://{scan_host}:{port}"
try:
test_url = f"{url}/instances"
response = requests.get(test_url, timeout=timeout) # 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 +281,49 @@ import os
def handle_sigint(signum, frame):
os._exit(0)
def periodic_discovery():
"""Periodically discover new instances"""
while True:
try:
# Use the full discovery range
_discover_instances(FULL_DISCOVERY_RANGE, timeout=0.5)
# 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;