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 os
import signal
import sys import sys
import time
import requests
import threading import threading
from typing import Dict import time
from threading import Lock from threading import Lock
from typing import Dict
import requests
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
# Track active Ghidra instances (port -> info dict) # Track active Ghidra instances (port -> info dict)
@ -146,26 +148,54 @@ def register_instance(port: int, url: str = None) -> str:
project_info = {"url": url} project_info = {"url": url}
try: try:
info_url = f"{url}/info" # Try the root endpoint first
info_response = requests.get(info_url, timeout=2) root_url = f"{url}/"
if info_response.ok: 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: try:
# Parse JSON response print(f"Got response from root: {root_response.text}", file=sys.stderr)
info_data = info_response.json() root_data = root_response.json()
# Extract relevant information # Extract basic information from root
project_info["project"] = info_data.get("project", "Unknown") 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 print(f"Root data parsed: {project_info}", file=sys.stderr)
file_info = info_data.get("file", {}) except Exception as e:
if file_info: print(f"Error parsing root info: {e}", file=sys.stderr)
project_info["file"] = file_info.get("name", "") else:
project_info["path"] = file_info.get("path", "") print(f"Root endpoint returned {root_response.status_code}", file=sys.stderr)
project_info["architecture"] = file_info.get("architecture", "")
project_info["endian"] = file_info.get("endian", "") # If we don't have project info yet, try the /info endpoint as a fallback
except ValueError: if not project_info.get("project") and not project_info.get("file"):
# Not valid JSON info_url = f"{url}/info"
pass 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: except Exception:
# Non-critical, continue with registration even if project info fails # Non-critical, continue with registration even if project info fails
pass pass
@ -223,6 +253,7 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
# Updated tool implementations with port parameter # Updated tool implementations with port parameter
from urllib.parse import quote from urllib.parse import quote
@mcp.tool() @mcp.tool()
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all functions with pagination""" """List all functions with pagination"""
@ -250,33 +281,211 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st
@mcp.tool() @mcp.tool()
def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "segments", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "symbols/imports", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "symbols/exports", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "namespaces", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "data", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list: 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: if not query:
return ["Error: query string is required"] return ["Error: query string is required"]
return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit}) return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit})
# Handle graceful shutdown @mcp.tool()
import signal def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
import os """
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): def handle_sigint(signum, frame):
os._exit(0) os._exit(0)

110
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>eu.starsong.ghidra</groupId> <groupId>eu.starsong.ghidra</groupId>
<artifactId>GhydraMCP</artifactId> <artifactId>GhydraMCP</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>11.3.1</version> <version>${revision}</version>
<name>GhydraMCP</name> <name>GhydraMCP</name>
<url>https://github.com/teal-bauer/GhydraMCP</url> <url>https://github.com/teal-bauer/GhydraMCP</url>
@ -16,10 +16,19 @@
<ghidra.jar.location>${project.basedir}/lib</ghidra.jar.location> <ghidra.jar.location>${project.basedir}/lib</ghidra.jar.location>
<maven.deploy.skip>true</maven.deploy.skip> <maven.deploy.skip>true</maven.deploy.skip>
<maven.install.skip>true</maven.install.skip> <maven.install.skip>true</maven.install.skip>
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>
<revision>dev-SNAPSHOT</revision>
</properties> </properties>
<dependencies> <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> <dependency>
<groupId>ghidra</groupId> <groupId>ghidra</groupId>
<artifactId>Generic</artifactId> <artifactId>Generic</artifactId>
@ -70,13 +79,6 @@
<systemPath>${ghidra.jar.location}/Base.jar</systemPath> <systemPath>${ghidra.jar.location}/Base.jar</systemPath>
</dependency> </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 --> <!-- Test dependencies -->
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
@ -87,8 +89,24 @@
</dependencies> </dependencies>
<build> <build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins> <plugins>
<!-- Set Java version --> <!-- 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> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
@ -101,13 +119,74 @@
</configuration> </configuration>
</plugin> </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 --> <!-- Use custom MANIFEST.MF -->
<plugin> <plugin>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version> <version>3.2.2</version>
<configuration> <configuration>
<archive> <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> </archive>
<finalName>GhydraMCP</finalName> <finalName>GhydraMCP</finalName>
<excludes> <excludes>
@ -134,7 +213,7 @@
<descriptors> <descriptors>
<descriptor>src/assembly/ghidra-extension.xml</descriptor> <descriptor>src/assembly/ghidra-extension.xml</descriptor>
</descriptors> </descriptors>
<finalName>GhydraMCP-${project.version}</finalName> <finalName>GhydraMCP-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId> <appendAssemblyId>false</appendAssemblyId>
</configuration> </configuration>
</execution> </execution>
@ -150,7 +229,7 @@
<descriptors> <descriptors>
<descriptor>src/assembly/complete-package.xml</descriptor> <descriptor>src/assembly/complete-package.xml</descriptor>
</descriptors> </descriptors>
<finalName>GhydraMCP-Complete-${project.version}</finalName> <finalName>GhydraMCP-Complete-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
<appendAssemblyId>false</appendAssemblyId> <appendAssemblyId>false</appendAssemblyId>
</configuration> </configuration>
</execution> </execution>
@ -186,7 +265,14 @@
<configuration> <configuration>
<failOnWarning>false</failOnWarning> <failOnWarning>false</failOnWarning>
<ignoredUnusedDeclaredDependencies> <ignoredUnusedDeclaredDependencies>
<ignoredUnusedDeclaredDependency>ghidra:Generic</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:SoftwareModeling</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Project</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency> <ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Decompiler</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Utility</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Base</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>junit:junit</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies> </ignoredUnusedDeclaredDependencies>
<ignoredSystemDependencies> <ignoredSystemDependencies>
<ignoredSystemDependency>ghidra:*</ignoredSystemDependency> <ignoredSystemDependency>ghidra:*</ignoredSystemDependency>

