diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 91b3fe5..6a17a27 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -44,75 +44,194 @@ def get_instance_url(port: int) -> str: 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""" +def safe_get(port: int, endpoint: str, params: dict = None) -> dict: + """Perform a GET request to a specific Ghidra instance and return JSON response""" if params is None: params = {} url = f"{get_instance_url(port)}/{endpoint}" try: - response = requests.get(url, params=params, timeout=5) - response.encoding = 'utf-8' + response = requests.get( + url, + params=params, + headers={'Accept': 'application/json'}, + timeout=5 + ) + 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()}"] + try: + # Always expect JSON response + json_data = response.json() + + # If the response has a 'result' field that's a string, extract it + if isinstance(json_data, dict) and 'result' in json_data: + return json_data + + # Otherwise, wrap the response in a standard format + return { + "success": True, + "data": json_data, + "timestamp": int(time.time() * 1000) + } + except ValueError: + # If not JSON, wrap the text in our standard format + return { + "success": False, + "error": "Invalid JSON response", + "response": response.text, + "timestamp": int(time.time() * 1000) + } else: - return [f"Error {response.status_code}: {response.text.strip()}"] + # Try falling back to default instance if this was a secondary instance + if port != DEFAULT_GHIDRA_PORT and response.status_code == 404: + return safe_get(DEFAULT_GHIDRA_PORT, endpoint, params) + + try: + error_data = response.json() + return { + "success": False, + "error": error_data.get("error", f"HTTP {response.status_code}"), + "status_code": response.status_code, + "timestamp": int(time.time() * 1000) + } + except ValueError: + return { + "success": False, + "error": response.text.strip(), + "status_code": response.status_code, + "timestamp": int(time.time() * 1000) + } 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"] + return { + "success": False, + "error": "Failed to connect to Ghidra instance", + "status_code": 503, + "timestamp": int(time.time() * 1000) + } except Exception as e: - return [f"Request failed: {str(e)}"] + return { + "success": False, + "error": str(e), + "exception": e.__class__.__name__, + "timestamp": int(time.time() * 1000) + } -def safe_put(port: int, endpoint: str, data: dict) -> str: - """Perform a PUT request to a specific Ghidra instance""" +def safe_put(port: int, endpoint: str, data: dict) -> dict: + """Perform a PUT request to a specific Ghidra instance with JSON payload""" try: url = f"{get_instance_url(port)}/{endpoint}" - response = requests.put(url, data=data, timeout=5) - response.encoding = 'utf-8' + response = requests.put( + url, + json=data, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + 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_put(DEFAULT_GHIDRA_PORT, endpoint, data) + try: + return response.json() + except ValueError: + return { + "success": True, + "result": response.text.strip() + } else: - return f"Error {response.status_code}: {response.text.strip()}" + # Try falling back to default instance if this was a secondary instance + if port != DEFAULT_GHIDRA_PORT and response.status_code == 404: + return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data) + + try: + error_data = response.json() + return { + "success": False, + "error": error_data.get("error", f"HTTP {response.status_code}"), + "status_code": response.status_code + } + except ValueError: + return { + "success": False, + "error": response.text.strip(), + "status_code": response.status_code + } except requests.exceptions.ConnectionError: if port != DEFAULT_GHIDRA_PORT: return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data) - return "Error: Failed to connect to Ghidra instance" + return { + "success": False, + "error": "Failed to connect to Ghidra instance", + "status_code": 503 + } except Exception as e: - return f"Request failed: {str(e)}" + return { + "success": False, + "error": str(e), + "exception": e.__class__.__name__ + } -def safe_post(port: int, endpoint: str, data: dict | str) -> str: - """Perform a POST request to a specific Ghidra instance""" +def safe_post(port: int, endpoint: str, data: dict | str) -> dict: + """Perform a POST request to a specific Ghidra instance with JSON payload""" try: url = f"{get_instance_url(port)}/{endpoint}" + if isinstance(data, dict): - response = requests.post(url, data=data, timeout=5) + response = requests.post( + url, + json=data, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) else: - response = requests.post(url, data=data.encode("utf-8"), timeout=5) - response.encoding = 'utf-8' + response = requests.post( + url, + data=data, + headers={'Content-Type': 'text/plain'}, + timeout=5 + ) + 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) + try: + return response.json() + except ValueError: + return { + "success": True, + "result": response.text.strip() + } else: - return f"Error {response.status_code}: {response.text.strip()}" + # 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() + return { + "success": False, + "error": error_data.get("error", f"HTTP {response.status_code}"), + "status_code": response.status_code + } + except ValueError: + return { + "success": False, + "error": response.text.strip(), + "status_code": response.status_code + } 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" + return { + "success": False, + "error": "Failed to connect to Ghidra instance", + "status_code": 503 + } except Exception as e: - return f"Request failed: {str(e)}" + return { + "success": False, + "error": str(e), + "exception": e.__class__.__name__ + } # Instance management tools @mcp.tool() diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index 16f91ef..dcfed31 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -1100,18 +1100,59 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { } /** - * Parse post body form params, e.g. oldName=foo&newName=bar + * Parse post body params from form data or simple JSON */ private Map parsePostParams(HttpExchange exchange) throws IOException { byte[] body = exchange.getRequestBody().readAllBytes(); String bodyStr = new String(body, StandardCharsets.UTF_8); Map params = new HashMap<>(); + + // Check if it looks like JSON + if (bodyStr.trim().startsWith("{")) { + try { + // Manual simple JSON parsing for key-value pairs + // This avoids using the JSONParser which might be causing issues + String jsonContent = bodyStr.trim(); + // Remove the outer braces + jsonContent = jsonContent.substring(1, jsonContent.length() - 1).trim(); + + // Split by commas not inside quotes + String[] pairs = jsonContent.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); + + for (String pair : pairs) { + String[] keyValue = pair.split(":", 2); + if (keyValue.length == 2) { + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + + // Remove quotes if present + if (key.startsWith("\"") && key.endsWith("\"")) { + key = key.substring(1, key.length() - 1); + } + + if (value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + + params.put(key, value); + } + } + + return params; + } catch (Exception e) { + Msg.error(this, "Failed to parse JSON request body: " + e.getMessage(), e); + // Fall through to form data parsing + } + } + + // If JSON parsing fails or it's not JSON, try form data for (String pair : bodyStr.split("&")) { String[] kv = pair.split("="); if (kv.length == 2) { params.put(kv[0], kv[1]); } } + return params; } @@ -1187,14 +1228,46 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { } - 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"); + private void sendResponse(HttpExchange exchange, Object response) throws IOException { + JSONObject json = new JSONObject(); + json.put("success", true); + if (response instanceof String) { + json.put("result", response); + } else { + json.put("data", response); + } + json.put("timestamp", System.currentTimeMillis()); + json.put("port", this.port); + if (this.isBaseInstance) { + json.put("instanceType", "base"); + } else { + json.put("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 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); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } private int findAvailablePort() {