Add headless support.

This commit is contained in:
Jason Tang
2026-03-07 03:48:05 -05:00
parent e5bdb7e991
commit ebf95a2eec
3 changed files with 203 additions and 1 deletions
@@ -469,7 +469,8 @@ public class GhidrAssistMCPBackend implements McpBackend {
*/
private Program resolveTargetProgram(Map<String, Object> arguments) {
if (manager == null) {
return null;
// Headless mode: no manager, fall back to getCurrentProgram()
return getCurrentProgram();
}
// Check if a specific program was requested
@@ -0,0 +1,145 @@
/*
* Headless-compatible MCP server launcher.
* Starts the Jetty MCP server without requiring the GUI plugin infrastructure.
* Used by GhidrAssistMCPHeadlessScript to provide MCP tools in analyzeHeadless mode.
*/
package ghidrassistmcp;
import java.util.Collections;
import java.util.List;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
/**
* Manages a headless MCP server instance that holds a direct program reference
* instead of relying on GhidrAssistMCPManager's PluginTool-based program discovery.
*/
public class GhidrAssistMCPHeadlessServer {
private static GhidrAssistMCPHeadlessServer instance;
private static final Object lock = new Object();
private GhidrAssistMCPServer server;
private HeadlessBackend headlessBackend;
private volatile boolean running = false;
private GhidrAssistMCPHeadlessServer() {
}
public static GhidrAssistMCPHeadlessServer getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new GhidrAssistMCPHeadlessServer();
}
}
}
return instance;
}
/**
* Start the MCP server with a direct program reference.
*
* @param program The current program from the headless script
* @param host Host to bind (typically "localhost" or "0.0.0.0")
* @param port Port to bind (typically 8080)
*/
public synchronized void start(Program program, String host, int port) throws Exception {
if (running) {
Msg.info(this, "Headless MCP server already running, updating program");
if (headlessBackend != null) {
headlessBackend.setProgram(program);
}
return;
}
Msg.info(this, "Starting headless MCP server on " + host + ":" + port);
// Create a backend wrapper that holds the program directly
headlessBackend = new HeadlessBackend(program);
server = new GhidrAssistMCPServer(host, port, headlessBackend);
server.start();
running = true;
Msg.info(this, "Headless MCP server started successfully with " +
headlessBackend.getAvailableTools().size() + " tools");
}
/**
* Stop the MCP server.
*/
public synchronized void stop() {
if (!running) {
return;
}
try {
if (server != null) {
server.stop();
}
Msg.info(this, "Headless MCP server stopped");
} catch (Exception e) {
Msg.error(this, "Error stopping headless MCP server", e);
}
server = null;
headlessBackend = null;
running = false;
synchronized (lock) {
instance = null;
}
}
public boolean isRunning() {
return running;
}
/**
* Update the program reference (e.g., when processing a new binary).
*/
public void setProgram(Program program) {
if (headlessBackend != null) {
headlessBackend.setProgram(program);
}
}
/**
* A thin wrapper around GhidrAssistMCPBackend that provides a direct program
* reference instead of going through GhidrAssistMCPManager.
*/
private static class HeadlessBackend extends GhidrAssistMCPBackend {
private volatile Program currentProgram;
HeadlessBackend(Program program) {
super();
this.currentProgram = program;
// Notify backend of the initial program
onProgramActivated(program);
}
void setProgram(Program program) {
if (this.currentProgram != null) {
onProgramDeactivated(this.currentProgram);
}
this.currentProgram = program;
if (program != null) {
onProgramActivated(program);
}
}
@Override
public Program getCurrentProgram() {
return currentProgram;
}
@Override
public List<Program> getAllOpenPrograms() {
if (currentProgram != null) {
return Collections.singletonList(currentProgram);
}
return Collections.emptyList();
}
}
}
@@ -0,0 +1,56 @@
package ghidrassistmcp.scripts;
import ghidra.app.script.GhidraScript;
import ghidra.util.Msg;
import ghidrassistmcp.GhidrAssistMCPHeadlessServer;
/**
* Headless GhidraScript that starts the GhidrAssistMCP server.
* Designed to be run as a -preScript before GhidrAssistHL scripts so that
* the MCP server is available for tool calls during ReAct analysis.
*
* Usage in analyzeHeadless:
* -preScript GAMCPStartServerScript.java
* -postScript GAHLQueryScript.java ...
*/
public class GAMCPStartServerScript extends GhidraScript {
@Override
protected void run() throws Exception {
if (currentProgram == null) {
Msg.warn(this, "GAMCPStartServerScript: No program loaded, skipping MCP server start");
return;
}
String host = "localhost";
int port = 8080;
// Parse optional arguments: host=... port=...
String[] args = getScriptArgs();
if (args != null) {
for (String arg : args) {
if (arg.startsWith("host=")) {
host = arg.substring(5);
} else if (arg.startsWith("port=")) {
try {
port = Integer.parseInt(arg.substring(5));
} catch (NumberFormatException e) {
Msg.warn(this, "Invalid port argument, using default 8080");
}
}
}
}
GhidrAssistMCPHeadlessServer mcpServer = GhidrAssistMCPHeadlessServer.getInstance();
if (mcpServer.isRunning()) {
Msg.info(this, "MCP server already running, updating program reference");
mcpServer.setProgram(currentProgram);
return;
}
Msg.info(this, "Starting headless MCP server for: " + currentProgram.getName());
mcpServer.start(currentProgram, host, port);
Msg.info(this, "Headless MCP server ready on " + host + ":" + port);
}
}