Make multi-headed and more RESTful
This commit is contained in:
parent
76e8355252
commit
33be44d7ef
91
README.md
91
README.md
@ -7,19 +7,28 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
# GhydraMCP
|
||||||
# ghidraMCP
|
GhydraMCP is an Model Context Protocol server for allowing LLMs to autonomously reverse engineer applications. It exposes numerous tools from core Ghidra functionality to MCP clients.
|
||||||
ghidraMCP is an Model Context Protocol server for allowing LLMs to autonomously reverse engineer applications. It exposes numerous tools from core Ghidra functionality to MCP clients.
|
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/36080514-f227-44bd-af84-78e29ee1d7f9
|
https://github.com/user-attachments/assets/36080514-f227-44bd-af84-78e29ee1d7f9
|
||||||
|
|
||||||
|
GhydraMCP is based on [GhidraMCP by Laurie Wired](https://github.com/LaurieWired/GhidraMCP/).
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
MCP Server + Ghidra Plugin
|
MCP Server + Ghidra Plugin
|
||||||
|
|
||||||
- Decompile and analyze binaries in Ghidra
|
- Full program analysis capabilities:
|
||||||
- Automatically rename methods and data
|
- Decompile functions to C code
|
||||||
- List methods, classes, imports, and exports
|
- Cross-reference analysis
|
||||||
|
- Data type propagation
|
||||||
|
- Interactive reverse engineering:
|
||||||
|
- Rename functions, variables, and data
|
||||||
|
- Add comments and labels
|
||||||
|
- Modify data types
|
||||||
|
- Program exploration:
|
||||||
|
- List functions, classes, namespaces
|
||||||
|
- View imports, exports, segments
|
||||||
|
- Search by name or pattern
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@ -29,14 +38,14 @@ MCP Server + Ghidra Plugin
|
|||||||
- MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
|
- MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
|
||||||
|
|
||||||
## Ghidra
|
## Ghidra
|
||||||
First, download the latest [release](https://github.com/LaurieWired/GhidraMCP/releases) from this repository. This contains the Ghidra plugin and Python MCP client. Then, you can directly import the plugin into Ghidra.
|
First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/releases) from this repository. This contains the Ghidra plugin and Python MCP client. Then, you can directly import the plugin into Ghidra.
|
||||||
|
|
||||||
1. Run Ghidra
|
1. Run Ghidra
|
||||||
2. Select `File` -> `Install Extensions`
|
2. Select `File` -> `Install Extensions`
|
||||||
3. Click the `+` button
|
3. Click the `+` button
|
||||||
4. Select the `GhidraMCP-1-0.zip` (or your chosen version) from the downloaded release
|
4. Select the `GhydraMCP-1-1.zip` (or your chosen version) from the downloaded release
|
||||||
5. Restart Ghidra
|
5. Restart Ghidra
|
||||||
6. Make sure the GhidraMCPPlugin is enabled in `File` -> `Configure` -> `Developer`
|
6. Make sure the GhydraMCPPlugin is enabled in `File` -> `Configure` -> `Developer`
|
||||||
|
|
||||||
Video Installation Guide:
|
Video Installation Guide:
|
||||||
|
|
||||||
@ -47,35 +56,63 @@ https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3
|
|||||||
|
|
||||||
## MCP Clients
|
## MCP Clients
|
||||||
|
|
||||||
Theoretically, any MCP client should work with ghidraMCP. Two examples are given below.
|
Theoretically, any MCP client should work with GhydraMCP. Two examples are given below.
|
||||||
|
|
||||||
## Example 1: Claude Desktop
|
## API Reference
|
||||||
To set up Claude Desktop as a Ghidra MCP client, go to `Claude` -> `Settings` -> `Developer` -> `Edit Config` -> `claude_desktop_config.json` and add the following:
|
|
||||||
|
|
||||||
|
### Available Tools
|
||||||
|
|
||||||
|
**Program Analysis**:
|
||||||
|
- `list_methods`: List all functions (params: offset, limit)
|
||||||
|
- `list_classes`: List all classes/namespaces (params: offset, limit)
|
||||||
|
- `decompile_function`: Get decompiled C code (params: name)
|
||||||
|
- `rename_function`: Rename a function (params: old_name, new_name)
|
||||||
|
- `rename_data`: Rename data at address (params: address, new_name)
|
||||||
|
- `list_segments`: View memory segments (params: offset, limit)
|
||||||
|
- `list_imports`: List imported symbols (params: offset, limit)
|
||||||
|
- `list_exports`: List exported functions (params: offset, limit)
|
||||||
|
- `list_namespaces`: Show namespaces (params: offset, limit)
|
||||||
|
- `list_data_items`: View data labels (params: offset, limit)
|
||||||
|
- `search_functions_by_name`: Find functions (params: query, offset, limit)
|
||||||
|
|
||||||
|
**Instance Management**:
|
||||||
|
- `list_instances`: List active Ghidra instances (no params)
|
||||||
|
- `register_instance`: Register new instance (params: port, url)
|
||||||
|
- `unregister_instance`: Remove instance (params: port)
|
||||||
|
|
||||||
|
**Example Usage**:
|
||||||
|
```python
|
||||||
|
# Program analysis
|
||||||
|
client.use_tool("ghydra", "decompile_function", {"name": "main"})
|
||||||
|
|
||||||
|
# Instance management
|
||||||
|
client.use_tool("ghydra", "register_instance", {"port": 8192, "url": "http://localhost:8192/"})
|
||||||
|
client.use_tool("ghydra", "register_instance", {"port": 8193})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Setup
|
||||||
|
|
||||||
|
### Claude Desktop Configuration
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"ghidra": {
|
"ghydra": {
|
||||||
"command": "python",
|
"command": "python",
|
||||||
"args": [
|
"args": [
|
||||||
"/ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py"
|
"/ABSOLUTE_PATH_TO/bridge_mcp_hydra.py"
|
||||||
]
|
],
|
||||||
|
"env": {
|
||||||
|
"GHIDRA_HYDRA_HOST": "localhost" // Optional - defaults to localhost
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, edit this file directly:
|
### 5ire Configuration
|
||||||
```
|
1. Tool Key: ghydra
|
||||||
/Users/YOUR_USER/Library/Application Support/Claude/claude_desktop_config.json
|
2. Name: GhydraMCP
|
||||||
```
|
3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_hydra.py`
|
||||||
|
|
||||||
## Example 2: 5ire
|
|
||||||
Another MCP client that supports multiple models on the backend is [5ire](https://github.com/nanbingxyz/5ire). To set up GhidraMCP, open 5ire and go to `Tools` -> `New` and set the following configurations:
|
|
||||||
|
|
||||||
1. Tool Key: ghidra
|
|
||||||
2. Name: GhidraMCP
|
|
||||||
3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py`
|
|
||||||
|
|
||||||
# Building from Source
|
# Building from Source
|
||||||
Build with Maven by running:
|
Build with Maven by running:
|
||||||
@ -84,6 +121,6 @@ Build with Maven by running:
|
|||||||
|
|
||||||
The generated zip file includes the built Ghidra plugin and its resources. These files are required for Ghidra to recognize the new extension.
|
The generated zip file includes the built Ghidra plugin and its resources. These files are required for Ghidra to recognize the new extension.
|
||||||
|
|
||||||
- lib/GhidraMCP.jar
|
- lib/GhydraMCP.jar
|
||||||
- extensions.properties
|
- extensions.properties
|
||||||
- Module.manifest
|
- Module.manifest
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
# "requests==2.32.3",
|
# "requests==2.32.3",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
@ -19,11 +20,11 @@ DEFAULT_GHIDRA_HOST = "localhost"
|
|||||||
|
|
||||||
mcp = FastMCP("hydra-mcp")
|
mcp = FastMCP("hydra-mcp")
|
||||||
|
|
||||||
# Get host from command line or use default
|
# Get host from environment variable, command line, or use default
|
||||||
ghidra_host = DEFAULT_GHIDRA_HOST
|
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
ghidra_host = sys.argv[1]
|
ghidra_host = sys.argv[1]
|
||||||
print(f"Using Ghidra host: {ghidra_host}")
|
print(f"Using Ghidra host: {ghidra_host}")
|
||||||
|
|
||||||
def get_instance_url(port: int) -> str:
|
def get_instance_url(port: int) -> str:
|
||||||
"""Get URL for a Ghidra instance by port"""
|
"""Get URL for a Ghidra instance by port"""
|
||||||
@ -65,6 +66,26 @@ def safe_get(port: int, endpoint: str, params: dict = None) -> list:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"Request failed: {str(e)}"]
|
return [f"Request failed: {str(e)}"]
|
||||||
|
|
||||||
|
def safe_put(port: int, endpoint: str, data: dict) -> str:
|
||||||
|
"""Perform a PUT request to a specific Ghidra instance"""
|
||||||
|
try:
|
||||||
|
url = f"{get_instance_url(port)}/{endpoint}"
|
||||||
|
response = requests.put(url, data=data, timeout=5)
|
||||||
|
response.encoding = 'utf-8'
|
||||||
|
if response.ok:
|
||||||
|
return response.text.strip()
|
||||||
|
elif response.status_code == 404 and port != DEFAULT_GHIDRA_PORT:
|
||||||
|
# Try falling back to default instance
|
||||||
|
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||||
|
else:
|
||||||
|
return f"Error {response.status_code}: {response.text.strip()}"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
if port != DEFAULT_GHIDRA_PORT:
|
||||||
|
return safe_put(DEFAULT_GHIDRA_PORT, endpoint, data)
|
||||||
|
return "Error: Failed to connect to Ghidra instance"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Request failed: {str(e)}"
|
||||||
|
|
||||||
def safe_post(port: int, endpoint: str, data: dict | str) -> str:
|
def safe_post(port: int, endpoint: str, data: dict | str) -> str:
|
||||||
"""Perform a POST request to a specific Ghidra instance"""
|
"""Perform a POST request to a specific Ghidra instance"""
|
||||||
try:
|
try:
|
||||||
@ -129,25 +150,32 @@ def unregister_instance(port: int) -> str:
|
|||||||
return f"No instance found on port {port}"
|
return f"No instance found on port {port}"
|
||||||
|
|
||||||
# Updated tool implementations with port parameter
|
# Updated tool implementations with port parameter
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_methods(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:
|
||||||
return safe_get(port, "methods", {"offset": offset, "limit": limit})
|
"""List all functions with pagination"""
|
||||||
|
return safe_get(port, "functions", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
def list_classes(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||||
|
"""List all classes with pagination"""
|
||||||
return safe_get(port, "classes", {"offset": offset, "limit": limit})
|
return safe_get(port, "classes", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def decompile_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "") -> str:
|
def get_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "") -> str:
|
||||||
return safe_post(port, "decompile", name)
|
"""Get decompiled code for a specific function"""
|
||||||
|
return safe_get(port, f"functions/{quote(name)}", {})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_function(port: int = DEFAULT_GHIDRA_PORT, old_name: str = "", new_name: str = "") -> str:
|
def update_function(port: int = DEFAULT_GHIDRA_PORT, name: str = "", new_name: str = "") -> str:
|
||||||
return safe_post(port, "renameFunction", {"oldName": old_name, "newName": new_name})
|
"""Rename a function"""
|
||||||
|
return safe_put(port, f"functions/{quote(name)}", {"newName": new_name})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str:
|
def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: str = "") -> str:
|
||||||
return safe_post(port, "renameData", {"address": address, "newName": new_name})
|
"""Rename data at specified address"""
|
||||||
|
return safe_put(port, "data", {"address": address, "newName": new_name})
|
||||||
|
|
||||||
@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:
|
||||||
@ -155,11 +183,11 @@ def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int =
|
|||||||
|
|
||||||
@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:
|
||||||
return safe_get(port, "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:
|
||||||
return safe_get(port, "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:
|
||||||
@ -173,7 +201,7 @@ def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int
|
|||||||
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:
|
||||||
if not query:
|
if not query:
|
||||||
return ["Error: query string is required"]
|
return ["Error: query string is required"]
|
||||||
return safe_get(port, "searchFunctions", {"query": query, "offset": offset, "limit": limit})
|
return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit})
|
||||||
|
|
||||||
# Handle graceful shutdown
|
# Handle graceful shutdown
|
||||||
import signal
|
import signal
|
||||||
12
pom.xml
12
pom.xml
@ -3,12 +3,12 @@
|
|||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.lauriewired</groupId>
|
<groupId>eu.starsong.ghidra</groupId>
|
||||||
<artifactId>HydraMCP</artifactId>
|
<artifactId>GhydraMCP</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>1.1</version>
|
<version>1.1</version>
|
||||||
<name>HydraMCP</name>
|
<name>GhydraMCP</name>
|
||||||
<url>https://github.com/LaurieWired/GhidraMCP</url>
|
<url>https://github.com/teal-bauer/GhydraMCP</url>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
|
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
|
||||||
</archive>
|
</archive>
|
||||||
<!-- Set a fixed name for the JAR without version -->
|
<!-- Set a fixed name for the JAR without version -->
|
||||||
<finalName>HydraMCP</finalName>
|
<finalName>GhydraMCP</finalName>
|
||||||
<!-- Exclude the App class -->
|
<!-- Exclude the App class -->
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>**/App.class</exclude>
|
<exclude>**/App.class</exclude>
|
||||||
@ -115,7 +115,7 @@
|
|||||||
</descriptors>
|
</descriptors>
|
||||||
|
|
||||||
<!-- The name of the final zip -->
|
<!-- The name of the final zip -->
|
||||||
<finalName>HydraMCP-${project.version}</finalName>
|
<finalName>GhydraMCP-${project.version}</finalName>
|
||||||
|
|
||||||
<!-- Don't append the assembly ID -->
|
<!-- Don't append the assembly ID -->
|
||||||
<appendAssemblyId>false</appendAssemblyId>
|
<appendAssemblyId>false</appendAssemblyId>
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
mcp==1.5.0
|
|
||||||
requests==2.32.3
|
|
||||||
@ -17,24 +17,24 @@
|
|||||||
|
|
||||||
<fileSets>
|
<fileSets>
|
||||||
<!-- 1) Copy extension.properties and Module.manifest into the top level
|
<!-- 1) Copy extension.properties and Module.manifest into the top level
|
||||||
of a folder named GhidraMCP/ (the actual extension folder). -->
|
of a folder named GhydraMCP/ (the actual extension folder). -->
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>src/main/resources</directory>
|
<directory>src/main/resources</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>extension.properties</include>
|
<include>extension.properties</include>
|
||||||
<include>Module.manifest</include>
|
<include>Module.manifest</include>
|
||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>HydraMCP</outputDirectory>
|
<outputDirectory>GhydraMCP</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
|
||||||
<!-- 2) Copy your built plugin JAR into HydraMCP/lib -->
|
<!-- 2) Copy your built plugin JAR into GhydraMCP/lib -->
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>${project.build.directory}</directory>
|
<directory>${project.build.directory}</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<!-- Use the finalized JAR name from the maven-jar-plugin -->
|
<!-- Use the finalized JAR name from the maven-jar-plugin -->
|
||||||
<include>HydraMCP.jar</include>
|
<include>GhydraMCP.jar</include>
|
||||||
</includes>
|
</includes>
|
||||||
<outputDirectory>HydraMCP/lib</outputDirectory>
|
<outputDirectory>GhydraMCP/lib</outputDirectory>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
</fileSets>
|
</fileSets>
|
||||||
</assembly>
|
</assembly>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package com.lauriewired;
|
package eu.starsong.ghidra;
|
||||||
|
|
||||||
import ghidra.framework.plugintool.*;
|
import ghidra.framework.plugintool.*;
|
||||||
import ghidra.framework.plugintool.util.*;
|
|
||||||
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;
|
||||||
@ -38,17 +37,16 @@ import java.util.concurrent.atomic.*;
|
|||||||
shortDescription = "HTTP server plugin",
|
shortDescription = "HTTP server plugin",
|
||||||
description = "Starts an embedded HTTP server to expose program data."
|
description = "Starts an embedded HTTP server to expose program data."
|
||||||
)
|
)
|
||||||
public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||||
|
|
||||||
private static final Map<Integer, HydraMCPPlugin> activeInstances = new ConcurrentHashMap<>();
|
private static final Map<Integer, GhydraMCPPlugin> activeInstances = new ConcurrentHashMap<>();
|
||||||
private static final AtomicInteger nextPort = new AtomicInteger(8192);
|
|
||||||
private static final Object baseInstanceLock = new Object();
|
private static final Object baseInstanceLock = new Object();
|
||||||
|
|
||||||
private HttpServer server;
|
private HttpServer server;
|
||||||
private int port;
|
private int port;
|
||||||
private boolean isBaseInstance = false;
|
private boolean isBaseInstance = false;
|
||||||
|
|
||||||
public HydraMCPPlugin(PluginTool tool) {
|
public GhydraMCPPlugin(PluginTool tool) {
|
||||||
super(tool);
|
super(tool);
|
||||||
|
|
||||||
// Find available port
|
// Find available port
|
||||||
@ -64,8 +62,8 @@ public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log to both console and log file
|
// Log to both console and log file
|
||||||
Msg.info(this, "HydraMCPPlugin loaded on port " + port);
|
Msg.info(this, "GhydraMCPPlugin loaded on port " + port);
|
||||||
System.out.println("[HydraMCP] Plugin loaded on port " + port);
|
System.out.println("[GhydraMCP] Plugin loaded on port " + port);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startServer();
|
startServer();
|
||||||
@ -82,85 +80,127 @@ public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
server = HttpServer.create(new InetSocketAddress(port), 0);
|
server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
|
|
||||||
// Each listing endpoint uses offset & limit from query params:
|
// Each listing endpoint uses offset & limit from query params:
|
||||||
server.createContext("/methods", exchange -> {
|
// Function resources
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
server.createContext("/functions", exchange -> {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
sendResponse(exchange, getAllFunctionNames(offset, limit));
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
String query = qparams.get("query");
|
||||||
|
|
||||||
|
if (query != null && !query.isEmpty()) {
|
||||||
|
sendResponse(exchange, searchFunctionsByName(query, offset, limit));
|
||||||
|
} else {
|
||||||
|
sendResponse(exchange, getAllFunctionNames(offset, limit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.createContext("/functions/", exchange -> {
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
|
String name = path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
try {
|
||||||
|
name = java.net.URLDecoder.decode(name, StandardCharsets.UTF_8.name());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Msg.error(this, "Failed to decode function name", e);
|
||||||
|
exchange.sendResponseHeaders(400, -1); // Bad Request
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
|
sendResponse(exchange, decompileFunctionByName(name));
|
||||||
|
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
||||||
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
|
String newName = params.get("newName");
|
||||||
|
String response = renameFunction(name, newName)
|
||||||
|
? "Renamed successfully" : "Rename failed";
|
||||||
|
sendResponse(exchange, response);
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Class resources
|
||||||
server.createContext("/classes", exchange -> {
|
server.createContext("/classes", exchange -> {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
sendResponse(exchange, getAllClassNames(offset, limit));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
});
|
sendResponse(exchange, getAllClassNames(offset, limit));
|
||||||
|
} else {
|
||||||
server.createContext("/decompile", exchange -> {
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
}
|
||||||
sendResponse(exchange, decompileFunctionByName(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
server.createContext("/renameFunction", exchange -> {
|
|
||||||
Map<String, String> params = parsePostParams(exchange);
|
|
||||||
String response = renameFunction(params.get("oldName"), params.get("newName"))
|
|
||||||
? "Renamed successfully" : "Rename failed";
|
|
||||||
sendResponse(exchange, response);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.createContext("/renameData", exchange -> {
|
|
||||||
Map<String, String> params = parsePostParams(exchange);
|
|
||||||
renameDataAtAddress(params.get("address"), params.get("newName"));
|
|
||||||
sendResponse(exchange, "Rename data attempted");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Memory segments
|
||||||
server.createContext("/segments", exchange -> {
|
server.createContext("/segments", exchange -> {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
sendResponse(exchange, listSegments(offset, limit));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listSegments(offset, limit));
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.createContext("/imports", exchange -> {
|
// Symbol resources (imports/exports)
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
server.createContext("/symbols/imports", exchange -> {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
sendResponse(exchange, listImports(offset, limit));
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listImports(offset, limit));
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.createContext("/exports", exchange -> {
|
server.createContext("/symbols/exports", exchange -> {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
sendResponse(exchange, listExports(offset, limit));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listExports(offset, limit));
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Namespace resources
|
||||||
server.createContext("/namespaces", exchange -> {
|
server.createContext("/namespaces", exchange -> {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
sendResponse(exchange, listNamespaces(offset, limit));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listNamespaces(offset, limit));
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Data resources
|
||||||
server.createContext("/data", exchange -> {
|
server.createContext("/data", exchange -> {
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
if ("GET".equals(exchange.getRequestMethod())) {
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
sendResponse(exchange, listDefinedData(offset, limit));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
});
|
sendResponse(exchange, listDefinedData(offset, limit));
|
||||||
|
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
||||||
server.createContext("/searchFunctions", exchange -> {
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
Map<String, String> qparams = parseQueryParams(exchange);
|
renameDataAtAddress(params.get("address"), params.get("newName"));
|
||||||
String searchTerm = qparams.get("query");
|
sendResponse(exchange, "Rename data attempted");
|
||||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
} else {
|
||||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||||
sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Instance management endpoints
|
// Instance management endpoints
|
||||||
server.createContext("/instances", exchange -> {
|
server.createContext("/instances", exchange -> {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (Map.Entry<Integer, HydraMCPPlugin> entry : activeInstances.entrySet()) {
|
for (Map.Entry<Integer, GhydraMCPPlugin> entry : activeInstances.entrySet()) {
|
||||||
sb.append(entry.getKey()).append(": ")
|
sb.append(entry.getKey()).append(": ")
|
||||||
.append(entry.getValue().isBaseInstance ? "base" : "secondary")
|
.append(entry.getValue().isBaseInstance ? "base" : "secondary")
|
||||||
.append("\n");
|
.append("\n");
|
||||||
@ -192,9 +232,9 @@ public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
server.setExecutor(null);
|
server.setExecutor(null);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
server.start();
|
server.start();
|
||||||
Msg.info(this, "HydraMCP HTTP server started on port " + port);
|
Msg.info(this, "GhydraMCP HTTP server started on port " + port);
|
||||||
System.out.println("[HydraMCP] HTTP server started on port " + port);
|
System.out.println("[GhydraMCP] HTTP server started on port " + port);
|
||||||
}, "HydraMCP-HTTP-Server").start();
|
}, "GhydraMCP-HTTP-Server").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------
|
||||||
@ -542,7 +582,7 @@ public class HydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
|||||||
if (server != null) {
|
if (server != null) {
|
||||||
server.stop(0);
|
server.stop(0);
|
||||||
Msg.info(this, "HTTP server stopped on port " + port);
|
Msg.info(this, "HTTP server stopped on port " + port);
|
||||||
System.out.println("[HydraMCP] HTTP server stopped on port " + port);
|
System.out.println("[GhydraMCP] HTTP server stopped on port " + port);
|
||||||
}
|
}
|
||||||
activeInstances.remove(port);
|
activeInstances.remove(port);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -1,6 +1,6 @@
|
|||||||
Manifest-Version: 1.0
|
Manifest-Version: 1.0
|
||||||
Plugin-Class: com.lauriewired.GhidraMCP
|
Plugin-Class: eu.starsong.ghidra.GhydraMCP
|
||||||
Plugin-Name: GhidraMCP
|
Plugin-Name: GhydraMCP
|
||||||
Plugin-Version: 1.0
|
Plugin-Version: 1.1
|
||||||
Plugin-Author: LaurieWired
|
Plugin-Author: LaurieWired, Teal Bauer
|
||||||
Plugin-Description: A custom plugin by LaurieWired
|
Plugin-Description: Expose multiple Ghidra tools to MCP servers
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
GHIDRA_MODULE_NAME=GhidraMCP
|
GHIDRA_MODULE_NAME=GhydraMCP
|
||||||
GHIDRA_MODULE_DESC=An HTTP server plugin for Ghidra
|
GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
name=HydraMCP
|
name=GhydraMCP
|
||||||
description=A plugin that runs an embedded HTTP server to expose program data.
|
description=A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||||
author=LaurieWired
|
author=Laurie Wired, Teal Bauer
|
||||||
createdOn=2025-03-22
|
createdOn=2025-03-29
|
||||||
version=11.3.1
|
version=11.3.1
|
||||||
ghidraVersion=11.3.1
|
ghidraVersion=11.3.1
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.lauriewired;
|
package eu.starsong.ghidra;
|
||||||
|
|
||||||
import junit.framework.Test;
|
import junit.framework.Test;
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
Loading…
x
Reference in New Issue
Block a user