diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..868268ac
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,61 @@
+name: Build with Maven
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ GHIDRA_VERSION: 11.3.2
+ GHIDRA_DATE: 20250415
+ GHIDRA_LIBS: >-
+ Features/Base/lib/Base.jar
+ Features/Decompiler/lib/Decompiler.jar
+ Framework/Docking/lib/Docking.jar
+ Framework/Generic/lib/Generic.jar
+ Framework/Project/lib/Project.jar
+ Framework/SoftwareModeling/lib/SoftwareModeling.jar
+ Framework/Utility/lib/Utility.jar
+ Framework/Gui/lib/Gui.jar
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Download Ghidra
+ run: |
+ wget --no-verbose -O ghidra.zip https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ env.GHIDRA_VERSION }}_build/ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC_${{ env.GHIDRA_DATE }}.zip
+ 7z x -bd ghidra.zip
+
+ - name: Copy Ghidra libs
+ run: |
+ mkdir -p ./lib
+ for libfile in ${{ env.GHIDRA_LIBS }}
+ do echo "Copying ${libfile} to lib/"
+ cp ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC/Ghidra/${libfile} ./lib/
+ done
+
+ - name: Build with Maven
+ run: mvn clean package assembly:single
+
+ - name: Assemble release directory
+ run: |
+ mkdir release
+ cp target/GhidraMCP-*-SNAPSHOT.zip release/
+ cp bridge_mcp_ghidra.py release/
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: GhidraMCP-artifact
+ path: |
+ release/*
diff --git a/.gitignore b/.gitignore
index 0701e633..2bdf632f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,6 @@ mvnw.cmd
hs_err_pid*
replay_pid*
+# Third party JAR files from Ghidra
+lib/*.jar
+
diff --git a/README.md b/README.md
index 08890f99..b5b92bc0 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3
## MCP Clients
-Theoretically, any MCP client should work with ghidraMCP. Two examples are given below.
+Theoretically, any MCP client should work with ghidraMCP. Three examples are given below.
## Example 1: Claude Desktop
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:
@@ -99,7 +99,16 @@ Another MCP client that supports multiple models on the backend is [5ire](https:
3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py`
# Building from Source
-Build with Maven by running:
+1. Copy the following files from your Ghidra directory to this project's `lib/` directory:
+- `Ghidra/Features/Base/lib/Base.jar`
+- `Ghidra/Features/Decompiler/lib/Decompiler.jar`
+- `Ghidra/Framework/Docking/lib/Docking.jar`
+- `Ghidra/Framework/Generic/lib/Generic.jar`
+- `Ghidra/Framework/Project/lib/Project.jar`
+- `Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar`
+- `Ghidra/Framework/Utility/lib/Utility.jar`
+- `Ghidra/Framework/Gui/lib/Gui.jar`
+2. Build with Maven by running:
`mvn clean package assembly:single`
diff --git a/bridge_mcp_ghidra.py b/bridge_mcp_ghidra.py
index e171a7ad..c492562d 100644
--- a/bridge_mcp_ghidra.py
+++ b/bridge_mcp_ghidra.py
@@ -10,6 +10,7 @@
import requests
import argparse
import logging
+from urllib.parse import urljoin
from mcp.server.fastmcp import FastMCP
@@ -29,7 +30,7 @@ def safe_get(endpoint: str, params: dict = None) -> list:
if params is None:
params = {}
- url = f"{ghidra_server_url}/{endpoint}"
+ url = urljoin(ghidra_server_url, endpoint)
try:
response = requests.get(url, params=params, timeout=5)
@@ -43,10 +44,11 @@ def safe_get(endpoint: str, params: dict = None) -> list:
def safe_post(endpoint: str, data: dict | str) -> str:
try:
+ url = urljoin(ghidra_server_url, endpoint)
if isinstance(data, dict):
- response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data, timeout=5)
+ response = requests.post(url, data=data, timeout=5)
else:
- response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data.encode("utf-8"), timeout=5)
+ response = requests.post(url, data=data.encode("utf-8"), timeout=5)
response.encoding = 'utf-8'
if response.ok:
return response.text.strip()
@@ -222,6 +224,69 @@ def set_local_variable_type(function_address: str, variable_name: str, new_type:
"""
return safe_post("set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type})
+@mcp.tool()
+def get_xrefs_to(address: str, offset: int = 0, limit: int = 100) -> list:
+ """
+ Get all references to the specified address (xref to).
+
+ Args:
+ address: Target address in hex format (e.g. "0x1400010a0")
+ offset: Pagination offset (default: 0)
+ limit: Maximum number of references to return (default: 100)
+
+ Returns:
+ List of references to the specified address
+ """
+ return safe_get("xrefs_to", {"address": address, "offset": offset, "limit": limit})
+
+@mcp.tool()
+def get_xrefs_from(address: str, offset: int = 0, limit: int = 100) -> list:
+ """
+ Get all references from the specified address (xref from).
+
+ Args:
+ address: Source address in hex format (e.g. "0x1400010a0")
+ offset: Pagination offset (default: 0)
+ limit: Maximum number of references to return (default: 100)
+
+ Returns:
+ List of references from the specified address
+ """
+ return safe_get("xrefs_from", {"address": address, "offset": offset, "limit": limit})
+
+@mcp.tool()
+def get_function_xrefs(name: str, offset: int = 0, limit: int = 100) -> list:
+ """
+ Get all references to the specified function by name.
+
+ Args:
+ name: Function name to search for
+ offset: Pagination offset (default: 0)
+ limit: Maximum number of references to return (default: 100)
+
+ Returns:
+ List of references to the specified function
+ """
+ return safe_get("function_xrefs", {"name": name, "offset": offset, "limit": limit})
+
+@mcp.tool()
+def list_strings(offset: int = 0, limit: int = 2000, filter: str = None) -> list:
+ """
+ List all defined strings in the program with their addresses.
+
+ Args:
+ offset: Pagination offset (default: 0)
+ limit: Maximum number of strings to return (default: 2000)
+ filter: Optional filter to match within string content
+
+ Returns:
+ List of strings with their addresses
+ """
+ params = {"offset": offset, "limit": limit}
+ if filter:
+ params["filter"] = filter
+ return safe_get("strings", params)
+
def main():
parser = argparse.ArgumentParser(description="MCP server for Ghidra")
parser.add_argument("--ghidra-server", type=str, default=DEFAULT_GHIDRA_SERVER,
@@ -234,6 +299,8 @@ def main():
help="Transport protocol for MCP, default: stdio")
args = parser.parse_args()
+ # Use the global variable to ensure it's properly updated
+ global ghidra_server_url
if args.ghidra_server:
ghidra_server_url = args.ghidra_server
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 00000000..c96a04f0
--- /dev/null
+++ b/lib/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index aa7c31dc..6452d96e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,56 +15,56 @@
ghidra
Generic
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Generic.jar
ghidra
SoftwareModeling
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/SoftwareModeling.jar
ghidra
Project
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Project.jar
ghidra
Docking
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Docking.jar
ghidra
Decompiler
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Decompiler.jar
ghidra
Utility
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Utility.jar
ghidra
Base
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Base.jar
ghidra
Gui
- 11.3.1
+ 11.3.2
system
${project.basedir}/lib/Gui.jar
diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/GhidraMCPPlugin.java
index 0716c2e8..88583bab 100644
--- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java
+++ b/src/main/java/com/lauriewired/GhidraMCPPlugin.java
@@ -7,6 +7,10 @@
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.*;
+import ghidra.program.model.symbol.ReferenceManager;
+import ghidra.program.model.symbol.Reference;
+import ghidra.program.model.symbol.ReferenceIterator;
+import ghidra.program.model.symbol.RefType;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.LocalSymbolMap;
@@ -305,6 +309,38 @@ private void startServer() throws IOException {
sendResponse(exchange, responseMsg.toString());
});
+ server.createContext("/xrefs_to", exchange -> {
+ Map qparams = parseQueryParams(exchange);
+ String address = qparams.get("address");
+ int offset = parseIntOrDefault(qparams.get("offset"), 0);
+ int limit = parseIntOrDefault(qparams.get("limit"), 100);
+ sendResponse(exchange, getXrefsTo(address, offset, limit));
+ });
+
+ server.createContext("/xrefs_from", exchange -> {
+ Map qparams = parseQueryParams(exchange);
+ String address = qparams.get("address");
+ int offset = parseIntOrDefault(qparams.get("offset"), 0);
+ int limit = parseIntOrDefault(qparams.get("limit"), 100);
+ sendResponse(exchange, getXrefsFrom(address, offset, limit));
+ });
+
+ server.createContext("/function_xrefs", exchange -> {
+ Map qparams = parseQueryParams(exchange);
+ String name = qparams.get("name");
+ int offset = parseIntOrDefault(qparams.get("offset"), 0);
+ int limit = parseIntOrDefault(qparams.get("limit"), 100);
+ sendResponse(exchange, getFunctionXrefs(name, offset, limit));
+ });
+
+ server.createContext("/strings", exchange -> {
+ Map qparams = parseQueryParams(exchange);
+ int offset = parseIntOrDefault(qparams.get("offset"), 0);
+ int limit = parseIntOrDefault(qparams.get("limit"), 100);
+ String filter = qparams.get("filter");
+ sendResponse(exchange, listDefinedStrings(offset, limit, filter));
+ });
+
server.setExecutor(null);
new Thread(() -> {
try {
@@ -494,7 +530,7 @@ private boolean renameFunction(String oldName, String newName) {
Msg.error(this, "Error renaming function", e);
}
finally {
- program.endTransaction(tx, successFlag.get());
+ successFlag.set(program.endTransaction(tx, successFlag.get()));
}
});
}
@@ -616,7 +652,7 @@ private String renameVariableInFunction(String functionName, String oldVarName,
Msg.error(this, "Failed to rename variable", e);
}
finally {
- program.endTransaction(tx, true);
+ successFlag.set(program.endTransaction(tx, true));
}
});
} catch (InterruptedException | InvocationTargetException e) {
@@ -839,7 +875,7 @@ private boolean setCommentAtAddress(String addressStr, String comment, int comme
} catch (Exception e) {
Msg.error(this, "Error setting " + transactionName.toLowerCase(), e);
} finally {
- program.endTransaction(tx, success.get());
+ success.set(program.endTransaction(tx, success.get()));
}
});
} catch (InterruptedException | InvocationTargetException e) {
@@ -1206,6 +1242,177 @@ private void updateVariableType(Program program, HighSymbol symbol, DataType dat
}
}
+ /**
+ * Get all references to a specific address (xref to)
+ */
+ private String getXrefsTo(String addressStr, int offset, int limit) {
+ Program program = getCurrentProgram();
+ if (program == null) return "No program loaded";
+ if (addressStr == null || addressStr.isEmpty()) return "Address is required";
+
+ try {
+ Address addr = program.getAddressFactory().getAddress(addressStr);
+ ReferenceManager refManager = program.getReferenceManager();
+
+ ReferenceIterator refIter = refManager.getReferencesTo(addr);
+
+ List refs = new ArrayList<>();
+ while (refIter.hasNext()) {
+ Reference ref = refIter.next();
+ Address fromAddr = ref.getFromAddress();
+ RefType refType = ref.getReferenceType();
+
+ Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr);
+ String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : "";
+
+ refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName()));
+ }
+
+ return paginateList(refs, offset, limit);
+ } catch (Exception e) {
+ return "Error getting references to address: " + e.getMessage();
+ }
+ }
+
+ /**
+ * Get all references from a specific address (xref from)
+ */
+ private String getXrefsFrom(String addressStr, int offset, int limit) {
+ Program program = getCurrentProgram();
+ if (program == null) return "No program loaded";
+ if (addressStr == null || addressStr.isEmpty()) return "Address is required";
+
+ try {
+ Address addr = program.getAddressFactory().getAddress(addressStr);
+ ReferenceManager refManager = program.getReferenceManager();
+
+ Reference[] references = refManager.getReferencesFrom(addr);
+
+ List refs = new ArrayList<>();
+ for (Reference ref : references) {
+ Address toAddr = ref.getToAddress();
+ RefType refType = ref.getReferenceType();
+
+ String targetInfo = "";
+ Function toFunc = program.getFunctionManager().getFunctionAt(toAddr);
+ if (toFunc != null) {
+ targetInfo = " to function " + toFunc.getName();
+ } else {
+ Data data = program.getListing().getDataAt(toAddr);
+ if (data != null) {
+ targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName());
+ }
+ }
+
+ refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName()));
+ }
+
+ return paginateList(refs, offset, limit);
+ } catch (Exception e) {
+ return "Error getting references from address: " + e.getMessage();
+ }
+ }
+
+ /**
+ * Get all references to a specific function by name
+ */
+ private String getFunctionXrefs(String functionName, int offset, int limit) {
+ Program program = getCurrentProgram();
+ if (program == null) return "No program loaded";
+ if (functionName == null || functionName.isEmpty()) return "Function name is required";
+
+ try {
+ List refs = new ArrayList<>();
+ FunctionManager funcManager = program.getFunctionManager();
+ for (Function function : funcManager.getFunctions(true)) {
+ if (function.getName().equals(functionName)) {
+ Address entryPoint = function.getEntryPoint();
+ ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint);
+
+ while (refIter.hasNext()) {
+ Reference ref = refIter.next();
+ Address fromAddr = ref.getFromAddress();
+ RefType refType = ref.getReferenceType();
+
+ Function fromFunc = funcManager.getFunctionContaining(fromAddr);
+ String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : "";
+
+ refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName()));
+ }
+ }
+ }
+
+ if (refs.isEmpty()) {
+ return "No references found to function: " + functionName;
+ }
+
+ return paginateList(refs, offset, limit);
+ } catch (Exception e) {
+ return "Error getting function references: " + e.getMessage();
+ }
+ }
+
+/**
+ * List all defined strings in the program with their addresses
+ */
+ private String listDefinedStrings(int offset, int limit, String filter) {
+ Program program = getCurrentProgram();
+ if (program == null) return "No program loaded";
+
+ List lines = new ArrayList<>();
+ DataIterator dataIt = program.getListing().getDefinedData(true);
+
+ while (dataIt.hasNext()) {
+ Data data = dataIt.next();
+
+ if (data != null && isStringData(data)) {
+ String value = data.getValue() != null ? data.getValue().toString() : "";
+
+ if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) {
+ String escapedValue = escapeString(value);
+ lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue));
+ }
+ }
+ }
+
+ return paginateList(lines, offset, limit);
+ }
+
+ /**
+ * Check if the given data is a string type
+ */
+ private boolean isStringData(Data data) {
+ if (data == null) return false;
+
+ DataType dt = data.getDataType();
+ String typeName = dt.getName().toLowerCase();
+ return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode");
+ }
+
+ /**
+ * Escape special characters in a string for display
+ */
+ private String escapeString(String input) {
+ if (input == null) return "";
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c >= 32 && c < 127) {
+ sb.append(c);
+ } else if (c == '\n') {
+ sb.append("\\n");
+ } else if (c == '\r') {
+ sb.append("\\r");
+ } else if (c == '\t') {
+ sb.append("\\t");
+ } else {
+ sb.append(String.format("\\x%02x", (int)c & 0xFF));
+ }
+ }
+ return sb.toString();
+ }
+
/**
* Resolves a data type by name, handling common types and pointer types
* @param dtm The data type manager
diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties
index b0c5ea9b..3ca8018c 100644
--- a/src/main/resources/extension.properties
+++ b/src/main/resources/extension.properties
@@ -2,5 +2,5 @@ name=GhidraMCP
description=A plugin that runs an embedded HTTP server to expose program data.
author=LaurieWired
createdOn=2025-03-22
-version=11.3.1
-ghidraVersion=11.3.1
\ No newline at end of file
+version=11.3.2
+ghidraVersion=11.3.2
\ No newline at end of file