diff --git a/bridge_mcp_ghidra.py b/bridge_mcp_ghidra.py
index 1c4477d..e9043e9 100644
--- a/bridge_mcp_ghidra.py
+++ b/bridge_mcp_ghidra.py
@@ -7,134 +7,184 @@
# ///
import sys
import requests
-
+from typing import Dict
+from threading import Lock
from mcp.server.fastmcp import FastMCP
-DEFAULT_GHIDRA_SERVER = "http://127.0.0.1:8080/"
-ghidra_server_url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_GHIDRA_SERVER
+# Track active Ghidra instances (port -> url)
+active_instances: Dict[int, str] = {}
+instances_lock = Lock()
+DEFAULT_GHIDRA_PORT = 8192
+DEFAULT_GHIDRA_HOST = "localhost"
-mcp = FastMCP("ghidra-mcp")
+mcp = FastMCP("hydra-mcp")
-def safe_get(endpoint: str, params: dict = None) -> list:
- """
- Perform a GET request with optional query parameters.
- """
+# Get host from command line or use default
+ghidra_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]
+
+ # Auto-register if not found but port is valid
+ if 8192 <= port <= 65535:
+ register_instance(port)
+ return active_instances[port]
+
+ return f"http://{ghidra_host}:{port}"
+
+def safe_get(port: int, endpoint: str, params: dict = None) -> list:
+ """Perform a GET request to a specific Ghidra instance"""
if params is None:
params = {}
- url = f"{ghidra_server_url}/{endpoint}"
+ url = f"{get_instance_url(port)}/{endpoint}"
try:
response = requests.get(url, params=params, timeout=5)
response.encoding = 'utf-8'
if response.ok:
return response.text.splitlines()
+ elif response.status_code == 404:
+ # Try falling back to default instance if this was a secondary instance
+ if port != DEFAULT_GHIDRA_PORT:
+ return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
+ return [f"Error {response.status_code}: {response.text.strip()}"]
else:
return [f"Error {response.status_code}: {response.text.strip()}"]
+ except requests.exceptions.ConnectionError:
+ # Instance may be down - try default instance if this was secondary
+ if port != DEFAULT_GHIDRA_PORT:
+ return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params)
+ return ["Error: Failed to connect to Ghidra instance"]
except Exception as e:
return [f"Request failed: {str(e)}"]
-def safe_post(endpoint: str, data: dict | str) -> str:
+def safe_post(port: int, endpoint: str, data: dict | str) -> str:
+ """Perform a POST request to a specific Ghidra instance"""
try:
+ url = f"{get_instance_url(port)}/{endpoint}"
if isinstance(data, dict):
- response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data, timeout=5)
+ response = requests.post(url, data=data, timeout=5)
else:
- response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data.encode("utf-8"), timeout=5)
+ response = requests.post(url, data=data.encode("utf-8"), timeout=5)
response.encoding = 'utf-8'
if response.ok:
return response.text.strip()
+ elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT:
+ # Try falling back to default instance
+ return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
else:
return f"Error {response.status_code}: {response.text.strip()}"
+ except requests.exceptions.ConnectionError:
+ if port != DEFAULT_GHIDRA_PORT:
+ return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
+ return "Error: Failed to connect to Ghidra instance"
except Exception as e:
return f"Request failed: {str(e)}"
+# Instance management tools
@mcp.tool()
-def list_methods(offset: int = 0, limit: int = 100) -> list:
- """
- List all function names in the program with pagination.
- """
- return safe_get("methods", {"offset": offset, "limit": limit})
+def list_instances() -> dict:
+ """List all active Ghidra instances"""
+ with instances_lock:
+ return {
+ "instances": [
+ {"port": port, "url": url}
+ for port, url in active_instances.items()
+ ]
+ }
@mcp.tool()
-def list_classes(offset: int = 0, limit: int = 100) -> list:
- """
- List all namespace/class names in the program with pagination.
- """
- return safe_get("classes", {"offset": offset, "limit": limit})
+def register_instance(port: int, url: str = None) -> str:
+ """Register a new Ghidra instance"""
+ if url is None:
+ url = f"http://{ghidra_host}:{port}"
+
+ # Verify instance is reachable before registering
+ try:
+ test_url = f"{url}/instances"
+ 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)}"
+
+ with instances_lock:
+ active_instances[port] = url
+ return f"Registered instance on port {port} at {url}"
@mcp.tool()
-def decompile_function(name: str) -> str:
- """
- Decompile a specific function by name and return the decompiled C code.
- """
- return safe_post("decompile", name)
+def unregister_instance(port: int) -> str:
+ """Unregister a Ghidra instance"""
+ with instances_lock:
+ if port in active_instances:
+ del active_instances[port]
+ return f"Unregistered instance on port {port}"
+ return f"No instance found on port {port}"
+
+# Updated tool implementations with port parameter
+@mcp.tool()
+def list_methods(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "methods", {"offset": offset, "limit": limit})
@mcp.tool()
-def rename_function(old_name: str, new_name: str) -> str:
- """
- Rename a function by its current name to a new user-defined name.
- """
- return safe_post("renameFunction", {"oldName": old_name, "newName": new_name})
+def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "classes", {"offset": offset, "limit": limit})
@mcp.tool()
-def rename_data(address: str, new_name: str) -> str:
- """
- Rename a data label at the specified address.
- """
- return safe_post("renameData", {"address": address, "newName": new_name})
+def decompile_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "") -> str:
+ return safe_post(port, "decompile", name)
@mcp.tool()
-def list_segments(offset: int = 0, limit: int = 100) -> list:
- """
- List all memory segments in the program with pagination.
- """
- return safe_get("segments", {"offset": offset, "limit": limit})
+def rename_function(port: int = DEFAULT_GHIDRA_PORT, old_name: str = "", new_name: str = "") -> str:
+ return safe_post(port, "renameFunction", {"oldName": old_name, "newName": new_name})
@mcp.tool()
-def list_imports(offset: int = 0, limit: int = 100) -> list:
- """
- List imported symbols in the program with pagination.
- """
- return safe_get("imports", {"offset": offset, "limit": limit})
+def rename_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str:
+ return safe_post(port, "renameData", {"address": address, "newName": new_name})
@mcp.tool()
-def list_exports(offset: int = 0, limit: int = 100) -> list:
- """
- List exported functions/symbols with pagination.
- """
- return safe_get("exports", {"offset": offset, "limit": limit})
+def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "segments", {"offset": offset, "limit": limit})
@mcp.tool()
-def list_namespaces(offset: int = 0, limit: int = 100) -> list:
- """
- List all non-global namespaces in the program with pagination.
- """
- return safe_get("namespaces", {"offset": offset, "limit": limit})
+def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "imports", {"offset": offset, "limit": limit})
@mcp.tool()
-def list_data_items(offset: int = 0, limit: int = 100) -> list:
- """
- List defined data labels and their values with pagination.
- """
- return safe_get("data", {"offset": offset, "limit": limit})
+def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "exports", {"offset": offset, "limit": limit})
@mcp.tool()
-def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list:
- """
- Search for functions whose name contains the given substring.
- """
+def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "namespaces", {"offset": offset, "limit": limit})
+
+@mcp.tool()
+def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
+ return safe_get(port, "data", {"offset": offset, "limit": limit})
+
+@mcp.tool()
+def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list:
if not query:
return ["Error: query string is required"]
- return safe_get("searchFunctions", {"query": query, "offset": offset, "limit": limit})
-
+ return safe_get(port, "searchFunctions", {"query": query, "offset": offset, "limit": limit})
+# Handle graceful shutdown
import signal
-
import os
def handle_sigint(signum, frame):
os._exit(0)
if __name__ == "__main__":
+ # Auto-register default instance
+ register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
+
signal.signal(signal.SIGINT, handle_sigint)
mcp.run()
diff --git a/lib/Base.jar b/lib/Base.jar
new file mode 100755
index 0000000..d347dbd
Binary files /dev/null and b/lib/Base.jar differ
diff --git a/lib/Decompiler.jar b/lib/Decompiler.jar
new file mode 100755
index 0000000..6da4136
Binary files /dev/null and b/lib/Decompiler.jar differ
diff --git a/lib/Docking.jar b/lib/Docking.jar
new file mode 100755
index 0000000..2e58f2e
Binary files /dev/null and b/lib/Docking.jar differ
diff --git a/lib/Generic.jar b/lib/Generic.jar
new file mode 100755
index 0000000..1755e14
Binary files /dev/null and b/lib/Generic.jar differ
diff --git a/lib/Project.jar b/lib/Project.jar
new file mode 100755
index 0000000..fb93904
Binary files /dev/null and b/lib/Project.jar differ
diff --git a/lib/SoftwareModeling.jar b/lib/SoftwareModeling.jar
new file mode 100755
index 0000000..b9ed858
Binary files /dev/null and b/lib/SoftwareModeling.jar differ
diff --git a/lib/Utility.jar b/lib/Utility.jar
new file mode 100755
index 0000000..4fe9673
Binary files /dev/null and b/lib/Utility.jar differ
diff --git a/pom.xml b/pom.xml
index ec29081..f7f5dc4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,11 +4,11 @@
4.0.0
com.lauriewired
- GhidraMCP
+ HydraMCP
jar
- 1.0-SNAPSHOT
- GhidraMCP
- http://maven.apache.org
+ 1.1
+ HydraMCP
+ https://github.com/LaurieWired/GhidraMCP
@@ -71,18 +71,29 @@
-
-
-
-
- maven-jar-plugin
- 3.2.2
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 11
+ 11
+
+
+
+
+
+ maven-jar-plugin
+ 3.2.2
src/main/resources/META-INF/MANIFEST.MF
- GhidraMCP
+ HydraMCP
**/App.class
@@ -104,7 +115,7 @@
- GhidraMCP-${project.version}
+ HydraMCP-${project.version}
false
diff --git a/src/assembly/ghidra-extension.xml b/src/assembly/ghidra-extension.xml
index 5941ca0..24cc449 100644
--- a/src/assembly/ghidra-extension.xml
+++ b/src/assembly/ghidra-extension.xml
@@ -24,17 +24,17 @@
extension.properties
Module.manifest
- GhidraMCP
+ HydraMCP
-
+
${project.build.directory}
- GhidraMCP.jar
+ HydraMCP.jar
- GhidraMCP/lib
+ HydraMCP/lib
diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/HydraMCPPlugin.java
similarity index 80%
rename from src/main/java/com/lauriewired/GhidraMCPPlugin.java
rename to src/main/java/com/lauriewired/HydraMCPPlugin.java
index 78aba8e..bd28383 100644
--- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java
+++ b/src/main/java/com/lauriewired/HydraMCPPlugin.java
@@ -1,7 +1,8 @@
package com.lauriewired;
-import ghidra.framework.plugintool.Plugin;
-import ghidra.framework.plugintool.PluginTool;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.*;
+import ghidra.framework.main.ApplicationLevelPlugin;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.GlobalNamespace;
import ghidra.program.model.listing.*;
@@ -24,9 +25,11 @@ import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
+import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.util.*;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
@PluginInfo(
status = PluginStatus.RELEASED,
@@ -35,23 +38,47 @@ import java.util.concurrent.atomic.AtomicBoolean;
shortDescription = "HTTP server plugin",
description = "Starts an embedded HTTP server to expose program data."
)
-public class GhidraMCPPlugin extends Plugin {
+public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
+ private static final Map activeInstances = new ConcurrentHashMap<>();
+ private static final AtomicInteger nextPort = new AtomicInteger(8192);
+ private static final Object baseInstanceLock = new Object();
+
private HttpServer server;
+ private int port;
+ private boolean isBaseInstance = false;
- public GhidraMCPPlugin(PluginTool tool) {
+ public HydraMCPPlugin(PluginTool tool) {
super(tool);
- Msg.info(this, "GhidraMCPPlugin loaded!");
+
+ // Find available port
+ this.port = findAvailablePort();
+ activeInstances.put(port, this);
+
+ // Check if we should be base instance
+ synchronized (baseInstanceLock) {
+ if (port == 8192 || activeInstances.get(8192) == null) {
+ this.isBaseInstance = true;
+ Msg.info(this, "Starting as base instance on port " + port);
+ }
+ }
+
+ // Log to both console and log file
+ Msg.info(this, "HydraMCPPlugin loaded on port " + port);
+ System.out.println("[HydraMCP] Plugin loaded on port " + port);
+
try {
startServer();
- }
- catch (IOException e) {
- Msg.error(this, "Failed to start HTTP server", e);
+ } catch (IOException e) {
+ Msg.error(this, "Failed to start HTTP server on port " + port, e);
+ if (e.getMessage().contains("Address already in use")) {
+ Msg.showError(this, null, "Port Conflict",
+ "Port " + port + " is already in use. Please specify a different port with -Dghidra.mcp.port=NEW_PORT");
+ }
}
}
private void startServer() throws IOException {
- int port = 8080;
server = HttpServer.create(new InetSocketAddress(port), 0);
// Each listing endpoint uses offset & limit from query params:
@@ -130,11 +157,44 @@ public class GhidraMCPPlugin extends Plugin {
sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
});
+ // Instance management endpoints
+ server.createContext("/instances", exchange -> {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry entry : activeInstances.entrySet()) {
+ sb.append(entry.getKey()).append(": ")
+ .append(entry.getValue().isBaseInstance ? "base" : "secondary")
+ .append("\n");
+ }
+ sendResponse(exchange, sb.toString());
+ });
+
+ server.createContext("/registerInstance", exchange -> {
+ Map params = parsePostParams(exchange);
+ int port = parseIntOrDefault(params.get("port"), 0);
+ if (port > 0) {
+ sendResponse(exchange, "Instance registered on port " + port);
+ } else {
+ sendResponse(exchange, "Invalid port number");
+ }
+ });
+
+ server.createContext("/unregisterInstance", exchange -> {
+ Map params = parsePostParams(exchange);
+ int port = parseIntOrDefault(params.get("port"), 0);
+ if (port > 0 && activeInstances.containsKey(port)) {
+ activeInstances.remove(port);
+ sendResponse(exchange, "Unregistered instance on port " + port);
+ } else {
+ sendResponse(exchange, "No instance found on port " + port);
+ }
+ });
+
server.setExecutor(null);
new Thread(() -> {
server.start();
- Msg.info(this, "GhidraMCP HTTP server started on port " + port);
- }, "GhidraMCP-HTTP-Server").start();
+ Msg.info(this, "HydraMCP HTTP server started on port " + port);
+ System.out.println("[HydraMCP] HTTP server started on port " + port);
+ }, "HydraMCP-HTTP-Server").start();
}
// ----------------------------------------------------------------------------------
@@ -278,19 +338,24 @@ public class GhidraMCPPlugin extends Plugin {
Program program = getCurrentProgram();
if (program == null) return "No program loaded";
DecompInterface decomp = new DecompInterface();
- decomp.openProgram(program);
+ try {
+ if (!decomp.openProgram(program)) {
+ return "Failed to initialize decompiler";
+ }
for (Function func : program.getFunctionManager().getFunctions(true)) {
if (func.getName().equals(name)) {
DecompileResults result =
decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
if (result != null && result.decompileCompleted()) {
return result.getDecompiledFunction().getC();
- } else {
- return "Decompilation failed";
}
+ return "Decompilation failed";
}
}
return "Function not found";
+ } finally {
+ decomp.dispose();
+ }
}
private boolean renameFunction(String oldName, String newName) {
@@ -455,12 +520,31 @@ public class GhidraMCPPlugin extends Plugin {
}
}
+ private int findAvailablePort() {
+ int basePort = 8192;
+ int maxAttempts = 10;
+
+ for (int attempt = 0; attempt < maxAttempts; attempt++) {
+ int candidate = basePort + attempt;
+ if (!activeInstances.containsKey(candidate)) {
+ try (ServerSocket s = new ServerSocket(candidate)) {
+ return candidate;
+ } catch (IOException e) {
+ continue;
+ }
+ }
+ }
+ throw new RuntimeException("Could not find available port after " + maxAttempts + " attempts");
+ }
+
@Override
public void dispose() {
if (server != null) {
server.stop(0);
- Msg.info(this, "HTTP server stopped.");
+ Msg.info(this, "HTTP server stopped on port " + port);
+ System.out.println("[HydraMCP] HTTP server stopped on port " + port);
}
+ activeInstances.remove(port);
super.dispose();
}
}
diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties
index b0c5ea9..409fa12 100644
--- a/src/main/resources/extension.properties
+++ b/src/main/resources/extension.properties
@@ -1,4 +1,4 @@
-name=GhidraMCP
+name=HydraMCP
description=A plugin that runs an embedded HTTP server to expose program data.
author=LaurieWired
createdOn=2025-03-22