mcghidra/src/main/java/eu/starsong/ghidra/MCGhidraPlugin.java
Ryan Malloy 1143489924
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
refactor: Rename project from ghydramcp to mcghidra
- Rename src/ghydramcp → src/mcghidra
- Rename GhydraMCPPlugin.java → MCGhidraPlugin.java
- Update all imports, class names, and references
- Update pyproject.toml package name and script entry
- Update Docker image names and container prefixes
- Update environment variables: GHYDRA_* → MCGHIDRA_*
- Update all documentation references
2026-02-07 02:13:53 -07:00

475 lines
21 KiB
Java

package eu.starsong.ghidra;
// Imports for refactored structure
import eu.starsong.ghidra.api.*;
import eu.starsong.ghidra.endpoints.*;
import eu.starsong.ghidra.util.*;
import eu.starsong.ghidra.model.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
// For JSON response handling
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.Headers;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.ProgramManager;
import ghidra.framework.main.ApplicationLevelPlugin;
import ghidra.framework.model.Project;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME,
category = PluginCategoryNames.ANALYSIS,
shortDescription = "MCGhidra Plugin for AI Analysis",
description = "Exposes program data via HATEOAS HTTP API for AI-assisted reverse engineering with MCP (Model Context Protocol).",
servicesRequired = { ProgramManager.class }
)
public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
// Made public static to be accessible by InstanceEndpoints
public static final Map<Integer, MCGhidraPlugin> activeInstances = new ConcurrentHashMap<>();
private static final Object baseInstanceLock = new Object();
private HttpServer server;
private int port;
private boolean isBaseInstance = false;
/**
* Constructor for MCGhidra Plugin.
* @param tool The Ghidra PluginTool
*/
public MCGhidraPlugin(PluginTool tool) {
super(tool);
this.port = findAvailablePort();
activeInstances.put(port, this);
synchronized (baseInstanceLock) {
if (port == ApiConstants.DEFAULT_PORT || activeInstances.get(ApiConstants.DEFAULT_PORT) == null) {
this.isBaseInstance = true;
Msg.info(this, "Starting as base instance on port " + port);
}
}
Msg.info(this, "MCGhidraPlugin loaded on port " + port);
System.out.println("[MCGhidra] Plugin loaded on port " + port);
try {
startServer();
} 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");
}
}
}
/**
* Starts the HTTP server and registers all endpoints
*/
private void startServer() throws IOException {
server = HttpServer.create(new InetSocketAddress(port), 0);
// Use a cached thread pool for better performance with multiple concurrent requests
server.setExecutor(Executors.newCachedThreadPool());
// --- Register Endpoints ---
Program currentProgram = getCurrentProgram(); // Get program once
// Register Meta Endpoints (these don't require a program)
registerMetaEndpoints(server);
// Register endpoints that don't require a program
registerProjectEndpoints(server);
new InstanceEndpoints(currentProgram, port, activeInstances).registerEndpoints(server);
// Register Resource Endpoints that require a program
registerProgramDependentEndpoints(server);
// Register Root Endpoint (should be last to include links to all other endpoints)
registerRootEndpoint(server);
new Thread(() -> {
server.start();
Msg.info(this, "MCGhidra HTTP server started on port " + port);
System.out.println("[MCGhidra] HTTP server started on port " + port);
}, "MCGhidra-HTTP-Server").start();
}
/**
* Register all endpoints that require a program to function.
* This method always registers all endpoints, even when no program is loaded.
* The endpoints will check for program availability at runtime when they're called.
*
* IMPORTANT: Endpoints are registered in order from most specific to least specific
* to ensure proper URL path matching.
*/
private void registerProgramDependentEndpoints(HttpServer server) {
// Register all endpoints without checking for a current program
// The endpoints will check for the current program at runtime when they're called
Msg.info(this, "Registering program-dependent endpoints. Programs will be checked at runtime.");
Program currentProgram = getCurrentProgram();
Msg.info(this, "Current program at registration time: " + (currentProgram != null ? currentProgram.getName() : "none"));
new FunctionEndpoints(currentProgram, port, tool).registerEndpoints(server);
new VariableEndpoints(currentProgram, port, tool).registerEndpoints(server);
new ClassEndpoints(currentProgram, port, tool).registerEndpoints(server);
new SegmentEndpoints(currentProgram, port, tool).registerEndpoints(server);
new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server);
new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server);
new DataEndpoints(currentProgram, port, tool).registerEndpoints(server);
new StructEndpoints(currentProgram, port, tool).registerEndpoints(server);
new MemoryEndpoints(currentProgram, port, tool).registerEndpoints(server);
new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server);
new AnalysisEndpoints(currentProgram, port, tool).registerEndpoints(server);
new ProgramEndpoints(currentProgram, port, tool).registerEndpoints(server);
Msg.info(this, "Registered program-dependent endpoints. Programs will be checked at runtime.");
}
/**
* Register additional endpoints for current program state
*/
private void registerProgramStateEndpoints(HttpServer server) {
// Any additional endpoints can be added here if needed
// But prefer to use the HATEOAS endpoints in ProgramEndpoints, FunctionEndpoints, etc.
}
// --- Endpoint Registration Methods ---
/**
* Register meta endpoints that provide plugin information
*/
private void registerMetaEndpoints(HttpServer server) {
// Plugin version endpoint
server.createContext("/plugin-version", exchange -> {
try {
if ("GET".equals(exchange.getRequestMethod())) {
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(Map.of(
"plugin_version", ApiConstants.PLUGIN_VERSION,
"api_version", ApiConstants.API_VERSION
))
.addLink("self", "/plugin-version")
.addLink("root", "/");
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} else {
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
}
} catch (IOException e) {
Msg.error(this, "Error handling /plugin-version", e);
}
});
// Info endpoint
server.createContext("/info", exchange -> {
try {
Map<String, Object> infoData = new HashMap<>();
infoData.put("isBaseInstance", isBaseInstance);
Program program = getCurrentProgram();
if (program != null) {
infoData.put("file", program.getName());
infoData.put("architecture", program.getLanguage().getLanguageID().getIdAsString());
infoData.put("processor", program.getLanguage().getProcessor().toString());
infoData.put("addressSize", program.getAddressFactory().getDefaultAddressSpace().getSize());
infoData.put("creationDate", program.getCreationDate());
infoData.put("executable", program.getExecutablePath());
}
Project project = tool.getProject();
if (project != null) {
infoData.put("project", project.getName());
infoData.put("projectLocation", project.getProjectLocator().toString());
}
// Add server details
infoData.put("serverPort", port);
infoData.put("serverStartTime", System.currentTimeMillis());
infoData.put("instanceCount", activeInstances.size());
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(infoData)
.addLink("self", "/info")
.addLink("root", "/")
.addLink("instances", "/instances");
// Add program link if available
if (program != null) {
builder.addLink("program", "/program");
}
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} catch (Exception e) {
Msg.error(this, "Error serving /info endpoint", e);
try {
HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port);
} catch (IOException ioEx) {
Msg.error(this, "Failed to send error for /info", ioEx);
}
}
});
}
/**
* Register project-related endpoints
*/
private void registerProjectEndpoints(HttpServer server) {
server.createContext("/projects", exchange -> {
try {
if ("GET".equals(exchange.getRequestMethod())) {
List<Map<String, String>> projects = new ArrayList<>();
Project project = tool.getProject();
if (project != null) {
Map<String, String> projInfo = new HashMap<>();
projInfo.put("name", project.getName());
projInfo.put("location", project.getProjectLocator().toString());
projects.add(projInfo);
}
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(projects)
.addLink("self", "/projects")
.addLink("create", "/projects", "POST");
// Add link to current project if available
if (project != null) {
builder.addLink("current", "/projects/" + project.getName());
}
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} else if ("POST".equals(exchange.getRequestMethod())) {
// Creating projects is not yet implemented
HttpUtil.sendErrorResponse(exchange, 501, "Creating projects via API is not implemented", "NOT_IMPLEMENTED", port);
} else {
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
}
} catch (Exception e) {
Msg.error(this, "Error serving /projects endpoint", e);
try {
HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port);
} catch (IOException ioEx) {
Msg.error(this, "Failed to send error for /projects", ioEx);
}
}
});
// Specific project endpoint
server.createContext("/projects/", exchange -> {
try {
String path = exchange.getRequestURI().getPath();
if (path.equals("/projects/") || path.equals("/projects")) {
// This should be handled by the /projects context
exchange.getResponseHeaders().set("Location", "/projects");
exchange.sendResponseHeaders(302, -1);
return;
}
// Extract project name from path
String projectName = path.substring("/projects/".length());
if ("GET".equals(exchange.getRequestMethod())) {
Project currentProject = tool.getProject();
if (currentProject == null) {
HttpUtil.sendErrorResponse(exchange, 404, "No project is currently open", "NO_PROJECT_OPEN", port);
return;
}
if (!currentProject.getName().equals(projectName)) {
HttpUtil.sendErrorResponse(exchange, 404, "Project not found: " + projectName, "PROJECT_NOT_FOUND", port);
return;
}
// Build project details
Map<String, Object> projectDetails = new HashMap<>();
projectDetails.put("name", currentProject.getName());
projectDetails.put("location", currentProject.getProjectLocator().toString());
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(projectDetails)
.addLink("self", "/projects/" + projectName)
.addLink("programs", "/programs?project=" + projectName);
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} else {
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
}
} catch (Exception e) {
Msg.error(this, "Error serving /projects/{name} endpoint", e);
try {
HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port);
} catch (IOException ioEx) {
Msg.error(this, "Failed to send error for /projects/{name}", ioEx);
}
}
});
}
/**
* Register the root endpoint which provides links to all other API endpoints
*/
private void registerRootEndpoint(HttpServer server) {
server.createContext("/", exchange -> {
try {
// Check if this is actually a CORS preflight request
if (exchange.getAttribute("cors.handled") != null) {
// CORS was already handled
return;
}
// Check if this is a request for the root endpoint specifically
if (!exchange.getRequestURI().getPath().equals("/")) {
HttpUtil.sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND", port);
return;
}
Map<String, Object> rootData = new HashMap<>();
rootData.put("message", "MCGhidra API " + ApiConstants.API_VERSION);
rootData.put("documentation", "See GHIDRA_HTTP_API.md for full API documentation");
rootData.put("isBaseInstance", isBaseInstance);
// Build the HATEOAS response
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(rootData)
.addLink("self", "/")
.addLink("info", "/info")
.addLink("plugin-version", "/plugin-version")
.addLink("projects", "/projects")
.addLink("instances", "/instances")
.addLink("programs", "/programs");
// Add links to program-dependent endpoints if a program is loaded
if (getCurrentProgram() != null) {
Project project = tool.getProject();
String projectName = (project != null) ? project.getName() : "unknown";
builder.addLink("program", "/program")
.addLink("project", "/projects/" + projectName)
.addLink("functions", "/functions")
.addLink("symbols", "/symbols")
.addLink("data", "/data")
.addLink("strings", "/strings")
.addLink("segments", "/segments")
.addLink("structs", "/structs")
.addLink("memory", "/memory")
.addLink("xrefs", "/xrefs")
.addLink("analysis", "/analysis")
.addLink("address", "/address")
.addLink("function", "/function");
}
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} catch (Exception e) {
Msg.error(this, "Error serving / endpoint", e);
try {
HttpUtil.sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR", port);
} catch (IOException ioEx) {
Msg.error(this, "Failed to send error for /", ioEx);
}
}
});
}
// ----------------------------------------------------------------------------------
// Core Plugin Methods
// ----------------------------------------------------------------------------------
/**
* Gets the current program from the Ghidra tool
* @return The current program or null if no program is loaded
*/
public Program getCurrentProgram() {
if (tool == null) {
Msg.debug(this, "Tool is null when trying to get current program");
return null;
}
ProgramManager pm = tool.getService(ProgramManager.class);
if (pm == null) {
Msg.debug(this, "ProgramManager service is not available");
return null;
}
return pm.getCurrentProgram();
}
/**
* Find an available port for the HTTP server
* @return An available port number
*/
private int findAvailablePort() {
int basePort = ApiConstants.DEFAULT_PORT;
int maxAttempts = ApiConstants.MAX_PORT_ATTEMPTS;
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) {
Msg.debug(this, "Port " + candidate + " is not available, trying next.");
}
} else {
Msg.debug(this, "Port " + candidate + " already tracked as active instance.");
}
}
Msg.error(this, "Could not find an available port between " + basePort + " and " + (basePort + maxAttempts - 1));
throw new RuntimeException("Could not find available port after " + maxAttempts + " attempts");
}
/**
* Called when the plugin is disposed
*/
@Override
public void dispose() {
if (server != null) {
server.stop(0); // Stop immediately
Msg.info(this, "MCGhidra HTTP server stopped on port " + port);
System.out.println("[MCGhidra] HTTP server stopped on port " + port);
}
activeInstances.remove(port);
super.dispose();
}
/**
* Get the port this plugin instance is running on
* @return The HTTP server port
*/
public int getPort() {
return port;
}
/**
* Check if this is the base instance
* @return true if this is the base instance
*/
public boolean isBaseInstance() {
return isBaseInstance;
}
}