View File

@ -4,13 +4,26 @@ import ghidra.framework.plugintool.*;
import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.framework.main.ApplicationLevelPlugin;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
import ghidra.program.model.address.GlobalNamespace; 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.listing.*;
import ghidra.program.model.mem.MemoryBlock; 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.program.model.symbol.*;
import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults; 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.plugin.PluginCategoryNames;
import ghidra.app.services.ProgramManager; import ghidra.app.services.ProgramManager;
import ghidra.app.util.demangler.DemanglerUtil;
import ghidra.framework.model.Project; import ghidra.framework.model.Project;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginInfo;
@ -35,6 +48,23 @@ import java.util.concurrent.atomic.*;
// For JSON response handling // For JSON response handling
import org.json.simple.JSONObject; 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( @PluginInfo(
status = PluginStatus.RELEASED, status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME, packageName = ghidra.app.DeveloperPluginPackage.NAME,
@ -105,25 +135,69 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
server.createContext("/functions/", exchange -> { server.createContext("/functions/", exchange -> {
String path = exchange.getRequestURI().getPath(); 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 { try {
name = java.net.URLDecoder.decode(name, StandardCharsets.UTF_8.name()); functionName = java.net.URLDecoder.decode(functionName, StandardCharsets.UTF_8.name());
} catch (Exception e) { } catch (Exception e) {
Msg.error(this, "Failed to decode function name", e); Msg.error(this, "Failed to decode function name", e);
exchange.sendResponseHeaders(400, -1); // Bad Request exchange.sendResponseHeaders(400, -1); // Bad Request
return; return;
} }
if ("GET".equals(exchange.getRequestMethod())) { // Check if we're dealing with a variables request
sendResponse(exchange, decompileFunctionByName(name)); if (pathParts.length > 3 && "variables".equals(pathParts[3])) {
} else if ("PUT".equals(exchange.getRequestMethod())) { if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> params = parsePostParams(exchange); // List all variables in function
String newName = params.get("newName"); sendResponse(exchange, listVariablesInFunction(functionName));
String response = renameFunction(name, newName) } else if ("PUT".equals(exchange.getRequestMethod()) && pathParts.length > 4) {
? "Renamed successfully" : "Rename failed"; // Handle operations on a specific variable
sendResponse(exchange, response); 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 { } 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 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 // Instance management endpoints
server.createContext("/instances", exchange -> { server.createContext("/instances", exchange -> {
@ -213,21 +305,99 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
sendResponse(exchange, sb.toString()); sendResponse(exchange, sb.toString());
}); });
// Info endpoints - both root and /info for flexibility // Super simple info endpoint with guaranteed response
server.createContext("/info", exchange -> { server.createContext("/info", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) { try {
sendJsonResponse(exchange, getProjectInfo()); String response = "{\n";
} else { response += "\"port\": " + port + ",\n";
exchange.sendResponseHeaders(405, -1); // Method Not Allowed 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 -> { server.createContext("/", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) { try {
sendJsonResponse(exchange, getProjectInfo()); String response = "{\n";
} else { response += "\"port\": " + port + ",\n";
exchange.sendResponseHeaders(405, -1); // Method Not Allowed 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<>(); List<String> names = new ArrayList<>();
for (Function f : program.getFunctionManager().getFunctions(true)) { for (Function f : program.getFunctionManager().getFunctions(true)) {
names.add(f.getName()); names.add(f.getName() + " @ " + f.getEntryPoint());
} }
return paginateList(names, offset, limit); 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); 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. // Utility: parse query params, parse post params, pagination, etc.
@ -569,56 +1160,32 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
return sb.toString(); 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() { public Program getCurrentProgram() {
JSONObject info = new JSONObject(); if (tool == null) {
Program program = getCurrentProgram(); Msg.debug(this, "Tool is null when trying to get current program");
return null;
// Get project information if available
Project project = tool.getProject();
if (project != null) {
info.put("project", project.getName());
} else {
info.put("project", "Unknown");
} }
// Create file information object try {
JSONObject fileInfo = new JSONObject(); ProgramManager pm = tool.getService(ProgramManager.class);
if (pm == null) {
// Get current file information if available Msg.debug(this, "ProgramManager service is not available");
if (program != null) { return 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());
} }
// Add any additional file info we might want Program program = pm.getCurrentProgram();
fileInfo.put("architecture", program.getLanguage().getProcessor().toString()); Msg.debug(this, "Got current program: " + (program != null ? program.getName() : "null"));
fileInfo.put("endian", program.getLanguage().isBigEndian() ? "big" : "little"); return program;
}
info.put("file", fileInfo); catch (Exception e) {
} else { Msg.error(this, "Error getting current program", e);
info.put("file", null); return null;
info.put("status", "No file open");
} }
// Add server metadata
info.put("port", port);
info.put("isBaseInstance", isBaseInstance);
return info;
} }
private void sendResponse(HttpExchange exchange, String response) throws IOException { private void sendResponse(HttpExchange exchange, String response) throws IOException {
byte[] bytes = response.getBytes(StandardCharsets.UTF_8); 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() { private int findAvailablePort() {
int basePort = 8192; int basePort = 8192;

View File

@ -3,4 +3,4 @@ Plugin-Class: eu.starsong.ghidra.GhydraMCP
Plugin-Name: GhydraMCP Plugin-Name: GhydraMCP
Plugin-Version: 11.3.1 Plugin-Version: 11.3.1
Plugin-Author: LaurieWired, Teal Bauer 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 Manifest-Version: 1.0
GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents. GHIDRA_MODULE_NAME: GhydraMCP
GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.