Switch to JSON as bridge/plugin comm protocol

This commit is contained in:
Teal Bauer 2025-04-04 16:05:22 +02:00
parent 04d088591b
commit cbe5dcc1f3
4 changed files with 228 additions and 149 deletions

View File

@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- Structured JSON communication between Python bridge and Java plugin
- Consistent response format with metadata (timestamp, port, instance type)
- Comprehensive test suites for HTTP API and MCP bridge
- Test runner script for easy test execution
- Detailed testing documentation in TESTING.md
### Changed
- Improved error handling in API responses
- Enhanced JSON parsing in the Java plugin
- Updated documentation with JSON communication details
## [1.3.0] - 2025-04-02
### Added

View File

@ -1,7 +1,7 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "mcp==1.5.0",
# "mcp==1.6.0",
# "requests==2.32.3",
# ]
# ///
@ -12,6 +12,7 @@ import threading
import time
from threading import Lock
from typing import Dict
from urllib.parse import quote
import requests
from mcp.server.fastmcp import FastMCP
@ -25,10 +26,16 @@ DEFAULT_GHIDRA_HOST = "localhost"
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")
instructions = """
GhydraMCP allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
First, run `discover_instances` to find open Ghidra instances. List tools to see what GhydraMCP can do.
"""
mcp = FastMCP("GhydraMCP", instructions=instructions)
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
print(f"Using Ghidra host: {ghidra_host}")
# print(f"Using Ghidra host: {ghidra_host}")
def get_instance_url(port: int) -> str:
"""Get URL for a Ghidra instance by port"""
@ -201,9 +208,9 @@ def safe_post(port: int, endpoint: str, data: dict | str) -> dict:
"result": response.text.strip()
}
else:
# Try falling back to default instance if this was a secondary instance
if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
# # Try falling back to default instance if this was a secondary instance
# if port != DEFAULT_GHIDRA_PORT and response.status_code == 404:
# return safe_post(DEFAULT_GHIDRA_PORT, endpoint, data)
try:
error_data = response.json()
@ -269,12 +276,10 @@ def register_instance(port: int, url: str = None) -> str:
try:
# Try the root endpoint first
root_url = f"{url}/"
print(f"Trying to get root info from {root_url}", file=sys.stderr)
root_response = requests.get(root_url, timeout=1.5) # Short timeout for root
if root_response.ok:
try:
print(f"Got response from root: {root_response.text}", file=sys.stderr)
root_data = root_response.json()
# Extract basic information from root
@ -283,16 +288,12 @@ def register_instance(port: int, url: str = None) -> str:
if "file" in root_data and root_data["file"]:
project_info["file"] = root_data["file"]
print(f"Root data parsed: {project_info}", file=sys.stderr)
except Exception as e:
print(f"Error parsing root info: {e}", file=sys.stderr)
else:
print(f"Root endpoint returned {root_response.status_code}", file=sys.stderr)
# If we don't have project info yet, try the /info endpoint as a fallback
if not project_info.get("project") and not project_info.get("file"):
info_url = f"{url}/info"
print(f"Trying fallback info from {info_url}", file=sys.stderr)
try:
info_response = requests.get(info_url, timeout=2)
@ -369,13 +370,18 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
"instances": found_instances
}
# Updated tool implementations with port parameter
from urllib.parse import quote
@mcp.tool()
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all functions with pagination"""
"""List all functions in the current program
Args:
port: Ghidra instance port (default: 8192)
offset: Pagination offset (default: 0)
limit: Maximum number of segments to return (default: 100)
Returns:
List of strings with function names and addresses
"""
return safe_get(port, "functions", {"offset": offset, "limit": limit})
@mcp.tool()
@ -522,18 +528,6 @@ def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str:
"""
return "\n".join(safe_get(port, "get_current_function"))
@mcp.tool()
def list_functions(port: int = DEFAULT_GHIDRA_PORT) -> list:
"""List all functions in the current program
Args:
port: Ghidra instance port (default: 8192)
Returns:
List of strings with function names and addresses
"""
return safe_get(port, "list_functions")
@mcp.tool()
def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
"""Decompile a function at a specific memory address
@ -716,19 +710,19 @@ def periodic_discovery():
time.sleep(30)
if __name__ == "__main__":
# Auto-register default instance
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
# # Auto-register default instance
# register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
# Auto-discover other instances
discover_instances()
# # 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()
# # 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()
# signal.signal(signal.SIGINT, handle_sigint)
mcp.run(transport="stdio")

View File

@ -23,9 +23,9 @@
<dependencies>
<!-- JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- Ghidra JARs as system-scoped dependencies -->

View File

@ -46,7 +46,8 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
// For JSON response handling
import org.json.simple.JSONObject;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import ghidra.app.services.CodeViewerService;
import ghidra.app.util.PseudoDisassembler;
@ -96,9 +97,11 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
}
// Log to both console and log file
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
Msg.info(this, "Marker");
// Log to both console and log file
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
try {
startServer();
@ -201,15 +204,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
});
// Class resources
// Class resources with detailed logging
server.createContext("/classes", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, getAllClassNames(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
try {
if ("GET".equals(exchange.getRequestMethod())) {
try {
Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100);
String result = getAllClassNames(offset, limit);
JsonObject json = new JsonObject();
json.addProperty("success", true);
json.addProperty("result", result);
json.addProperty("timestamp", System.currentTimeMillis());
json.addProperty("port", this.port);
Gson gson = new Gson();
String jsonStr = gson.toJson(json);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
OutputStream os = exchange.getResponseBody();
os.write(bytes);
os.flush();
os.close();
} catch (Exception e) {
Msg.error(this, "/classes: Error in request processing: " + e.getMessage(), e);
try {
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
} catch (IOException ioe) {
Msg.error(this, "/classes: Failed to send error response: " + ioe.getMessage(), ioe);
}
}
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
} catch (Exception e) {
Msg.error(this, "/classes: Unhandled error: " + e.getMessage(), e);
}
});
@ -353,8 +393,16 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
}
});
// Super simple root endpoint - exact same as /info for consistency
// Root endpoint - only handle exact "/" path
server.createContext("/", exchange -> {
// Only handle exact root path
if (!exchange.getRequestURI().getPath().equals("/")) {
// Return 404 for any other path that reaches this handler
Msg.info(this, "Received request for unknown path: " + exchange.getRequestURI().getPath());
sendErrorResponse(exchange, 404, "Endpoint not found");
return;
}
try {
String response = "{\n";
response += "\"port\": " + port + ",\n";
@ -1229,39 +1277,64 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
private void sendResponse(HttpExchange exchange, Object response) throws IOException {
JSONObject json = new JSONObject();
json.put("success", true);
JsonObject json = new JsonObject();
json.addProperty("success", true);
if (response instanceof String) {
json.put("result", response);
json.addProperty("result", (String)response);
} else {
json.put("data", response);
json.addProperty("data", response.toString());
}
json.put("timestamp", System.currentTimeMillis());
json.put("port", this.port);
json.addProperty("timestamp", System.currentTimeMillis());
json.addProperty("port", this.port);
if (this.isBaseInstance) {
json.put("instanceType", "base");
json.addProperty("instanceType", "base");
} else {
json.put("instanceType", "secondary");
json.addProperty("instanceType", "secondary");
}
sendJsonResponse(exchange, json);
}
private void sendJsonResponse(HttpExchange exchange, JSONObject jsonObj) throws IOException {
String json = jsonObj.toJSONString();
byte[] bytes = json.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 void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj) throws IOException {
try {
Gson gson = new Gson();
String json = gson.toJson(jsonObj);
Msg.debug(this, "Sending JSON response: " + json);
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(200, bytes.length);
OutputStream os = null;
try {
os = exchange.getResponseBody();
os.write(bytes);
os.flush();
} catch (IOException e) {
Msg.error(this, "Error writing response body: " + e.getMessage(), e);
throw e;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
Msg.error(this, "Error closing output stream: " + e.getMessage(), e);
}
}
}
} catch (Exception e) {
Msg.error(this, "Error in sendJsonResponse: " + e.getMessage(), e);
throw new IOException("Failed to send JSON response", e);
}
}
private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) throws IOException {
JSONObject error = new JSONObject();
error.put("error", message);
error.put("status", statusCode);
error.put("success", false);
byte[] bytes = error.toJSONString().getBytes(StandardCharsets.UTF_8);
JsonObject error = new JsonObject();
error.addProperty("error", message);
error.addProperty("status", statusCode);
error.addProperty("success", false);
Gson gson = new Gson();
byte[] bytes = gson.toJson(error).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {