diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..78e258d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Added docstrings for all @mcp.tool functions +- Variable manipulation tools (rename/retype variables) +- New endpoints for function variable management +- Dynamic version output in API responses +- Enhanced function analysis capabilities +- Support for searching variables by name +- New tools for working with function variables: + - get_function_by_address + - get_current_address + - get_current_function + - decompile_function_by_address + - disassemble_function + - set_decompiler_comment + - set_disassembly_comment + - rename_local_variable + - rename_function_by_address + - set_function_prototype + - set_local_variable_type + +### Changed +- Improved version handling in build system +- Reorganized imports in bridge_mcp_hydra.py +- Updated MANIFEST.MF with more detailed description + +## [1.2.0] - 2024-06-15 + +### Added +- Enhanced function analysis capabilities +- Additional variable manipulation tools +- Support for multiple Ghidra instances + +### Changed +- Improved error handling in API calls +- Optimized performance for large binaries + +## [1.0.0] - 2024-03-15 + +### Added +- Initial release of GhydraMCP bridge +- Basic Ghidra instance management tools +- Function analysis tools +- Variable manipulation tools diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index b376960..9c5a278 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -6,12 +6,14 @@ # ] # /// import os +import signal import sys -import time -import requests import threading -from typing import Dict +import time from threading import Lock +from typing import Dict + +import requests from mcp.server.fastmcp import FastMCP # Track active Ghidra instances (port -> info dict) @@ -146,26 +148,54 @@ def register_instance(port: int, url: str = None) -> str: project_info = {"url": url} try: - info_url = f"{url}/info" - info_response = requests.get(info_url, timeout=2) - if info_response.ok: + # 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: - # Parse JSON response - info_data = info_response.json() + print(f"Got response from root: {root_response.text}", file=sys.stderr) + root_data = root_response.json() - # Extract relevant information - project_info["project"] = info_data.get("project", "Unknown") + # Extract basic information from root + if "project" in root_data and root_data["project"]: + project_info["project"] = root_data["project"] + if "file" in root_data and root_data["file"]: + project_info["file"] = root_data["file"] - # Handle file information which is nested - file_info = info_data.get("file", {}) - if file_info: - project_info["file"] = file_info.get("name", "") - project_info["path"] = file_info.get("path", "") - project_info["architecture"] = file_info.get("architecture", "") - project_info["endian"] = file_info.get("endian", "") - except ValueError: - # Not valid JSON - pass + 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) + if info_response.ok: + try: + info_data = info_response.json() + # Extract relevant information + if "project" in info_data and info_data["project"]: + project_info["project"] = info_data["project"] + + # Handle file information + file_info = info_data.get("file", {}) + if isinstance(file_info, dict) and file_info.get("name"): + project_info["file"] = file_info.get("name", "") + project_info["path"] = file_info.get("path", "") + project_info["architecture"] = file_info.get("architecture", "") + project_info["endian"] = file_info.get("endian", "") + print(f"Info data parsed: {project_info}", file=sys.stderr) + except Exception as e: + print(f"Error parsing info endpoint: {e}", file=sys.stderr) + except Exception as e: + print(f"Error connecting to info endpoint: {e}", file=sys.stderr) except Exception: # Non-critical, continue with registration even if project info fails pass @@ -223,6 +253,7 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict: # 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""" @@ -250,33 +281,211 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st @mcp.tool() def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all memory segments in the current program with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of segments to return (default: 100) + + Returns: + List of segment information strings + """ return safe_get(port, "segments", {"offset": offset, "limit": limit}) @mcp.tool() def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all imported symbols with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of imports to return (default: 100) + + Returns: + List of import information strings + """ return safe_get(port, "symbols/imports", {"offset": offset, "limit": limit}) @mcp.tool() def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all exported symbols with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of exports to return (default: 100) + + Returns: + List of export information strings + """ return safe_get(port, "symbols/exports", {"offset": offset, "limit": limit}) @mcp.tool() def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all namespaces in the current program with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of namespaces to return (default: 100) + + Returns: + List of namespace information strings + """ 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: + """List all defined data items with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of data items to return (default: 100) + + Returns: + List of data item information strings + """ 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: + """Search for functions by name with pagination + + Args: + port: Ghidra instance port (default: 8192) + query: Search string to match against function names + offset: Pagination offset (default: 0) + limit: Maximum number of functions to return (default: 100) + + Returns: + List of matching function information strings or error message if query is empty + """ if not query: return ["Error: query string is required"] return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit}) -# Handle graceful shutdown -import signal -import os +@mcp.tool() +def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str: + """ + Get a function by its address. + """ + return "\n".join(safe_get(port, "get_function_by_address", {"address": address})) + +@mcp.tool() +def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> str: + """ + Get the address currently selected by the user. + """ + return "\n".join(safe_get(port, "get_current_address")) + +@mcp.tool() +def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str: + """ + Get the function currently selected by the user. + """ + 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 database. + """ + 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 the given address. + """ + return "\n".join(safe_get(port, "decompile_function", {"address": address})) + +@mcp.tool() +def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> list: + """ + Get assembly code (address: instruction; comment) for a function. + """ + return safe_get(port, "disassemble_function", {"address": address}) + +@mcp.tool() +def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: + """ + Set a comment for a given address in the function pseudocode. + """ + return safe_post(port, "set_decompiler_comment", {"address": address, "comment": comment}) + +@mcp.tool() +def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: + """ + Set a comment for a given address in the function disassembly. + """ + return safe_post(port, "set_disassembly_comment", {"address": address, "comment": comment}) + +@mcp.tool() +def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", old_name: str = "", new_name: str = "") -> str: + """ + Rename a local variable in a function. + """ + return safe_post(port, "rename_local_variable", {"function_address": function_address, "old_name": old_name, "new_name": new_name}) + +@mcp.tool() +def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", new_name: str = "") -> str: + """ + Rename a function by its address. + """ + return safe_post(port, "rename_function_by_address", {"function_address": function_address, "new_name": new_name}) + +@mcp.tool() +def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", prototype: str = "") -> str: + """ + Set a function's prototype. + """ + return safe_post(port, "set_function_prototype", {"function_address": function_address, "prototype": prototype}) + +@mcp.tool() +def set_local_variable_type(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", variable_name: str = "", new_type: str = "") -> str: + """ + Set a local variable's type. + """ + return safe_post(port, "set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type}) + +@mcp.tool() +def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> list: + """List global variables with optional search""" + params = {"offset": offset, "limit": limit} + if search: + params["search"] = search + return safe_get(port, "variables", params) + +@mcp.tool() +def list_function_variables(port: int = DEFAULT_GHIDRA_PORT, function: str = "") -> str: + """List variables in a specific function""" + if not function: + return "Error: function name is required" + + encoded_name = quote(function) + return safe_get(port, f"functions/{encoded_name}/variables", {}) + +@mcp.tool() +def rename_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", new_name: str = "") -> str: + """Rename a variable in a function""" + if not function or not name or not new_name: + return "Error: function, name, and new_name parameters are required" + + encoded_function = quote(function) + encoded_var = quote(name) + return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"newName": new_name}) + +@mcp.tool() +def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", data_type: str = "") -> str: + """Change the data type of a variable in a function""" + if not function or not name or not data_type: + return "Error: function, name, and data_type parameters are required" + + encoded_function = quote(function) + encoded_var = quote(name) + return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type}) def handle_sigint(signum, frame): os._exit(0) diff --git a/pom.xml b/pom.xml index b4fec6b..033978c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ eu.starsong.ghidra GhydraMCP jar - 11.3.1 + ${revision} GhydraMCP https://github.com/teal-bauer/GhydraMCP @@ -16,10 +16,19 @@ ${project.basedir}/lib true true + yyyyMMdd-HHmmss + dev-SNAPSHOT - + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + ghidra Generic @@ -70,13 +79,6 @@ ${ghidra.jar.location}/Base.jar - - - com.googlecode.json-simple - json-simple - 1.1.1 - - junit @@ -87,8 +89,24 @@ + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + org.apache.maven.plugins maven-compiler-plugin @@ -101,13 +119,74 @@ + + + io.github.git-commit-id + git-commit-id-maven-plugin + 5.0.0 + + + get-git-info + initialize + + revision + + + + + true + ${project.build.outputDirectory}/git.properties + + git.commit.id.abbrev + git.commit.time + git.closest.tag.name + git.build.version + + full + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + set-revision-from-git + initialize + + regex-property + + + revision + ${git.commit.id.abbrev}-${maven.build.timestamp} + .* + $0 + false + + + + + maven-jar-plugin 3.2.2 - src/main/resources/META-INF/MANIFEST.MF + + false + + + GhydraMCP + ${git.commit.id.abbrev}-${maven.build.timestamp} + eu.starsong.ghidra.GhydraMCP + GhydraMCP + ${git.commit.id.abbrev}-${maven.build.timestamp} + LaurieWired, Teal Bauer + Expose multiple Ghidra tools to MCP servers with variable management + GhydraMCP @@ -134,7 +213,7 @@ src/assembly/ghidra-extension.xml - GhydraMCP-${project.version} + GhydraMCP-${git.commit.id.abbrev}-${maven.build.timestamp} false @@ -150,7 +229,7 @@ src/assembly/complete-package.xml - GhydraMCP-Complete-${project.version} + GhydraMCP-Complete-${git.commit.id.abbrev}-${maven.build.timestamp} false @@ -186,7 +265,14 @@ false + ghidra:Generic + ghidra:SoftwareModeling + ghidra:Project ghidra:Docking + ghidra:Decompiler + ghidra:Utility + ghidra:Base + junit:junit ghidra:* diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index ea71816..16f91ef 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -4,13 +4,26 @@ import ghidra.framework.plugintool.*; import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.program.model.address.Address; import ghidra.program.model.address.GlobalNamespace; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; import ghidra.program.model.listing.*; import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.pcode.HighVariable; +import ghidra.program.model.pcode.HighSymbol; +import ghidra.program.model.pcode.VarnodeAST; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.HighFunctionDBUtil; +import ghidra.program.model.pcode.LocalSymbolMap; +import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; import ghidra.program.model.symbol.*; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileResults; +import ghidra.app.decompiler.ClangNode; +import ghidra.app.decompiler.ClangTokenGroup; +import ghidra.app.decompiler.ClangVariableToken; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.ProgramManager; +import ghidra.app.util.demangler.DemanglerUtil; import ghidra.framework.model.Project; import ghidra.framework.model.DomainFile; import ghidra.framework.plugintool.PluginInfo; @@ -35,6 +48,23 @@ import java.util.concurrent.atomic.*; // For JSON response handling import org.json.simple.JSONObject; +import ghidra.app.services.CodeViewerService; +import ghidra.app.util.PseudoDisassembler; +import ghidra.app.cmd.function.SetVariableNameCmd; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.listing.LocalVariableImpl; +import ghidra.program.model.listing.ParameterImpl; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.program.util.ProgramLocation; +import ghidra.util.task.TaskMonitor; +import ghidra.program.model.pcode.Varnode; +import ghidra.program.model.data.PointerDataType; +import ghidra.program.model.data.Undefined1DataType; +import ghidra.program.model.listing.Variable; +import ghidra.app.decompiler.component.DecompilerUtils; +import ghidra.app.decompiler.ClangToken; + @PluginInfo( status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, @@ -105,25 +135,69 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { server.createContext("/functions/", exchange -> { String path = exchange.getRequestURI().getPath(); - String name = path.substring(path.lastIndexOf('/') + 1); + + // Handle sub-paths: /functions/{name} + // or /functions/{name}/variables + String[] pathParts = path.split("/"); + + if (pathParts.length < 3) { + exchange.sendResponseHeaders(400, -1); // Bad Request + return; + } + + String functionName = pathParts[2]; try { - name = java.net.URLDecoder.decode(name, StandardCharsets.UTF_8.name()); + functionName = java.net.URLDecoder.decode(functionName, StandardCharsets.UTF_8.name()); } catch (Exception e) { Msg.error(this, "Failed to decode function name", e); exchange.sendResponseHeaders(400, -1); // Bad Request return; } - if ("GET".equals(exchange.getRequestMethod())) { - sendResponse(exchange, decompileFunctionByName(name)); - } else if ("PUT".equals(exchange.getRequestMethod())) { - Map params = parsePostParams(exchange); - String newName = params.get("newName"); - String response = renameFunction(name, newName) - ? "Renamed successfully" : "Rename failed"; - sendResponse(exchange, response); + // Check if we're dealing with a variables request + if (pathParts.length > 3 && "variables".equals(pathParts[3])) { + if ("GET".equals(exchange.getRequestMethod())) { + // List all variables in function + sendResponse(exchange, listVariablesInFunction(functionName)); + } else if ("PUT".equals(exchange.getRequestMethod()) && pathParts.length > 4) { + // Handle operations on a specific variable + String variableName = pathParts[4]; + try { + variableName = java.net.URLDecoder.decode(variableName, StandardCharsets.UTF_8.name()); + } catch (Exception e) { + Msg.error(this, "Failed to decode variable name", e); + exchange.sendResponseHeaders(400, -1); + return; + } + + Map params = parsePostParams(exchange); + if (params.containsKey("newName")) { + // Rename variable + String result = renameVariable(functionName, variableName, params.get("newName")); + sendResponse(exchange, result); + } else if (params.containsKey("dataType")) { + // Retype variable + String result = retypeVariable(functionName, variableName, params.get("dataType")); + sendResponse(exchange, result); + } else { + sendResponse(exchange, "Missing required parameter: newName or dataType"); + } + } else { + exchange.sendResponseHeaders(405, -1); // Method Not Allowed + } } else { - exchange.sendResponseHeaders(405, -1); // Method Not Allowed + // Simple function operations + if ("GET".equals(exchange.getRequestMethod())) { + sendResponse(exchange, decompileFunctionByName(functionName)); + } else if ("PUT".equals(exchange.getRequestMethod())) { + Map params = parsePostParams(exchange); + String newName = params.get("newName"); + String response = renameFunction(functionName, newName) + ? "Renamed successfully" : "Rename failed"; + sendResponse(exchange, response); + } else { + exchange.sendResponseHeaders(405, -1); // Method Not Allowed + } } }); @@ -201,6 +275,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { exchange.sendResponseHeaders(405, -1); // Method Not Allowed } }); + + // Global variables endpoint + server.createContext("/variables", exchange -> { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + String search = qparams.get("search"); + + if (search != null && !search.isEmpty()) { + sendResponse(exchange, searchVariables(search, offset, limit)); + } else { + sendResponse(exchange, listGlobalVariables(offset, limit)); + } + } else { + exchange.sendResponseHeaders(405, -1); // Method Not Allowed + } + }); // Instance management endpoints server.createContext("/instances", exchange -> { @@ -213,21 +305,99 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { sendResponse(exchange, sb.toString()); }); - // Info endpoints - both root and /info for flexibility + // Super simple info endpoint with guaranteed response server.createContext("/info", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - sendJsonResponse(exchange, getProjectInfo()); - } else { - exchange.sendResponseHeaders(405, -1); // Method Not Allowed + try { + String response = "{\n"; + response += "\"port\": " + port + ",\n"; + response += "\"isBaseInstance\": " + isBaseInstance + ",\n"; + + // Try to get program info if available + Program program = getCurrentProgram(); + String programName = "\"\""; + if (program != null) { + programName = "\"" + program.getName() + "\""; + } + + // Try to get project info if available + Project project = tool.getProject(); + String projectName = "\"\""; + if (project != null) { + projectName = "\"" + project.getName() + "\""; + } + + response += "\"project\": " + projectName + ",\n"; + response += "\"file\": " + programName + "\n"; + response += "}"; + + Msg.info(this, "Sending /info response: " + response); + byte[] bytes = response.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); + } + } catch (Exception e) { + Msg.error(this, "Error serving /info endpoint", e); + try { + String error = "{\"error\": \"Internal error\", \"port\": " + port + "}"; + byte[] bytes = error.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); + } + } catch (IOException ioe) { + Msg.error(this, "Failed to send error response", ioe); + } } }); - // Root endpoint also returns project info + // Super simple root endpoint - exact same as /info for consistency server.createContext("/", exchange -> { - if ("GET".equals(exchange.getRequestMethod())) { - sendJsonResponse(exchange, getProjectInfo()); - } else { - exchange.sendResponseHeaders(405, -1); // Method Not Allowed + try { + String response = "{\n"; + response += "\"port\": " + port + ",\n"; + response += "\"isBaseInstance\": " + isBaseInstance + ",\n"; + + // Try to get program info if available + Program program = getCurrentProgram(); + String programName = "\"\""; + if (program != null) { + programName = "\"" + program.getName() + "\""; + } + + // Try to get project info if available + Project project = tool.getProject(); + String projectName = "\"\""; + if (project != null) { + projectName = "\"" + project.getName() + "\""; + } + + response += "\"project\": " + projectName + ",\n"; + response += "\"file\": " + programName + "\n"; + response += "}"; + + Msg.info(this, "Sending / response: " + response); + byte[] bytes = response.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); + } + } catch (Exception e) { + Msg.error(this, "Error serving / endpoint", e); + try { + String error = "{\"error\": \"Internal error\", \"port\": " + port + "}"; + byte[] bytes = error.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); + } + } catch (IOException ioe) { + Msg.error(this, "Failed to send error response", ioe); + } } }); @@ -270,7 +440,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { List names = new ArrayList<>(); for (Function f : program.getFunctionManager().getFunctions(true)) { - names.add(f.getName()); + names.add(f.getName() + " @ " + f.getEntryPoint()); } return paginateList(names, offset, limit); } @@ -485,6 +655,427 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { Msg.error(this, "Failed to execute rename data on Swing thread", e); } } + + // ---------------------------------------------------------------------------------- + // New variable handling methods + // ---------------------------------------------------------------------------------- + + private String listVariablesInFunction(String functionName) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + + DecompInterface decomp = new DecompInterface(); + try { + if (!decomp.openProgram(program)) { + return "Failed to initialize decompiler"; + } + + Function function = findFunctionByName(program, functionName); + if (function == null) { + return "Function not found: " + functionName; + } + + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + if (results == null || !results.decompileCompleted()) { + return "Failed to decompile function: " + functionName; + } + + // Get high-level pcode representation for the function + HighFunction highFunction = results.getHighFunction(); + if (highFunction == null) { + return "Failed to get high function for: " + functionName; + } + + // Get local variables + List variables = new ArrayList<>(); + Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.getHighVariable() != null) { + DataType dt = symbol.getDataType(); + String dtName = dt != null ? dt.getName() : "unknown"; + variables.add(String.format("%s: %s @ %s", + symbol.getName(), dtName, symbol.getPCAddress())); + } + } + + // Get parameters + List parameters = new ArrayList<>(); + // In older Ghidra versions, we need to filter symbols to find parameters + symbolIter = highFunction.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.isParameter()) { + DataType dt = symbol.getDataType(); + String dtName = dt != null ? dt.getName() : "unknown"; + parameters.add(String.format("%s: %s (parameter)", + symbol.getName(), dtName)); + } + } + + // Format the response + StringBuilder sb = new StringBuilder(); + sb.append("Function: ").append(functionName).append("\n\n"); + + sb.append("Parameters:\n"); + if (parameters.isEmpty()) { + sb.append(" none\n"); + } else { + for (String param : parameters) { + sb.append(" ").append(param).append("\n"); + } + } + + sb.append("\nLocal Variables:\n"); + if (variables.isEmpty()) { + sb.append(" none\n"); + } else { + for (String var : variables) { + sb.append(" ").append(var).append("\n"); + } + } + + return sb.toString(); + } finally { + decomp.dispose(); + } + } + + private String renameVariable(String functionName, String oldName, String newName) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + + Function func = null; + for (Function f : program.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(functionName)) { + func = f; + break; + } + } + + if (func == null) { + return "Function not found"; + } + + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + if (result == null || !result.decompileCompleted()) { + return "Decompilation failed"; + } + + HighFunction highFunction = result.getHighFunction(); + if (highFunction == null) { + return "Decompilation failed (no high function)"; + } + + LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); + if (localSymbolMap == null) { + return "Decompilation failed (no local symbol map)"; + } + + HighSymbol highSymbol = null; + Iterator symbols = localSymbolMap.getSymbols(); + while (symbols.hasNext()) { + HighSymbol symbol = symbols.next(); + String symbolName = symbol.getName(); + + if (symbolName.equals(oldName)) { + highSymbol = symbol; + } + if (symbolName.equals(newName)) { + return "Error: A variable with name '" + newName + "' already exists in this function"; + } + } + + if (highSymbol == null) { + return "Variable not found"; + } + + boolean commitRequired = checkFullCommit(highSymbol, highFunction); + + final HighSymbol finalHighSymbol = highSymbol; + final Function finalFunction = func; + AtomicBoolean successFlag = new AtomicBoolean(false); + + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename variable"); + try { + if (commitRequired) { + HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, + ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); + } + HighFunctionDBUtil.updateDBVariable( + finalHighSymbol, + newName, + null, + SourceType.USER_DEFINED + ); + successFlag.set(true); + } + catch (Exception e) { + Msg.error(this, "Failed to rename variable", e); + } + finally { + program.endTransaction(tx, true); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage(); + Msg.error(this, errorMsg, e); + return errorMsg; + } + return successFlag.get() ? "Variable renamed" : "Failed to rename variable"; + } + + /** + * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. + * Compare the given HighFunction's idea of the prototype with the Function's idea. + * Return true if there is a difference. If a specific symbol is being changed, + * it can be passed in to check whether or not the prototype is being affected. + * @param highSymbol (if not null) is the symbol being modified + * @param hfunction is the given HighFunction + * @return true if there is a difference (and a full commit is required) + */ + protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { + if (highSymbol != null && !highSymbol.isParameter()) { + return false; + } + Function function = hfunction.getFunction(); + Parameter[] parameters = function.getParameters(); + LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); + int numParams = localSymbolMap.getNumParams(); + if (numParams != parameters.length) { + return true; + } + + for (int i = 0; i < numParams; i++) { + HighSymbol param = localSymbolMap.getParamSymbol(i); + if (param.getCategoryIndex() != i) { + return true; + } + VariableStorage storage = param.getStorage(); + // Don't compare using the equals method so that DynamicVariableStorage can match + if (0 != storage.compareTo(parameters[i].getVariableStorage())) { + return true; + } + } + + return false; + } + + private String retypeVariable(String functionName, String varName, String dataTypeName) { + if (varName == null || varName.isEmpty() || dataTypeName == null || dataTypeName.isEmpty()) { + return "Both variable name and data type are required"; + } + + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + + AtomicReference result = new AtomicReference<>("Variable retype failed"); + + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Retype variable via HTTP"); + try { + Function function = findFunctionByName(program, functionName); + if (function == null) { + result.set("Function not found: " + functionName); + return; + } + + // Initialize decompiler + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + + if (decompRes == null || !decompRes.decompileCompleted()) { + result.set("Failed to decompile function: " + functionName); + return; + } + + HighFunction highFunction = decompRes.getHighFunction(); + if (highFunction == null) { + result.set("Failed to get high function"); + return; + } + + // Find the variable by name - must match exactly and be in current scope + HighSymbol targetSymbol = null; + Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.getName().equals(varName) && + symbol.getPCAddress().equals(function.getEntryPoint())) { + targetSymbol = symbol; + break; + } + } + + if (targetSymbol == null) { + result.set("Variable not found: " + varName); + return; + } + + // Find the data type by name + DataType dataType = findDataType(program, dataTypeName); + if (dataType == null) { + result.set("Data type not found: " + dataTypeName); + return; + } + + // Retype the variable + HighFunctionDBUtil.updateDBVariable(targetSymbol, targetSymbol.getName(), dataType, + SourceType.USER_DEFINED); + + result.set("Variable '" + varName + "' retyped to '" + dataTypeName + "'"); + } catch (Exception e) { + Msg.error(this, "Error retyping variable", e); + result.set("Error: " + e.getMessage()); + } finally { + program.endTransaction(tx, true); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute on Swing thread", e); + result.set("Error: " + e.getMessage()); + } + + return result.get(); + } + + private String listGlobalVariables(int offset, int limit) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + + List globalVars = new ArrayList<>(); + SymbolTable symbolTable = program.getSymbolTable(); + SymbolIterator it = symbolTable.getSymbolIterator(); + + while (it.hasNext()) { + Symbol symbol = it.next(); + // Check for globals - look for symbols that are in global space and not functions + if (symbol.isGlobal() && + symbol.getSymbolType() != SymbolType.FUNCTION && + symbol.getSymbolType() != SymbolType.LABEL) { + globalVars.add(String.format("%s @ %s", + symbol.getName(), symbol.getAddress())); + } + } + + Collections.sort(globalVars); + return paginateList(globalVars, offset, limit); + } + + private String searchVariables(String searchTerm, int offset, int limit) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + if (searchTerm == null || searchTerm.isEmpty()) return "Search term is required"; + + List matchedVars = new ArrayList<>(); + + // Search global variables + SymbolTable symbolTable = program.getSymbolTable(); + SymbolIterator it = symbolTable.getSymbolIterator(); + while (it.hasNext()) { + Symbol symbol = it.next(); + if (symbol.isGlobal() && + symbol.getSymbolType() != SymbolType.FUNCTION && + symbol.getSymbolType() != SymbolType.LABEL && + symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { + matchedVars.add(String.format("%s @ %s (global)", + symbol.getName(), symbol.getAddress())); + } + } + + // Search local variables in functions + DecompInterface decomp = new DecompInterface(); + try { + if (decomp.openProgram(program)) { + for (Function function : program.getFunctionManager().getFunctions(true)) { + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + if (results != null && results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + // Check each local variable and parameter + Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) { + if (symbol.isParameter()) { + matchedVars.add(String.format("%s in %s (parameter)", + symbol.getName(), function.getName())); + } else { + matchedVars.add(String.format("%s in %s @ %s (local)", + symbol.getName(), function.getName(), symbol.getPCAddress())); + } + } + } + } + } + } + } + } finally { + decomp.dispose(); + } + + Collections.sort(matchedVars); + + if (matchedVars.isEmpty()) { + return "No variables matching '" + searchTerm + "'"; + } + return paginateList(matchedVars, offset, limit); + } + + // ---------------------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------------------- + + private Function findFunctionByName(Program program, String name) { + if (program == null || name == null || name.isEmpty()) { + return null; + } + + for (Function function : program.getFunctionManager().getFunctions(true)) { + if (function.getName().equals(name)) { + return function; + } + } + return null; + } + + private DataType findDataType(Program program, String name) { + if (program == null || name == null || name.isEmpty()) { + return null; + } + + DataTypeManager dtm = program.getDataTypeManager(); + + // First try direct lookup + DataType dt = dtm.getDataType("/" + name); + if (dt != null) { + return dt; + } + + // Try built-in types by simple name + dt = dtm.findDataType(name); + if (dt != null) { + return dt; + } + + // Try to find a matching type by name only + Iterator dtIter = dtm.getAllDataTypes(); + while (dtIter.hasNext()) { + DataType type = dtIter.next(); + if (type.getName().equals(name)) { + return type; + } + } + + return null; + } // ---------------------------------------------------------------------------------- // Utility: parse query params, parse post params, pagination, etc. @@ -569,56 +1160,32 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { return sb.toString(); } - public Program getCurrentProgram() { - ProgramManager pm = tool.getService(ProgramManager.class); - return pm != null ? pm.getCurrentProgram() : null; - } - /** - * Get information about the current project and open file in JSON format + * Get the current program from the tool */ - private JSONObject getProjectInfo() { - JSONObject info = new JSONObject(); - Program program = getCurrentProgram(); - - // Get project information if available - Project project = tool.getProject(); - if (project != null) { - info.put("project", project.getName()); - } else { - info.put("project", "Unknown"); + public Program getCurrentProgram() { + if (tool == null) { + Msg.debug(this, "Tool is null when trying to get current program"); + return null; } - - // Create file information object - JSONObject fileInfo = new JSONObject(); - - // Get current file information if available - if (program != null) { - // Basic info - fileInfo.put("name", program.getName()); - - // Try to get more detailed info - DomainFile domainFile = program.getDomainFile(); - if (domainFile != null) { - fileInfo.put("path", domainFile.getPathname()); + + try { + ProgramManager pm = tool.getService(ProgramManager.class); + if (pm == null) { + Msg.debug(this, "ProgramManager service is not available"); + return null; } - // Add any additional file info we might want - fileInfo.put("architecture", program.getLanguage().getProcessor().toString()); - fileInfo.put("endian", program.getLanguage().isBigEndian() ? "big" : "little"); - - info.put("file", fileInfo); - } else { - info.put("file", null); - info.put("status", "No file open"); + Program program = pm.getCurrentProgram(); + Msg.debug(this, "Got current program: " + (program != null ? program.getName() : "null")); + return program; + } + catch (Exception e) { + Msg.error(this, "Error getting current program", e); + return null; } - - // Add server metadata - info.put("port", port); - info.put("isBaseInstance", isBaseInstance); - - return info; } + private void sendResponse(HttpExchange exchange, String response) throws IOException { byte[] bytes = response.getBytes(StandardCharsets.UTF_8); @@ -629,18 +1196,6 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { } } - /** - * Send a JSON response to the client - */ - private void sendJsonResponse(HttpExchange exchange, JSONObject json) throws IOException { - String jsonString = json.toJSONString(); - byte[] bytes = jsonString.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 int findAvailablePort() { int basePort = 8192; diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF index bf0595e..bc8e073 100644 --- a/src/main/resources/META-INF/MANIFEST.MF +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -3,4 +3,4 @@ Plugin-Class: eu.starsong.ghidra.GhydraMCP Plugin-Name: GhydraMCP Plugin-Version: 11.3.1 Plugin-Author: LaurieWired, Teal Bauer -Plugin-Description: Expose multiple Ghidra tools to MCP servers +Plugin-Description: Expose multiple Ghidra tools to MCP servers with variable management diff --git a/src/main/resources/Module.manifest b/src/main/resources/Module.manifest index 00f5a85..8be1e50 100644 --- a/src/main/resources/Module.manifest +++ b/src/main/resources/Module.manifest @@ -1,2 +1,3 @@ -GHIDRA_MODULE_NAME=GhydraMCP -GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents. +Manifest-Version: 1.0 +GHIDRA_MODULE_NAME: GhydraMCP +GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.