Merge branch 'variable-mangling'

This commit is contained in:
Teal Bauer 2025-04-02 18:54:36 +02:00
commit d4611377c8
6 changed files with 1018 additions and 115 deletions

52
CHANGELOG.md Normal file
View File

@ -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

View File

@ -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)

110
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>eu.starsong.ghidra</groupId>
<artifactId>GhydraMCP</artifactId>
<packaging>jar</packaging>
<version>11.3.1</version>
<version>${revision}</version>
<name>GhydraMCP</name>
<url>https://github.com/teal-bauer/GhydraMCP</url>
@ -16,10 +16,19 @@
<ghidra.jar.location>${project.basedir}/lib</ghidra.jar.location>
<maven.deploy.skip>true</maven.deploy.skip>
<maven.install.skip>true</maven.install.skip>
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>
<revision>dev-SNAPSHOT</revision>
</properties>
<dependencies>
<!-- Ghidra JARs as system-scoped dependencies for runtime -->
<!-- JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Ghidra JARs as system-scoped dependencies -->
<dependency>
<groupId>ghidra</groupId>
<artifactId>Generic</artifactId>
@ -70,13 +79,6 @@
<systemPath>${ghidra.jar.location}/Base.jar</systemPath>
</dependency>
<!-- JSON Simple for JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
@ -87,8 +89,24 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- Set Java version -->
<!-- Resources plugin to handle filtering -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -101,13 +119,74 @@
</configuration>
</plugin>
<!-- Git Commit ID plugin to generate version from git -->
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<version>5.0.0</version>
<executions>
<execution>
<id>get-git-info</id>
<phase>initialize</phase>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
<includeOnlyProperties>
<includeOnlyProperty>git.commit.id.abbrev</includeOnlyProperty>
<includeOnlyProperty>git.commit.time</includeOnlyProperty>
<includeOnlyProperty>git.closest.tag.name</includeOnlyProperty>
<includeOnlyProperty>git.build.version</includeOnlyProperty>
</includeOnlyProperties>
<commitIdGenerationMode>full</commitIdGenerationMode>
</configuration>
</plugin>
<!-- Set revision property from git info -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>set-revision-from-git</id>
<phase>initialize</phase>
<goals>
<goal>regex-property</goal>
</goals>
<configuration>
<name>revision</name>
<value>${git.commit.id.abbrev}-${maven.build.timestamp}</value>
<regex>.*</regex>
<replacement>$0</replacement>
<failIfNoMatch>false</failIfNoMatch>
</configuration>
</execution>
</executions>
</plugin>
<!-- Use custom MANIFEST.MF -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
<manifest>
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Implementation-Title>GhydraMCP</Implementation-Title>
<Implementation-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Implementation-Version>
<Plugin-Class>eu.starsong.ghidra.GhydraMCP</Plugin-Class>
<Plugin-Name>GhydraMCP</Plugin-Name>
<Plugin-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Plugin-Version>
<Plugin-Author>LaurieWired, Teal Bauer</Plugin-Author>
<Plugin-Description>Expose multiple Ghidra tools to MCP servers with variable management</Plugin-Description>
</manifestEntries>
</archive>
<finalName>GhydraMCP</finalName>
<excludes>
@ -134,7 +213,7 @@
<descriptors>
<descriptor>src/assembly/ghidra-extension.xml</descriptor>
</descriptors>
<finalName>GhydraMCP-${project.version}</finalName>
<finalName>GhydraMCP-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
@ -150,7 +229,7 @@
<descriptors>
<descriptor>src/assembly/complete-package.xml</descriptor>
</descriptors>
<finalName>GhydraMCP-Complete-${project.version}</finalName>
<finalName>GhydraMCP-Complete-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
@ -186,7 +265,14 @@
<configuration>
<failOnWarning>false</failOnWarning>
<ignoredUnusedDeclaredDependencies>
<ignoredUnusedDeclaredDependency>ghidra:Generic</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:SoftwareModeling</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Project</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Decompiler</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Utility</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Base</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>junit:junit</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies>
<ignoredSystemDependencies>
<ignoredSystemDependency>ghidra:*</ignoredSystemDependency>

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String> 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<String> variables = new ArrayList<>();
Iterator<HighSymbol> 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<String> 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<HighSymbol> 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<String> 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<HighSymbol> 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<String> 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<String> 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<HighSymbol> 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<DataType> 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;

View File

@ -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

View File

@ -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.