Release v2.0.0-beta.5: Critical fixes for stable release
- Fixed memory_write() endpoint to use correct ProgramEndpoints path - Standardized all error responses to structured format with error codes - Enhanced instances_discover() to return both new and existing instances - Updated API version to 2005 for compatibility tracking - Verified all bridge-to-plugin endpoint mappings are correct - Confirmed route registration order follows proper specificity Ready for v2.0.0 stable release after final testing.
This commit is contained in:
parent
977791432f
commit
4379bea14f
@ -31,15 +31,15 @@ DEFAULT_GHIDRA_HOST = "localhost"
|
|||||||
QUICK_DISCOVERY_RANGE = range(DEFAULT_GHIDRA_PORT, DEFAULT_GHIDRA_PORT+10)
|
QUICK_DISCOVERY_RANGE = range(DEFAULT_GHIDRA_PORT, DEFAULT_GHIDRA_PORT+10)
|
||||||
FULL_DISCOVERY_RANGE = range(DEFAULT_GHIDRA_PORT, DEFAULT_GHIDRA_PORT+20)
|
FULL_DISCOVERY_RANGE = range(DEFAULT_GHIDRA_PORT, DEFAULT_GHIDRA_PORT+20)
|
||||||
|
|
||||||
BRIDGE_VERSION = "v2.0.0-beta.4"
|
BRIDGE_VERSION = "v2.0.0-beta.5"
|
||||||
REQUIRED_API_VERSION = 2004
|
REQUIRED_API_VERSION = 2005
|
||||||
|
|
||||||
current_instance_port = DEFAULT_GHIDRA_PORT
|
current_instance_port = DEFAULT_GHIDRA_PORT
|
||||||
|
|
||||||
instructions = """
|
instructions = """
|
||||||
GhydraMCP allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
|
GhydraMCP allows interacting with multiple Ghidra SRE instances. Ghidra SRE is a tool for reverse engineering and analyzing binaries, e.g. malware.
|
||||||
|
|
||||||
First, run `instances_discover()` to find open Ghidra instances. Then use `instances_use(port)` to set your working instance.
|
First, run `instances_discover()` to find all available Ghidra instances (both already known and newly discovered). Then use `instances_use(port)` to set your working instance.
|
||||||
|
|
||||||
The API is organized into namespaces for different types of operations:
|
The API is organized into namespaces for different types of operations:
|
||||||
- instances_* : For managing Ghidra instances
|
- instances_* : For managing Ghidra instances
|
||||||
@ -447,13 +447,18 @@ def register_instance(port: int, url: str = None) -> str:
|
|||||||
return f"Error: Could not connect to instance at {url}: {str(e)}"
|
return f"Error: Could not connect to instance at {url}: {str(e)}"
|
||||||
|
|
||||||
def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
|
def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
|
||||||
"""Internal function to discover Ghidra instances by scanning ports"""
|
"""Internal function to discover NEW Ghidra instances by scanning ports
|
||||||
|
|
||||||
|
This function only returns newly discovered instances that weren't already
|
||||||
|
in the active_instances registry. Use instances_discover() for a complete
|
||||||
|
list including already known instances.
|
||||||
|
"""
|
||||||
found_instances = []
|
found_instances = []
|
||||||
scan_host = host if host is not None else ghidra_host
|
scan_host = host if host is not None else ghidra_host
|
||||||
|
|
||||||
for port in port_range:
|
for port in port_range:
|
||||||
if port in active_instances:
|
if port in active_instances:
|
||||||
continue
|
continue # Skip already known instances
|
||||||
|
|
||||||
url = f"http://{scan_host}:{port}"
|
url = f"http://{scan_host}:{port}"
|
||||||
try:
|
try:
|
||||||
@ -597,7 +602,14 @@ def ghidra_instance(port: int = None) -> dict:
|
|||||||
result = response.get("result", {})
|
result = response.get("result", {})
|
||||||
|
|
||||||
if not isinstance(result, dict):
|
if not isinstance(result, dict):
|
||||||
return {"error": "Invalid response format from Ghidra instance"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": {
|
||||||
|
"code": "INVALID_RESPONSE",
|
||||||
|
"message": "Invalid response format from Ghidra instance"
|
||||||
|
},
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
instance_info = {
|
instance_info = {
|
||||||
"port": port,
|
"port": port,
|
||||||
@ -727,7 +739,14 @@ def function_info_by_address(port: int = None, address: str = None) -> dict:
|
|||||||
dict: Complete function information including signature, parameters, etc.
|
dict: Complete function information including signature, parameters, etc.
|
||||||
"""
|
"""
|
||||||
if not address:
|
if not address:
|
||||||
return {"error": "Address parameter is required"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": {
|
||||||
|
"code": "MISSING_PARAMETER",
|
||||||
|
"message": "Address parameter is required"
|
||||||
|
},
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
port = _get_instance_port(port)
|
port = _get_instance_port(port)
|
||||||
|
|
||||||
@ -739,10 +758,15 @@ def function_info_by_address(port: int = None, address: str = None) -> dict:
|
|||||||
if (not isinstance(simplified, dict) or
|
if (not isinstance(simplified, dict) or
|
||||||
not simplified.get("success", False) or
|
not simplified.get("success", False) or
|
||||||
"result" not in simplified):
|
"result" not in simplified):
|
||||||
error = {"error": "Could not get function information"}
|
return {
|
||||||
if isinstance(simplified, dict) and "error" in simplified:
|
"success": False,
|
||||||
error["error_details"] = simplified["error"]
|
"error": {
|
||||||
return error
|
"code": "FUNCTION_NOT_FOUND",
|
||||||
|
"message": "Could not get function information",
|
||||||
|
"details": simplified.get("error") if isinstance(simplified, dict) else None
|
||||||
|
},
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
# Return just the function data without API metadata
|
# Return just the function data without API metadata
|
||||||
return simplified["result"]
|
return simplified["result"]
|
||||||
@ -759,7 +783,14 @@ def function_info_by_name(port: int = None, name: str = None) -> dict:
|
|||||||
dict: Complete function information including signature, parameters, etc.
|
dict: Complete function information including signature, parameters, etc.
|
||||||
"""
|
"""
|
||||||
if not name:
|
if not name:
|
||||||
return {"error": "Name parameter is required"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": {
|
||||||
|
"code": "MISSING_PARAMETER",
|
||||||
|
"message": "Name parameter is required"
|
||||||
|
},
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
port = _get_instance_port(port)
|
port = _get_instance_port(port)
|
||||||
|
|
||||||
@ -771,10 +802,15 @@ def function_info_by_name(port: int = None, name: str = None) -> dict:
|
|||||||
if (not isinstance(simplified, dict) or
|
if (not isinstance(simplified, dict) or
|
||||||
not simplified.get("success", False) or
|
not simplified.get("success", False) or
|
||||||
"result" not in simplified):
|
"result" not in simplified):
|
||||||
error = {"error": "Could not get function information"}
|
return {
|
||||||
if isinstance(simplified, dict) and "error" in simplified:
|
"success": False,
|
||||||
error["error_details"] = simplified["error"]
|
"error": {
|
||||||
return error
|
"code": "FUNCTION_NOT_FOUND",
|
||||||
|
"message": "Could not get function information",
|
||||||
|
"details": simplified.get("error") if isinstance(simplified, dict) else None
|
||||||
|
},
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
# Return just the function data without API metadata
|
# Return just the function data without API metadata
|
||||||
return simplified["result"]
|
return simplified["result"]
|
||||||
@ -1132,14 +1168,44 @@ def instances_list() -> dict:
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def instances_discover(host: str = None) -> dict:
|
def instances_discover(host: str = None) -> dict:
|
||||||
"""Discover available Ghidra instances by scanning ports
|
"""Discover available Ghidra instances by scanning ports
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
host: Optional host to scan (default: configured ghidra_host)
|
host: Optional host to scan (default: configured ghidra_host)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Contains 'found' count and 'instances' list with discovery results
|
dict: Contains 'found' count, 'new_instances' count, and 'instances' list with all available instances
|
||||||
"""
|
"""
|
||||||
return _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5)
|
# Get newly discovered instances
|
||||||
|
discovery_result = _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5)
|
||||||
|
new_instances = discovery_result.get("instances", [])
|
||||||
|
new_count = len(new_instances)
|
||||||
|
|
||||||
|
# Get all currently known instances (including ones that were already registered)
|
||||||
|
all_instances = []
|
||||||
|
with instances_lock:
|
||||||
|
for port, info in active_instances.items():
|
||||||
|
instance_info = {
|
||||||
|
"port": port,
|
||||||
|
"url": info["url"],
|
||||||
|
"project": info.get("project", ""),
|
||||||
|
"file": info.get("file", ""),
|
||||||
|
"plugin_version": info.get("plugin_version", "unknown"),
|
||||||
|
"api_version": info.get("api_version", "unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark if this was newly discovered in this call
|
||||||
|
instance_info["newly_discovered"] = any(inst["port"] == port for inst in new_instances)
|
||||||
|
|
||||||
|
all_instances.append(instance_info)
|
||||||
|
|
||||||
|
# Sort by port for consistent ordering
|
||||||
|
all_instances.sort(key=lambda x: x["port"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"found": len(all_instances), # Total instances available
|
||||||
|
"new_instances": new_count, # How many were newly discovered
|
||||||
|
"instances": all_instances # All available instances
|
||||||
|
}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def instances_register(port: int, url: str = None) -> str:
|
def instances_register(port: int, url: str = None) -> str:
|
||||||
@ -1597,7 +1663,8 @@ def memory_write(address: str, bytes_data: str, format: str = "hex", port: int =
|
|||||||
"format": format
|
"format": format
|
||||||
}
|
}
|
||||||
|
|
||||||
response = safe_patch(port, f"memory/{address}", payload)
|
# Memory write is handled by ProgramEndpoints, not MemoryEndpoints
|
||||||
|
response = safe_patch(port, f"programs/current/memory/{address}", payload)
|
||||||
return simplify_response(response)
|
return simplify_response(response)
|
||||||
|
|
||||||
# Xrefs tools
|
# Xrefs tools
|
||||||
@ -1713,7 +1780,10 @@ def data_create(address: str, data_type: str, size: int = None, port: int = None
|
|||||||
if not address or not data_type:
|
if not address or not data_type:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "Address and data_type parameters are required",
|
"error": {
|
||||||
|
"code": "MISSING_PARAMETER",
|
||||||
|
"message": "Address and data_type parameters are required"
|
||||||
|
},
|
||||||
"timestamp": int(time.time() * 1000)
|
"timestamp": int(time.time() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1802,7 +1872,10 @@ def data_delete(address: str, port: int = None) -> dict:
|
|||||||
if not address:
|
if not address:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "Address parameter is required",
|
"error": {
|
||||||
|
"code": "MISSING_PARAMETER",
|
||||||
|
"message": "Address parameter is required"
|
||||||
|
},
|
||||||
"timestamp": int(time.time() * 1000)
|
"timestamp": int(time.time() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1908,7 +1981,10 @@ def analysis_get_dataflow(address: str, direction: str = "forward", max_steps: i
|
|||||||
if not address:
|
if not address:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "Address parameter is required",
|
"error": {
|
||||||
|
"code": "MISSING_PARAMETER",
|
||||||
|
"message": "Address parameter is required"
|
||||||
|
},
|
||||||
"timestamp": int(time.time() * 1000)
|
"timestamp": int(time.time() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package eu.starsong.ghidra.api;
|
package eu.starsong.ghidra.api;
|
||||||
|
|
||||||
public class ApiConstants {
|
public class ApiConstants {
|
||||||
public static final String PLUGIN_VERSION = "v2.0.0-beta.4";
|
public static final String PLUGIN_VERSION = "v2.0.0-beta.5";
|
||||||
public static final int API_VERSION = 2004;
|
public static final int API_VERSION = 2005;
|
||||||
public static final int DEFAULT_PORT = 8192;
|
public static final int DEFAULT_PORT = 8192;
|
||||||
public static final int MAX_PORT_ATTEMPTS = 10;
|
public static final int MAX_PORT_ATTEMPTS = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user