Make multi-headed and more RESTful

This commit is contained in:
Teal Bauer 2025-03-29 22:53:59 +01:00
parent 76e8355252
commit 33be44d7ef
10 changed files with 240 additions and 137 deletions

View File

@ -7,19 +7,28 @@
![ghidra_MCP_logo](https://github.com/user-attachments/assets/4986d702-be3f-4697-acce-aea55cd79ad3) ![ghidra_MCP_logo](https://github.com/user-attachments/assets/4986d702-be3f-4697-acce-aea55cd79ad3)
# 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

View File

@ -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,8 +20,8 @@ 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}")
@ -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
View File

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

View File

@ -1,2 +0,0 @@
mcp==1.5.0
requests==2.32.3

View File

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

View File

@ -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
server.createContext("/functions", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); 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)); 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 -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, getAllClassNames(offset, limit)); sendResponse(exchange, getAllClassNames(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
}); });
server.createContext("/decompile", exchange -> { // Memory segments
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");
});
server.createContext("/segments", exchange -> { server.createContext("/segments", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, listSegments(offset, limit)); sendResponse(exchange, listSegments(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
}); });
server.createContext("/imports", exchange -> { // Symbol resources (imports/exports)
server.createContext("/symbols/imports", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, listImports(offset, limit)); sendResponse(exchange, listImports(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
}); });
server.createContext("/exports", exchange -> { server.createContext("/symbols/exports", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, listExports(offset, limit)); sendResponse(exchange, listExports(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
}); });
// Namespace resources
server.createContext("/namespaces", exchange -> { server.createContext("/namespaces", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, listNamespaces(offset, limit)); sendResponse(exchange, listNamespaces(offset, limit));
} else {
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
}
}); });
// Data resources
server.createContext("/data", exchange -> { server.createContext("/data", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> qparams = parseQueryParams(exchange); Map<String, String> qparams = parseQueryParams(exchange);
int offset = parseIntOrDefault(qparams.get("offset"), 0); int offset = parseIntOrDefault(qparams.get("offset"), 0);
int limit = parseIntOrDefault(qparams.get("limit"), 100); int limit = parseIntOrDefault(qparams.get("limit"), 100);
sendResponse(exchange, listDefinedData(offset, limit)); sendResponse(exchange, listDefinedData(offset, limit));
}); } else if ("PUT".equals(exchange.getRequestMethod())) {
Map<String, String> params = parsePostParams(exchange);
server.createContext("/searchFunctions", exchange -> { renameDataAtAddress(params.get("address"), params.get("newName"));
Map<String, String> qparams = parseQueryParams(exchange); sendResponse(exchange, "Rename data attempted");
String searchTerm = qparams.get("query"); } else {
int offset = parseIntOrDefault(qparams.get("offset"), 0); exchange.sendResponseHeaders(405, -1); // Method Not Allowed
int limit = parseIntOrDefault(qparams.get("limit"), 100); }
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();

View File

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

View File

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

View File

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

View File

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