Merge branch 'variable-mangling'
This commit is contained in:
commit
d4611377c8
52
CHANGELOG.md
Normal file
52
CHANGELOG.md
Normal 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
|
||||||
@ -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:
|
||||||
|
# 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:
|
||||||
|
print(f"Got response from root: {root_response.text}", file=sys.stderr)
|
||||||
|
root_data = root_response.json()
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
|
||||||
|
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"
|
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)
|
info_response = requests.get(info_url, timeout=2)
|
||||||
if info_response.ok:
|
if info_response.ok:
|
||||||
try:
|
try:
|
||||||
# Parse JSON response
|
|
||||||
info_data = info_response.json()
|
info_data = info_response.json()
|
||||||
|
|
||||||
# Extract relevant information
|
# Extract relevant information
|
||||||
project_info["project"] = info_data.get("project", "Unknown")
|
if "project" in info_data and info_data["project"]:
|
||||||
|
project_info["project"] = info_data["project"]
|
||||||
|
|
||||||
# Handle file information which is nested
|
# Handle file information
|
||||||
file_info = info_data.get("file", {})
|
file_info = info_data.get("file", {})
|
||||||
if file_info:
|
if isinstance(file_info, dict) and file_info.get("name"):
|
||||||
project_info["file"] = file_info.get("name", "")
|
project_info["file"] = file_info.get("name", "")
|
||||||
project_info["path"] = file_info.get("path", "")
|
project_info["path"] = file_info.get("path", "")
|
||||||
project_info["architecture"] = file_info.get("architecture", "")
|
project_info["architecture"] = file_info.get("architecture", "")
|
||||||
project_info["endian"] = file_info.get("endian", "")
|
project_info["endian"] = file_info.get("endian", "")
|
||||||
except ValueError:
|
print(f"Info data parsed: {project_info}", file=sys.stderr)
|
||||||
# Not valid JSON
|
except Exception as e:
|
||||||
pass
|
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
110
pom.xml
@ -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>
|
||||||
|
|||||||
@ -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,26 +135,70 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we're dealing with a variables request
|
||||||
|
if (pathParts.length > 3 && "variables".equals(pathParts[3])) {
|
||||||
if ("GET".equals(exchange.getRequestMethod())) {
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
sendResponse(exchange, decompileFunctionByName(name));
|
// 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 {
|
||||||
|
// Simple function operations
|
||||||
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
|
sendResponse(exchange, decompileFunctionByName(functionName));
|
||||||
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
||||||
Map<String, String> params = parsePostParams(exchange);
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
String newName = params.get("newName");
|
String newName = params.get("newName");
|
||||||
String response = renameFunction(name, newName)
|
String response = renameFunction(functionName, newName)
|
||||||
? "Renamed successfully" : "Rename failed";
|
? "Renamed successfully" : "Rename failed";
|
||||||
sendResponse(exchange, response);
|
sendResponse(exchange, response);
|
||||||
} else {
|
} else {
|
||||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Class resources
|
// Class resources
|
||||||
@ -202,6 +276,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 -> {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -486,6 +656,427 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
// 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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user