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.