Initial commit.

This commit is contained in:
Jason Tang
2025-07-08 20:56:39 -04:00
commit dd0af2738b
45 changed files with 5641 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
.antProperties.xml
.classpath
.gradle/
.settings/
bin/
data/
dist/
ghidra_scripts/
lib/
os/
lucene/
ghidrassist_analysis.db
ghidrassist_rlhf.db
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2025 Jason Tang
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View File
+383
View File
@@ -0,0 +1,383 @@
# GhidrAssistMCP
A powerful Ghidra extension that provides an MCP (Model Context Protocol) server, enabling AI assistants and other tools to interact with Ghidra's reverse engineering capabilities through a standardized API.
## Overview
GhidrAssistMCP bridges the gap between AI-powered analysis tools and Ghidra's comprehensive reverse engineering platform. By implementing the Model Context Protocol, this extension allows external AI assistants, automated analysis tools, and custom scripts to seamlessly interact with Ghidra's analysis capabilities.
### Key Features
- ** MCP Server Integration**: Full Model Context Protocol server implementation using official SDK
- ** 29 Built-in Tools**: Comprehensive set of analysis tools covering functions, data, cross-references, and more
- ** Configurable UI**: Easy-to-use interface for managing tools and monitoring activity
- ** Real-time Logging**: Track all MCP requests and responses with detailed logging
- ** Dynamic Tool Management**: Enable/disable tools individually with persistent settings
- ** Current Context Awareness**: Tools that understand Ghidra's current cursor position and active function
## Installation
### Prerequisites
- **Ghidra 11.4+** (tested with Ghidra 11.4 Public)
- **An MCP Client (Like GhidrAssist)**
### Binary Release (Recommended)
1. **Download the latest release**:
- Go to the [Releases page](https://github.com/jtang613/GhidrAssistMCP/releases)
- Download the latest `.zip` file (e.g., `GhidrAssistMCP-v1.0.0.zip`)
2. **Install the extension**:
- In Ghidra: **File → Install Extensions → Add Extension**
- Select the downloaded ZIP file
- Restart Ghidra when prompted
3. **Enable the plugin**:
- **File → Configure → Configure Plugins**
- Search for "GhidrAssistMCP"
- Check the box to enable the plugin
### Building from Source
1. **Clone the repository**:
```bash
git clone <repository-url>
cd GhidrAssistMCP
```
2. **Set Ghidra installation path**:
```bash
export GHIDRA_INSTALL_DIR=/path/to/your/ghidra/installation
```
3. **Build the extension**:
```bash
gradle buildExtension
```
4. **Install the extension**:
- Copy the generated ZIP file from `dist/` directory
- In Ghidra: **File → Install Extensions → Add Extension**
- Select the ZIP file and restart Ghidra
5. **Enable the plugin**:
- **File → Configure → Configure Plugins**
- Search for "GhidrAssistMCP"
- Check the box to enable the plugin
## Configuration
### Initial Setup
1. **Open the Control Panel**:
- Window → GhidrAssistMCP (or use the toolbar icon)
2. **Configure Server Settings**:
- **Host**: Default is `localhost`
- **Port**: Default is `8080`
- **Enable/Disable**: Toggle the MCP server on/off
### Tool Management
The Configuration tab allows you to:
- **View all available tools** (29 total)
- **Enable/disable individual tools** using checkboxes
- **Save configuration** to persist across sessions
- **Monitor tool status** in real-time
### Available Tools
#### Program Analysis
- `get_program_info` - Get basic program information
- `list_functions` - List all functions in the program
- `list_data` - List data definitions
- `list_strings` - List string references
- `list_imports` - List imported functions
- `list_exports` - List exported functions
- `list_segments` - List memory segments
- `list_namespaces` - List namespaces
- `list_classes` - List class definitions
- `list_methods` - List method definitions
#### Function Analysis
- `get_function_info` - Get detailed function information
- `get_function_by_address` - Find function at specific address
- `get_current_function` - Get function at cursor position
- `decompile_function` - Decompile function to C-like code
- `disassemble_function` - Get assembly disassembly
- `search_functions` - Search functions by name pattern
- `function_xrefs` - Get function cross-references
#### Location & Navigation
- `get_current_address` - Get current cursor address
- `xrefs_to` - Find references to an address
- `xrefs_from` - Find references from an address
#### Modification Tools
- `rename_function` - Rename functions
- `rename_function_by_address` - Rename function at specific address
- `rename_variable` - Rename variables
- `rename_data` - Rename data definitions
- `set_function_prototype` - Set function signatures
- `set_local_variable_type` - Set variable data types
- `set_disassembly_comment` - Add disassembly comments
- `set_decompiler_comment` - Add decompiler comments
#### Advanced Analysis
- `auto_create_struct` - Automatically create structures from variable usage patterns
## Usage Examples
### Basic Program Information
```json
{
"method": "tools/call",
"params": {
"name": "get_program_info"
}
}
```
### Function Analysis
```json
{
"method": "tools/call",
"params": {
"name": "get_function_info",
"arguments": {
"function_name": "main"
}
}
}
```
### Decompilation
```json
{
"method": "tools/call",
"params": {
"name": "decompile_function",
"arguments": {
"function_name": "encrypt_data"
}
}
}
```
### Structure Creation
```json
{
"method": "tools/call",
"params": {
"name": "auto_create_struct",
"arguments": {
"function_identifier": "0x00401000",
"variable_name": "ctx"
}
}
}
```
### Setting Function Prototype
```json
{
"method": "tools/call",
"params": {
"name": "set_function_prototype",
"arguments": {
"function_address": "0x00401000",
"prototype": "int main(int argc, char** argv)"
}
}
}
```
## Architecture
### Core Components
```
GhidrAssistMCP/
├── GhidrAssistMCPPlugin # Main plugin entry point
├── GhidrAssistMCPServer # HTTP/SSE MCP server
├── GhidrAssistMCPBackend # Tool management and execution
├── GhidrAssistMCPProvider # UI component provider
└── tools/ # Individual MCP tools
├── Analysis Tools/
├── Modification Tools/
└── Navigation Tools/
```
### MCP Protocol Implementation
- **Transport**: HTTP with Server-Sent Events (SSE)
- **Endpoints**:
- `GET /sse` - SSE connection for bidirectional communication
- `POST /message` - Message exchange endpoint
- **Tool Registration**: Dynamic tool discovery and registration
- **Session Management**: Stateful sessions with proper lifecycle management
### Plugin Architecture
1. **Observer Pattern**: Decoupled UI updates using event listeners
2. **Transaction Management**: Safe database operations with rollback support
3. **Tool Registry**: Dynamic tool registration with enable/disable capability
4. **Settings Persistence**: Configuration saved in Ghidra's settings system
5. **Thread Safety**: Proper Swing EDT handling for UI operations
## Development
### Project Structure
```
src/main/java/ghidrassistmcp/
├── GhidrAssistMCPPlugin.java # Main plugin class
├── GhidrAssistMCPProvider.java # UI provider with tabs
├── GhidrAssistMCPServer.java # MCP server implementation
├── GhidrAssistMCPBackend.java # Backend tool management
├── McpBackend.java # Backend interface
├── McpTool.java # Tool interface
├── McpEventListener.java # Event notification interface
└── tools/ # Tool implementations
├── ProgramInfoTool.java
├── ListFunctionsTool.java
├── DecompileFunctionTool.java
├── AutoCreateStructTool.java
└── ... (29 total tools)
```
### Adding New Tools
1. **Implement McpTool interface**:
```java
public class MyCustomTool implements McpTool {
@Override
public String getName() { return "my_custom_tool"; }
@Override
public String getDescription() { return "Description"; }
@Override
public McpSchema.JsonSchema getInputSchema() { /* ... */ }
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program program) {
// Implementation
}
}
```
2. **Register in backend**:
```java
// In GhidrAssistMCPBackend constructor
registerTool(new MyCustomTool());
```
### Build Commands
```bash
# Clean build
gradle clean
# Build extension
gradle buildExtension
# Build with specific Ghidra path
gradle -PGHIDRA_INSTALL_DIR=/path/to/ghidra buildExtension
# Debug build
gradle buildExtension --debug
```
### Dependencies
- **MCP SDK**: `io.modelcontextprotocol.sdk:mcp:0.10.0`
- **Jetty Server**: `11.0.20` (HTTP/SSE transport)
- **Jackson**: `2.17.0` (JSON processing)
- **Ghidra API**: Bundled with Ghidra installation
## Logging
### UI Logging
The **Log** tab provides real-time monitoring:
- **Session Events**: Server start/stop, program changes
- **Tool Requests**: `REQ: tool_name {parameters...}`
- **Tool Responses**: `RES: tool_name {response...}`
- **Error Messages**: Failed operations and diagnostics
### Console Logging
Detailed logging in Ghidra's console:
- Tool registration and initialization
- MCP server lifecycle events
- Database transaction operations
- Error stack traces and debugging information
## Troubleshooting
### Common Issues
**Server Won't Start**
- Check if port 8080 is available
- Verify Ghidra installation path
- Examine console logs for errors
**Tools Not Appearing**
- Ensure plugin is enabled
- Check Configuration tab for tool status
- Verify backend initialization in logs
**MCP Client Connection Issues**
- Confirm server is running (check GhidrAssistMCP window)
- Test connection: `curl http://localhost:8080/sse`
- Check firewall settings
**Tool Execution Failures**
- Verify program is loaded in Ghidra
- Check tool parameters are correct
- Review error messages in Log tab
### Debug Mode
Enable debug logging by adding to Ghidra startup:
```bash
-Dlog4j.logger.ghidrassistmcp=DEBUG
```
## Contributing
1. **Fork the repository**
2. **Create a feature branch**: `git checkout -b feature-name`
3. **Make your changes** with proper tests
4. **Follow code style**: Use existing patterns and conventions
5. **Submit a pull request** with detailed description
### Code Standards
- **Java 21+ features** where appropriate
- **Proper exception handling** with meaningful messages
- **Transaction safety** for all database operations
- **Thread safety** for UI operations
- **Comprehensive documentation** for public APIs
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- **NSA/Ghidra Team** for the excellent reverse engineering platform
- **Anthropic** for the Model Context Protocol specification
---
** Questions or Issues?**
Please open an issue on the project repository for bug reports, feature requests, or questions about usage and development.
+60
View File
@@ -0,0 +1,60 @@
/*
*
*/
// Builds a Ghidra Extension for a given Ghidra installation.
//
// An absolute path to the Ghidra installation directory must be supplied either by setting the
// GHIDRA_INSTALL_DIR environment variable or Gradle project property:
//
// > export GHIDRA_INSTALL_DIR=<Absolute path to Ghidra>
// > gradle
//
// or
//
// > gradle -PGHIDRA_INSTALL_DIR=<Absolute path to Ghidra>
//
// Gradle should be invoked from the directory of the project to build. Please see the
// application.gradle.version property in <GHIDRA_INSTALL_DIR>/Ghidra/application.properties
// for the correction version of Gradle to use for the Ghidra installation you specify.
//----------------------START "DO NOT MODIFY" SECTION------------------------------
def ghidraInstallDir
if (System.env.GHIDRA_INSTALL_DIR) {
ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR
}
else if (project.hasProperty("GHIDRA_INSTALL_DIR")) {
ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR")
}
else {
ghidraInstallDir = "<REPLACE>"
}
task distributeExtension {
group = "Ghidra"
apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle"
dependsOn ':buildExtension'
}
//----------------------END "DO NOT MODIFY" SECTION-------------------------------
repositories {
mavenCentral()
}
dependencies {
// MCP SDK core dependency
implementation 'io.modelcontextprotocol.sdk:mcp:0.10.0'
// Jetty embedded server for servlet container
implementation 'org.eclipse.jetty:jetty-server:11.0.20'
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
// Jackson for JSON processing (required by MCP)
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.0'
}
// Exclude additional files from the built extension
// Ex: buildExtension.exclude '.idea/**'
+5
View File
@@ -0,0 +1,5 @@
name=@extname@
description=GhidrAssistMCP - MCP (Model Context Protocol) server for Ghidra analysis capabilities
author=
createdOn=
version=@extversion@
+57
View File
@@ -0,0 +1,57 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
<!-- Uncomment and adjust fields to add help topic to help system's Table of Contents
<tocref id="Ghidra Functionality">
<tocdef id="HelpAnchor" text="My Feature" target="help/topics/my_topic/help.html" />
</tocref>
-->
</tocroot>
@@ -0,0 +1,23 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<META http-equiv="Content-Language" content="en-us">
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<META name="GENERATOR" content="Microsoft FrontPage 4.0">
<META name="ProgId" content="FrontPage.Editor.Document">
<TITLE>Skeleton Help File for a Module</TITLE>
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY>
<H1><a name="HelpAnchor"></a>Skeleton Help File for a Module</H1>
<P>This is a simple skeleton help topic. For a better description of what should and should not
go in here, see the "sample" Ghidra extension in the Extensions/Ghidra directory, or see your
favorite help topic. In general, language modules do not have their own help topics.</P>
</BODY>
</HTML>
@@ -0,0 +1,359 @@
/*
*
*/
package ghidrassistmcp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
import ghidrassistmcp.tools.AutoCreateStructTool;
import ghidrassistmcp.tools.DecompileFunctionTool;
import ghidrassistmcp.tools.DisassembleFunctionTool;
import ghidrassistmcp.tools.FunctionXrefsTool;
import ghidrassistmcp.tools.GetCurrentAddressTool;
import ghidrassistmcp.tools.GetCurrentFunctionTool;
import ghidrassistmcp.tools.GetFunctionByAddressTool;
import ghidrassistmcp.tools.GetFunctionInfoTool;
import ghidrassistmcp.tools.ListClassesTool;
import ghidrassistmcp.tools.ListDataTool;
import ghidrassistmcp.tools.ListExportsTool;
import ghidrassistmcp.tools.ListFunctionsTool;
import ghidrassistmcp.tools.ListImportsTool;
import ghidrassistmcp.tools.ListMethodsTool;
import ghidrassistmcp.tools.ListNamespacesTool;
import ghidrassistmcp.tools.ListSegmentsTool;
import ghidrassistmcp.tools.ListStringsTool;
import ghidrassistmcp.tools.ProgramInfoTool;
import ghidrassistmcp.tools.RenameDataTool;
import ghidrassistmcp.tools.RenameFunctionByAddressTool;
import ghidrassistmcp.tools.RenameFunctionTool;
import ghidrassistmcp.tools.RenameVariableTool;
import ghidrassistmcp.tools.SearchFunctionsTool;
import ghidrassistmcp.tools.SetDecompilerCommentTool;
import ghidrassistmcp.tools.SetDisassemblyCommentTool;
import ghidrassistmcp.tools.SetFunctionPrototypeTool;
import ghidrassistmcp.tools.SetLocalVariableTypeTool;
import ghidrassistmcp.tools.XrefsFromTool;
import ghidrassistmcp.tools.XrefsToTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* Implementation of the MCP backend that manages tools and program state.
*/
public class GhidrAssistMCPBackend implements McpBackend {
private final Map<String, McpTool> tools = new ConcurrentHashMap<>();
private final Map<String, Boolean> toolEnabledStates = new ConcurrentHashMap<>();
private volatile Program currentProgram;
private final List<McpEventListener> eventListeners = new CopyOnWriteArrayList<>();
private volatile GhidrAssistMCPPlugin plugin;
public GhidrAssistMCPBackend() {
// Register built-in tools
registerTool(new ProgramInfoTool());
registerTool(new ListFunctionsTool());
registerTool(new GetFunctionInfoTool());
registerTool(new DecompileFunctionTool());
registerTool(new DisassembleFunctionTool());
registerTool(new RenameFunctionTool());
registerTool(new RenameFunctionByAddressTool());
registerTool(new RenameVariableTool());
registerTool(new XrefsToTool());
registerTool(new XrefsFromTool());
registerTool(new ListMethodsTool());
registerTool(new ListSegmentsTool());
registerTool(new ListImportsTool());
registerTool(new ListExportsTool());
registerTool(new ListStringsTool());
registerTool(new SearchFunctionsTool());
registerTool(new GetFunctionByAddressTool());
registerTool(new GetCurrentAddressTool());
registerTool(new GetCurrentFunctionTool());
registerTool(new SetDisassemblyCommentTool());
registerTool(new SetDecompilerCommentTool());
registerTool(new ListDataTool());
registerTool(new ListNamespacesTool());
registerTool(new ListClassesTool());
registerTool(new RenameDataTool());
registerTool(new FunctionXrefsTool());
registerTool(new SetFunctionPrototypeTool());
registerTool(new SetLocalVariableTypeTool());
registerTool(new AutoCreateStructTool());
Msg.info(this, "GhidrAssistMCP Backend initialized with " + tools.size() + " tools");
}
@Override
public void registerTool(McpTool tool) {
tools.put(tool.getName(), tool);
// Tools are enabled by default when registered
toolEnabledStates.put(tool.getName(), true);
Msg.info(this, "Registered MCP tool: " + tool.getName());
}
@Override
public void unregisterTool(String toolName) {
McpTool removed = tools.remove(toolName);
toolEnabledStates.remove(toolName);
if (removed != null) {
Msg.info(this, "Unregistered MCP tool: " + toolName);
}
}
@Override
public List<McpSchema.Tool> getAvailableTools() {
List<McpSchema.Tool> toolList = new ArrayList<>();
for (McpTool tool : tools.values()) {
// Only include enabled tools in the available tools list
if (toolEnabledStates.getOrDefault(tool.getName(), true)) {
toolList.add(new McpSchema.Tool(
tool.getName(),
tool.getDescription(),
tool.getInputSchema()
));
}
}
// Sort tools alphabetically by name for consistent ordering
toolList.sort((a, b) -> a.name().compareToIgnoreCase(b.name()));
return toolList;
}
@Override
public McpSchema.CallToolResult callTool(String toolName, Map<String, Object> arguments) {
McpTool tool = tools.get(toolName);
if (tool == null) {
Msg.warn(this, "Tool not found: " + toolName);
return McpSchema.CallToolResult.builder()
.addTextContent("Tool not found: " + toolName)
.build();
}
// Check if tool is enabled
if (!toolEnabledStates.getOrDefault(toolName, true)) {
Msg.warn(this, "Tool is disabled: " + toolName);
return McpSchema.CallToolResult.builder()
.addTextContent("Tool is disabled: " + toolName)
.build();
}
try {
// Notify listeners of the request
notifyToolRequest(toolName, arguments);
Msg.info(this, "Executing tool: " + toolName);
// Use the enhanced execute method if plugin is available
McpSchema.CallToolResult result;
if (plugin != null) {
result = tool.execute(arguments, currentProgram, plugin);
} else {
result = tool.execute(arguments, currentProgram);
}
// Notify listeners of the response
notifyToolResponse(toolName, result);
return result;
} catch (Exception e) {
Msg.error(this, "Error executing tool " + toolName, e);
McpSchema.CallToolResult errorResult = McpSchema.CallToolResult.builder()
.addTextContent("Error executing tool " + toolName + ": " + e.getMessage())
.build();
// Notify listeners of the error response
notifyToolResponse(toolName, errorResult);
return errorResult;
}
}
@Override
public void onProgramActivated(Program program) {
this.currentProgram = program;
if (program != null) {
Msg.info(this, "Backend tracking program: " + program.getName());
}
}
@Override
public void onProgramDeactivated(Program program) {
this.currentProgram = null;
Msg.info(this, "Backend no longer tracking a program");
}
@Override
public McpSchema.Implementation getServerInfo() {
return new McpSchema.Implementation("ghidra-mcp", "1.0.0");
}
@Override
public McpSchema.ServerCapabilities getCapabilities() {
return McpSchema.ServerCapabilities.builder()
.tools(true)
.build();
}
/**
* Get the currently tracked program
*/
public Program getCurrentProgram() {
return currentProgram;
}
/**
* Add an event listener for MCP operations.
*/
public void addEventListener(McpEventListener listener) {
if (listener != null) {
eventListeners.add(listener);
Msg.info(this, "Added MCP event listener: " + listener.getClass().getSimpleName() + " (total listeners: " + eventListeners.size() + ")");
}
}
/**
* Remove an event listener.
*/
public void removeEventListener(McpEventListener listener) {
if (listener != null) {
eventListeners.remove(listener);
Msg.info(this, "Removed MCP event listener: " + listener.getClass().getSimpleName());
}
}
/**
* Set the plugin reference for tools that need UI access.
*/
public void setPlugin(GhidrAssistMCPPlugin plugin) {
this.plugin = plugin;
Msg.info(this, "Plugin reference set for UI-aware tool execution");
}
/**
* Notify listeners of a tool request.
*/
private void notifyToolRequest(String toolName, Map<String, Object> arguments) {
String params = arguments != null ? arguments.toString() : "{}";
if (params.length() > 60) {
params = params.substring(0, 57) + "...";
}
Msg.info(this, "Notifying " + eventListeners.size() + " listeners of tool request: " + toolName);
for (McpEventListener listener : eventListeners) {
try {
listener.onToolRequest(toolName, params);
} catch (Exception e) {
Msg.error(this, "Error notifying listener of tool request", e);
}
}
}
/**
* Notify listeners of a tool response.
*/
private void notifyToolResponse(String toolName, McpSchema.CallToolResult result) {
String response = "Empty response";
if (result != null && !result.content().isEmpty()) {
var firstContent = result.content().get(0);
if (firstContent instanceof McpSchema.TextContent) {
response = ((McpSchema.TextContent) firstContent).text();
if (response.length() > 60) {
response = response.substring(0, 57) + "...";
}
}
}
for (McpEventListener listener : eventListeners) {
try {
listener.onToolResponse(toolName, response);
} catch (Exception e) {
Msg.error(this, "Error notifying listener of tool response", e);
}
}
}
/**
* Notify listeners of a session event.
*/
private void notifySessionEvent(String event) {
for (McpEventListener listener : eventListeners) {
try {
listener.onSessionEvent(event);
} catch (Exception e) {
Msg.error(this, "Error notifying listener of session event", e);
}
}
}
/**
* Notify listeners of a general log message.
*/
private void notifyLogMessage(String message) {
for (McpEventListener listener : eventListeners) {
try {
listener.onLogMessage(message);
} catch (Exception e) {
Msg.error(this, "Error notifying listener of log message", e);
}
}
}
/**
* Set the enabled state of a tool.
*/
public void setToolEnabled(String toolName, boolean enabled) {
if (tools.containsKey(toolName)) {
toolEnabledStates.put(toolName, enabled);
Msg.info(this, "Tool " + toolName + " " + (enabled ? "enabled" : "disabled"));
}
}
/**
* Get the enabled state of a tool.
*/
public boolean isToolEnabled(String toolName) {
return toolEnabledStates.getOrDefault(toolName, true);
}
/**
* Get all tool enabled states.
*/
public Map<String, Boolean> getToolEnabledStates() {
return new HashMap<>(toolEnabledStates);
}
/**
* Update multiple tool enabled states at once.
*/
public void updateToolEnabledStates(Map<String, Boolean> newStates) {
for (Map.Entry<String, Boolean> entry : newStates.entrySet()) {
String toolName = entry.getKey();
if (tools.containsKey(toolName)) {
toolEnabledStates.put(toolName, entry.getValue());
}
}
Msg.info(this, "Updated enabled states for " + newStates.size() + " tools");
}
/**
* Get all tools (including disabled ones) for configuration purposes.
*/
public List<McpSchema.Tool> getAllTools() {
List<McpSchema.Tool> toolList = new ArrayList<>();
for (McpTool tool : tools.values()) {
toolList.add(new McpSchema.Tool(
tool.getName(),
tool.getDescription(),
tool.getInputSchema()
));
}
// Sort tools alphabetically by name for consistent ordering
toolList.sort((a, b) -> a.name().compareToIgnoreCase(b.name()));
return toolList;
}
}
@@ -0,0 +1,298 @@
/*
*
*/
package ghidrassistmcp;
import java.util.Map;
import ghidra.MiscellaneousPluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.Msg;
/**
* GhidrAssistMCP Plugin - Provides an MCP (Model Context Protocol) server for Ghidra analysis capabilities.
* Features a configurable UI with tool management and request logging.
*/
@PluginInfo(
status = PluginStatus.STABLE,
packageName = MiscellaneousPluginPackage.NAME,
category = PluginCategoryNames.COMMON,
shortDescription = "MCP Server for Ghidra",
description = "Provides a configurable MCP (Model Context Protocol) server for Ghidra analysis capabilities with tool management and logging."
)
public class GhidrAssistMCPPlugin extends ProgramPlugin {
private GhidrAssistMCPProvider provider;
private GhidrAssistMCPServer mcpServer;
private GhidrAssistMCPBackend backend;
// Current configuration
private String currentHost = "localhost";
private int currentPort = 8080;
private boolean serverEnabled = true;
// Current UI location tracking
private volatile ProgramLocation currentLocation1;
/**
* Plugin constructor.
*
* @param tool The plugin tool that this plugin is added to.
*/
public GhidrAssistMCPPlugin(PluginTool tool) {
super(tool);
// Create the UI provider but don't register it yet
provider = new GhidrAssistMCPProvider(tool, this);
}
@Override
public void init() {
super.init();
// Initialize MCP Backend first
backend = new GhidrAssistMCPBackend();
// Set plugin reference for UI-aware tools
backend.setPlugin(this);
// Register provider as event listener regardless of UI registration success
// This ensures logging works even if UI fails to register
if (provider != null) {
backend.addEventListener(provider);
provider.onBackendReady();
Msg.info(this, "Provider registered as event listener");
} else {
Msg.warn(this, "Provider is null - event listener not registered");
}
// Register the UI provider with the tool (separate from event listening)
if (provider != null) {
try {
tool.addComponentProvider(provider, true);
Msg.info(this, "Successfully registered UI provider");
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("was already added")) {
Msg.info(this, "UI provider already registered, continuing");
} else {
Msg.error(this, "Failed to register UI provider (non-fatal): " + e.getMessage());
// Don't set provider to null - keep it for event listening
}
} catch (Exception e) {
Msg.error(this, "Failed to register UI provider (non-fatal): " + e.getMessage());
// Don't set provider to null - keep it for event listening
}
}
// Start server with initial configuration
startServer();
if (provider != null) {
provider.logSession("Plugin initialized");
}
}
/**
* Apply new configuration from the UI.
*/
public void applyConfiguration(String host, int port, boolean enabled, Map<String, Boolean> toolStates) {
if (provider != null) {
provider.logMessage("Applying configuration: " + host + ":" + port + " enabled=" + enabled);
}
boolean needsRestart = false;
// Check if server settings changed
if (!host.equals(currentHost) || port != currentPort) {
currentHost = host;
currentPort = port;
needsRestart = true;
}
// Check if server enabled state changed
if (enabled != serverEnabled) {
serverEnabled = enabled;
needsRestart = true;
}
// Update tool enabled states
updateToolStates(toolStates);
// Restart server if needed
if (needsRestart) {
stopServer();
if (serverEnabled) {
startServer();
}
}
if (provider != null) {
provider.refreshToolsList();
}
}
private void updateToolStates(Map<String, Boolean> toolStates) {
if (backend != null) {
// Update backend with new tool states
backend.updateToolEnabledStates(toolStates);
if (provider != null) {
provider.logMessage("Updated tool states for " + toolStates.size() + " tools");
}
}
}
private void startServer() {
if (!serverEnabled) {
if (provider != null) {
provider.logSession("Server disabled - not starting");
}
return;
}
try {
mcpServer = new GhidrAssistMCPServer(currentPort, backend, provider);
mcpServer.start();
if (provider != null) {
provider.logSession("Server started on " + currentHost + ":" + currentPort);
}
Msg.info(this, "MCP Server started on port " + currentPort);
} catch (Exception e) {
if (provider != null) {
provider.logSession("Failed to start server: " + e.getMessage());
}
Msg.error(this, "Failed to start MCP Server", e);
}
}
private void stopServer() {
if (mcpServer != null) {
try {
mcpServer.stop();
if (provider != null) {
provider.logSession("Server stopped");
}
Msg.info(this, "MCP Server stopped");
} catch (Exception e) {
if (provider != null) {
provider.logSession("Error stopping server: " + e.getMessage());
}
Msg.error(this, "Failed to stop MCP Server", e);
}
mcpServer = null;
}
}
@Override
protected void programActivated(Program program) {
super.programActivated(program);
if (backend != null) {
backend.onProgramActivated(program);
if (provider != null) {
provider.logSession("Program activated: " + program.getName());
}
}
}
@Override
protected void locationChanged(ProgramLocation loc) {
super.locationChanged(loc);
this.currentLocation1 = loc;
if (provider != null && loc != null) {
provider.logMessage("Location changed to: " + loc.getAddress());
}
}
@Override
protected void programDeactivated(Program program) {
super.programDeactivated(program);
if (backend != null) {
backend.onProgramDeactivated(program);
if (provider != null) {
provider.logSession("Program deactivated: " + (program != null ? program.getName() : "null"));
}
}
}
@Override
protected void dispose() {
if (provider != null) {
provider.logSession("Plugin disposing");
// Remove provider as event listener
if (backend != null) {
backend.removeEventListener(provider);
}
try {
tool.removeComponentProvider(provider);
} catch (Exception e) {
Msg.error(this, "Error removing UI provider", e);
}
provider = null;
}
stopServer();
super.dispose();
}
/**
* Get the MCP backend for tool management.
*/
public GhidrAssistMCPBackend getBackend() {
return backend;
}
/**
* Get the current server configuration.
*/
public String getCurrentHost() {
return currentHost;
}
public int getCurrentPort() {
return currentPort;
}
public boolean isServerEnabled() {
return serverEnabled;
}
/**
* Get the current program.
*/
public Program getCurrentProgram() {
return super.getCurrentProgram();
}
/**
* Get the current UI address from the location tracker.
*/
public Address getCurrentAddress() {
if (currentLocation1 != null) {
return currentLocation1.getAddress();
}
return null;
}
/**
* Get the current function containing the UI cursor.
*/
public Function getCurrentFunction() {
Program program = getCurrentProgram();
Address address = getCurrentAddress();
if (program != null && address != null) {
FunctionManager functionManager = program.getFunctionManager();
return functionManager.getFunctionContaining(address);
}
return null;
}
}
@@ -0,0 +1,411 @@
/*
*
*/
package ghidrassistmcp;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import ghidra.framework.options.Options;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
import resources.Icons;
/**
* UI Provider for the GhidrAssistMCP plugin featuring configuration and logging tabs.
*/
public class GhidrAssistMCPProvider extends ComponentProvider implements McpEventListener {
private static final String NAME = "GhidrAssistMCP";
private static final String OWNER = "GhidrAssistMCPPlugin";
// Settings constants
private static final String SETTINGS_CATEGORY = "GhidrAssistMCP";
private static final String HOST_SETTING = "Server Host";
private static final String PORT_SETTING = "Server Port";
private static final String ENABLED_SETTING = "Server Enabled";
private static final String TOOL_PREFIX = "Tool.";
// Default values
private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 8080;
private static final boolean DEFAULT_ENABLED = true;
private final PluginTool tool;
private final GhidrAssistMCPPlugin plugin;
private JTabbedPane tabbedPane;
// Configuration tab components
private JTextField hostField;
private JSpinner portSpinner;
private JCheckBox enabledCheckBox;
private JTable toolsTable;
private DefaultTableModel toolsTableModel;
private JButton saveButton;
private Map<String, Boolean> toolEnabledStates;
// Log tab components
private JTextArea logTextArea;
private JButton clearButton;
private SimpleDateFormat dateFormat;
public GhidrAssistMCPProvider(PluginTool tool, GhidrAssistMCPPlugin plugin) {
super(tool, NAME, OWNER);
this.tool = tool;
this.plugin = plugin;
this.toolEnabledStates = new HashMap<>();
this.dateFormat = new SimpleDateFormat("HH:mm:ss");
buildComponent();
createActions();
// Don't load settings yet - wait for backend to be ready
setHelpLocation(new HelpLocation("GhidrAssistMCP", "GhidrAssistMCP_Provider"));
setVisible(true);
// Add focus listener to refresh tools when window receives focus
addFocusListener();
}
private void buildComponent() {
tabbedPane = new JTabbedPane();
// Configuration tab
JPanel configPanel = createConfigurationPanel();
tabbedPane.addTab("Configuration", configPanel);
// Log tab
JPanel logPanel = createLogPanel();
tabbedPane.addTab("Log", logPanel);
// Component will be returned by getComponent() method
}
private JPanel createConfigurationPanel() {
JPanel panel = new JPanel(new BorderLayout());
// Server settings panel
JPanel serverPanel = new JPanel(new GridBagLayout());
serverPanel.setBorder(BorderFactory.createTitledBorder("Server Settings"));
GridBagConstraints gbc = new GridBagConstraints();
// Host setting
gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST;
serverPanel.add(new JLabel("Host:"), gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
hostField = new JTextField(DEFAULT_HOST, 20);
serverPanel.add(hostField, gbc);
// Port setting
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
serverPanel.add(new JLabel("Port:"), gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
portSpinner = new JSpinner(new SpinnerNumberModel(DEFAULT_PORT, 1, 65535, 1));
serverPanel.add(portSpinner, gbc);
// Enabled setting
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.NONE;
enabledCheckBox = new JCheckBox("Enable MCP Server", DEFAULT_ENABLED);
serverPanel.add(enabledCheckBox, gbc);
panel.add(serverPanel, BorderLayout.NORTH);
// Tools panel
JPanel toolsPanel = new JPanel(new BorderLayout());
toolsPanel.setBorder(BorderFactory.createTitledBorder("MCP Tools"));
// Tools table
String[] columnNames = {"Enabled", "Tool Name", "Description"};
toolsTableModel = new DefaultTableModel(columnNames, 0) {
@Override
public Class<?> getColumnClass(int column) {
return column == 0 ? Boolean.class : String.class;
}
@Override
public boolean isCellEditable(int row, int column) {
return column == 0; // Only the checkbox column is editable
}
};
toolsTable = new JTable(toolsTableModel);
toolsTable.getColumnModel().getColumn(0).setMaxWidth(60);
toolsTable.getColumnModel().getColumn(1).setPreferredWidth(150);
toolsTable.getColumnModel().getColumn(2).setPreferredWidth(300);
JScrollPane scrollPane = new JScrollPane(toolsTable);
scrollPane.setPreferredSize(new Dimension(500, 200));
toolsPanel.add(scrollPane, BorderLayout.CENTER);
panel.add(toolsPanel, BorderLayout.CENTER);
// Save button
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
saveButton = new JButton("Save Configuration");
saveButton.addActionListener(new SaveConfigurationListener());
buttonPanel.add(saveButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
private JPanel createLogPanel() {
JPanel panel = new JPanel(new BorderLayout());
// Log text area
logTextArea = new JTextArea(20, 60);
logTextArea.setEditable(false);
logTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
logTextArea.setBackground(Color.BLACK);
logTextArea.setForeground(Color.GREEN);
JScrollPane scrollPane = new JScrollPane(logTextArea);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
panel.add(scrollPane, BorderLayout.CENTER);
// Clear button
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
clearButton = new JButton("Clear Log");
clearButton.addActionListener(e -> clearLog());
buttonPanel.add(clearButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
private void createActions() {
DockingAction refreshAction = new DockingAction("Refresh", OWNER) {
@Override
public void actionPerformed(ActionContext context) {
refreshToolsList();
}
};
refreshAction.setToolBarData(new ToolBarData(Icons.REFRESH_ICON, null));
refreshAction.setDescription("Refresh tools list");
refreshAction.setHelpLocation(new HelpLocation("GhidrAssistMCP", "Refresh"));
addLocalAction(refreshAction);
}
private void addFocusListener() {
// Add focus listener to the main component to refresh tools when window receives focus
tabbedPane.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
// Refresh tools list when the window receives focus
refreshToolsList();
}
@Override
public void focusLost(FocusEvent e) {
// No action needed when focus is lost
}
});
}
public void refreshToolsList() {
// Clear existing rows
toolsTableModel.setRowCount(0);
// Get tools from backend
if (plugin != null && plugin.getBackend() != null) {
try {
// Get all tools (including disabled ones) for configuration display
var tools = plugin.getBackend().getAllTools();
// Sync enabled states with backend
var backendStates = plugin.getBackend().getToolEnabledStates();
toolEnabledStates.putAll(backendStates);
for (var tool1 : tools) {
String toolName = tool1.name();
boolean enabled = toolEnabledStates.getOrDefault(toolName, true);
String description = tool1.description();
// Truncate long descriptions
if (description != null && description.length() > 80) {
description = description.substring(0, 77) + "...";
}
toolsTableModel.addRow(new Object[]{enabled, toolName, description});
}
logMessage("Refreshed tools list: " + tools.size() + " tools available");
} catch (Exception e) {
logMessage("Error refreshing tools list: " + e.getMessage());
}
} else {
logMessage("Backend not available yet - tools list empty");
}
}
private void loadSettings() {
Options options = tool.getOptions(SETTINGS_CATEGORY);
// Load server settings
String host = options.getString(HOST_SETTING, DEFAULT_HOST);
int port = options.getInt(PORT_SETTING, DEFAULT_PORT);
boolean enabled = options.getBoolean(ENABLED_SETTING, DEFAULT_ENABLED);
hostField.setText(host);
portSpinner.setValue(port);
enabledCheckBox.setSelected(enabled);
// Load tool enabled states
toolEnabledStates.clear();
if (plugin != null && plugin.getBackend() != null) {
try {
var tools = plugin.getBackend().getAllTools();
int loadedCount = 0;
for (var tool1 : tools) {
String toolName = tool1.name();
boolean toolEnabled = options.getBoolean(TOOL_PREFIX + toolName, true);
toolEnabledStates.put(toolName, toolEnabled);
loadedCount++;
}
// Update backend with loaded settings
plugin.getBackend().updateToolEnabledStates(toolEnabledStates);
logMessage("Loaded tool states from settings: " + loadedCount + " tools configured");
} catch (Exception e) {
logMessage("Error loading tool states: " + e.getMessage());
}
} else {
logMessage("Backend not available - skipping tool state loading");
}
// Note: Options change listening would require additional implementation
}
private void saveSettings() {
Options options = tool.getOptions(SETTINGS_CATEGORY);
// Save server settings
options.setString(HOST_SETTING, hostField.getText());
options.setInt(PORT_SETTING, (Integer) portSpinner.getValue());
options.setBoolean(ENABLED_SETTING, enabledCheckBox.isSelected());
// Save tool enabled states from table
int savedCount = 0;
for (int i = 0; i < toolsTableModel.getRowCount(); i++) {
String toolName = (String) toolsTableModel.getValueAt(i, 1);
boolean enabled = (Boolean) toolsTableModel.getValueAt(i, 0);
toolEnabledStates.put(toolName, enabled);
options.setBoolean(TOOL_PREFIX + toolName, enabled);
savedCount++;
}
logMessage("Saved tool states to settings: " + savedCount + " tools configured");
// Apply changes to the plugin
plugin.applyConfiguration(hostField.getText(), (Integer) portSpinner.getValue(),
enabledCheckBox.isSelected(), toolEnabledStates);
}
public void logMessage(String message) {
SwingUtilities.invokeLater(() -> {
String timestamp = dateFormat.format(new Date());
String logEntry = "[" + timestamp + "] " + message + "\n";
logTextArea.append(logEntry);
logTextArea.setCaretPosition(logTextArea.getDocument().getLength());
});
}
public void logRequest(String method, String params) {
String truncatedParams = params.length() > 60 ? params.substring(0, 77) + "..." : params;
logMessage("REQ: " + method + " " + truncatedParams.replace("\n", "\\n"));
}
public void logResponse(String method, String response) {
String truncatedResponse = response.length() > 60 ? response.substring(0, 77) + "..." : response;
logMessage("RES: " + method + " " + truncatedResponse.replace("\n", "\\n"));
}
public void logSession(String event) {
logMessage("SESSION: " + event);
}
private void clearLog() {
logTextArea.setText("");
}
// Add the required getComponent method
@Override
public JComponent getComponent() {
return tabbedPane;
}
private class SaveConfigurationListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
saveSettings();
logMessage("Configuration saved");
}
}
// Getters for current configuration
public String getHost() {
return hostField.getText();
}
public int getPort() {
return (Integer) portSpinner.getValue();
}
public boolean isServerEnabled() {
return enabledCheckBox.isSelected();
}
public Map<String, Boolean> getToolEnabledStates() {
return new HashMap<>(toolEnabledStates);
}
/**
* Public method to refresh tools list - can be called when backend becomes available
*/
public void onBackendReady() {
// Load settings now that backend is ready
loadSettings();
refreshToolsList();
logMessage("Backend ready - settings loaded and tools list refreshed");
}
// McpEventListener implementation
@Override
public void onToolRequest(String toolName, String parameters) {
System.out.println("DEBUG: onToolRequest called - " + toolName + " with params: " + parameters);
logRequest(toolName, parameters);
}
@Override
public void onToolResponse(String toolName, String response) {
System.out.println("DEBUG: onToolResponse called - " + toolName + " with response: " + response);
logResponse(toolName, response);
}
@Override
public void onSessionEvent(String event) {
System.out.println("DEBUG: onSessionEvent called - " + event);
logSession(event);
}
@Override
public void onLogMessage(String message) {
System.out.println("DEBUG: onLogMessage called - " + message);
logMessage(message);
}
}
@@ -0,0 +1,154 @@
/*
*
*/
package ghidrassistmcp;
import java.util.Map;
import java.util.function.BiFunction;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import ghidra.program.model.listing.Program;
import ghidra.util.Msg;
/**
* Refactored MCP Server implementation that uses the backend architecture.
* This class handles HTTP transport and delegates business logic to McpBackend.
*/
public class GhidrAssistMCPServer {
private final McpBackend backend;
private final GhidrAssistMCPProvider provider;
private Server jettyServer;
private final int port;
public GhidrAssistMCPServer(int port, McpBackend backend) {
this(port, backend, null);
}
public GhidrAssistMCPServer(int port, McpBackend backend, GhidrAssistMCPProvider provider) {
this.port = port;
this.backend = backend;
this.provider = provider;
}
public void start() throws Exception {
Msg.info(this, "Starting MCP Server initialization...");
try {
// Create Jetty server
Msg.info(this, "Creating Jetty server on port " + port);
jettyServer = new Server(port);
// Create servlet context
Msg.info(this, "Setting up servlet context");
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
jettyServer.setHandler(context);
// Create MCP transport provider using simple constructor
Msg.info(this, "Creating MCP transport provider");
ObjectMapper mapper = new ObjectMapper();
String messageEndpoint = "message";
HttpServletSseServerTransportProvider transportProvider =
new HttpServletSseServerTransportProvider(mapper, messageEndpoint);
// Build MCP server using backend for configuration
Msg.info(this, "Building MCP server with backend tools");
var serverBuilder = McpServer.sync(transportProvider)
.serverInfo(backend.getServerInfo())
.capabilities(backend.getCapabilities());
// Register each tool individually with its own handler
for (McpSchema.Tool toolSchema : backend.getAvailableTools()) {
String toolName = toolSchema.name();
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> toolHandler =
(exchange, params) -> {
// The backend now handles all logging through event listeners
return backend.callTool(toolName, params);
};
serverBuilder.tool(toolSchema, toolHandler);
Msg.info(this, "Registered tool with MCP server: " + toolName);
}
serverBuilder.build();
// Register MCP servlet - use root path since transport provider handles routing internally
Msg.info(this, "Registering MCP servlet");
try {
ServletHolder mcpServletHolder = new ServletHolder("mcp-transport", transportProvider);
mcpServletHolder.setAsyncSupported(true);
context.addServlet(mcpServletHolder, "/*");
Msg.info(this, "Successfully registered MCP servlet for all paths: /*");
// Log configuration
Msg.info(this, "Transport provider class: " + transportProvider.getClass().getName());
Msg.info(this, "Message endpoint configured as: " + messageEndpoint);
Msg.info(this, "SSE endpoint will be: /sse (default)");
Msg.info(this, "Expected client URLs:");
Msg.info(this, " SSE: http://localhost:" + port + "/sse");
Msg.info(this, " Messages: http://localhost:" + port + "/" + messageEndpoint);
} catch (Exception e) {
Msg.error(this, "Failed to register MCP servlet", e);
}
// Start Jetty server
Msg.info(this, "Starting Jetty server...");
jettyServer.start();
// Verify server is listening
if (jettyServer.isStarted()) {
Msg.info(this, "GhidrAssistMCP Server successfully started on port " + port);
Msg.info(this, "MCP SSE endpoint: http://localhost:" + port + "/sse");
Msg.info(this, "MCP message endpoint: http://localhost:" + port + "/" + messageEndpoint);
Msg.info(this, "Server state: " + jettyServer.getState());
// Log all registered servlets
var servletHandler = context.getServletHandler();
var servletMappings = servletHandler.getServletMappings();
Msg.info(this, "Registered servlet mappings:");
for (var mapping : servletMappings) {
Msg.info(this, " " + mapping.getServletName() + " -> " + String.join(", ", mapping.getPathSpecs()));
}
// Log server startup to UI
if (provider != null) {
provider.logSession("Jetty server listening on port " + port);
provider.logSession("Registered " + backend.getAvailableTools().size() + " MCP tools");
provider.logSession("Ready for MCP client connections");
}
} else {
Msg.error(this, "Failed to start Jetty server - server not in started state");
}
} catch (Exception e) {
Msg.error(this, "Exception during MCP Server startup: " + e.getMessage(), e);
throw e;
}
}
public void stop() throws Exception {
if (jettyServer != null) {
jettyServer.stop();
Msg.info(this, "GhidrAssistMCP Server stopped");
}
}
public void setCurrentProgram(Program program) {
backend.onProgramActivated(program);
}
}
@@ -0,0 +1,57 @@
/*
*
*/
package ghidrassistmcp;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import io.modelcontextprotocol.spec.McpSchema;
/**
* Interface for the MCP backend that handles tool management and execution.
* This provides separation between the HTTP transport layer and business logic.
*/
public interface McpBackend {
/**
* Register a new MCP tool
*/
void registerTool(McpTool tool);
/**
* Unregister an MCP tool by name
*/
void unregisterTool(String toolName);
/**
* Get list of all available tools
*/
List<McpSchema.Tool> getAvailableTools();
/**
* Execute a tool with given arguments
*/
McpSchema.CallToolResult callTool(String toolName, Map<String, Object> arguments);
/**
* Notify backend when a program is activated
*/
void onProgramActivated(Program program);
/**
* Notify backend when a program is deactivated
*/
void onProgramDeactivated(Program program);
/**
* Get server implementation info
*/
McpSchema.Implementation getServerInfo();
/**
* Get server capabilities
*/
McpSchema.ServerCapabilities getCapabilities();
}
@@ -0,0 +1,37 @@
/*
*
*/
package ghidrassistmcp;
/**
* Event listener interface for MCP operations.
* This allows decoupling between the backend and UI components.
*/
public interface McpEventListener {
/**
* Called when a tool request is received.
* @param toolName The name of the tool being called
* @param parameters The request parameters (may be truncated for logging)
*/
void onToolRequest(String toolName, String parameters);
/**
* Called when a tool response is generated.
* @param toolName The name of the tool that was called
* @param response The response content (may be truncated for logging)
*/
void onToolResponse(String toolName, String response);
/**
* Called when a session event occurs.
* @param event The session event description
*/
void onSessionEvent(String event);
/**
* Called for general logging messages.
* @param message The log message
*/
void onLogMessage(String message);
}
+43
View File
@@ -0,0 +1,43 @@
/*
*
*/
package ghidrassistmcp;
import java.util.Map;
import ghidra.program.model.listing.Program;
import io.modelcontextprotocol.spec.McpSchema;
/**
* Interface for individual MCP tools that can be registered with the backend.
*/
public interface McpTool {
/**
* Get the tool name (used for MCP tool calls)
*/
String getName();
/**
* Get the tool description
*/
String getDescription();
/**
* Get the input schema for this tool
*/
McpSchema.JsonSchema getInputSchema();
/**
* Execute the tool with given arguments and current program context
*/
McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram);
/**
* Execute the tool with given arguments, program context, and plugin reference for UI access
*/
default McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram, GhidrAssistMCPPlugin plugin) {
// Default implementation delegates to the original method for backward compatibility
return execute(arguments, currentProgram);
}
}
@@ -0,0 +1,359 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.SwingUtilities;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileOptions;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeConflictHandler;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.data.PointerDataType;
import ghidra.program.model.data.Structure;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Function.FunctionUpdateType;
import ghidra.program.model.listing.Parameter;
import ghidra.program.model.listing.ParameterImpl;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.Variable;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighFunctionDBUtil;
import ghidra.program.model.pcode.HighVariable;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.Msg;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;
import ghidrassistmcp.GhidrAssistMCPPlugin;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that implements Ghidra's 'Auto create structure' and 'Auto fill in structure' functionality.
* This tool analyzes a variable and creates a structure based on its usage patterns.
*/
public class AutoCreateStructTool implements McpTool {
@Override
public String getName() {
return "auto_create_struct";
}
@Override
public String getDescription() {
return "Auto create and apply structure to a variable (equivalent to Ghidra's auto create structure)";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_identifier", new McpSchema.JsonSchema("string", null, null, null, null, null),
"variable_name", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("function_identifier", "variable_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
// Fallback for when plugin reference is not available
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Auto create structure functionality requires plugin context for decompiler access")
.build();
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram, GhidrAssistMCPPlugin plugin) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionIdentifier = (String) arguments.get("function_identifier");
String variableName = (String) arguments.get("variable_name");
if (functionIdentifier == null || functionIdentifier.isEmpty()) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_identifier parameter is required (function name or address)")
.build();
}
if (variableName == null || variableName.isEmpty()) {
return McpSchema.CallToolResult.builder()
.addTextContent("variable_name parameter is required")
.build();
}
// Use proper structure creation with transaction handling
StructureResult result = createAndApplyStructure(currentProgram, functionIdentifier, variableName);
return McpSchema.CallToolResult.builder()
.addTextContent(result.success ?
"Successfully created and applied structure: " + result.message :
"Failed to create structure: " + result.errorMessage)
.build();
}
/**
* Result class for structure operations
*/
private static class StructureResult {
final boolean success;
final String message;
final String errorMessage;
StructureResult(boolean success, String message, String errorMessage) {
this.success = success;
this.message = message;
this.errorMessage = errorMessage;
}
}
/**
* Create and apply structure with proper error handling
*/
private StructureResult createAndApplyStructure(Program program, String functionIdentifier, String variableName) {
final StringBuilder errorMessage = new StringBuilder();
final StringBuilder successMessage = new StringBuilder();
final AtomicBoolean success = new AtomicBoolean(false);
try {
SwingUtilities.invokeAndWait(() ->
performStructureCreation(program, functionIdentifier, variableName, success, successMessage, errorMessage));
} catch (Exception e) {
String msg = "Failed to create structure on Swing thread: " + e.getMessage();
errorMessage.append(msg);
Msg.error(this, msg, e);
}
return new StructureResult(success.get(), successMessage.toString(), errorMessage.toString());
}
/**
* Perform the structure creation within proper threading context
*/
private void performStructureCreation(Program program, String functionIdentifier, String variableName,
AtomicBoolean success, StringBuilder successMessage, StringBuilder errorMessage) {
int txId = program.startTransaction("Auto Create Structure");
boolean committed = false;
try {
// Find the function
Function function = findFunction(program, functionIdentifier);
if (function == null) {
errorMessage.append("Could not find function: ").append(functionIdentifier);
return;
}
Msg.info(this, "Creating structure for variable '" + variableName + "' in function " + function.getName());
// Set up decompiler
DecompInterface decompiler = new DecompInterface();
DecompileOptions options = new DecompileOptions();
decompiler.setOptions(options);
if (!decompiler.openProgram(program)) {
errorMessage.append("Failed to initialize decompiler");
return;
}
try {
// Decompile the function
DecompileResults decompileResults = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
HighFunction highFunction = decompileResults.getHighFunction();
if (highFunction == null) {
errorMessage.append("Failed to decompile function");
return;
}
// Find the variable
HighVariable highVar = findHighVariable(highFunction, variableName);
if (highVar == null) {
errorMessage.append("Could not find variable '").append(variableName).append("' in function");
return;
}
// Create and apply the structure
createAndApplyStructureImpl(program, function, highVar, decompiler, successMessage, errorMessage);
success.set(true);
committed = true;
} finally {
decompiler.closeProgram();
}
} catch (Exception e) {
String msg = "Error during structure creation: " + e.getMessage();
errorMessage.append(msg);
Msg.error(this, msg, e);
} finally {
program.endTransaction(txId, committed);
}
}
/**
* Core structure creation logic adapted from reference code
*/
private void createAndApplyStructureImpl(Program program, Function function, HighVariable highVar,
DecompInterface decompiler, StringBuilder successMessage, StringBuilder errorMessage) throws Exception {
// Try to use FillOutStructureHelper to process the structure
// Note: This class may not be available in all Ghidra versions
Structure structDT = null;
try {
Class<?> fillHelperClass = Class.forName("ghidra.app.util.datatype.microsoft.FillOutStructureHelper");
Object fillHelper = fillHelperClass.getConstructor(Program.class, TaskMonitor.class)
.newInstance(program, TaskMonitor.DUMMY);
java.lang.reflect.Method processMethod = fillHelperClass.getMethod("processStructure",
HighVariable.class, Function.class, boolean.class, boolean.class, DecompInterface.class);
structDT = (Structure) processMethod.invoke(fillHelper, highVar, function, false, true, decompiler);
} catch (Exception e) {
errorMessage.append("FillOutStructureHelper not available or failed: ").append(e.getMessage());
return;
}
if (structDT == null) {
errorMessage.append("Failed to create structure from variable usage");
return;
}
// Add structure to data type manager
DataTypeManager dtm = program.getDataTypeManager();
structDT = (Structure) dtm.addDataType(structDT, DataTypeConflictHandler.DEFAULT_HANDLER);
PointerDataType ptrStruct = new PointerDataType(structDT);
// Find the variable in the function
Variable var = findVariable(function, highVar.getSymbol().getName());
if (var instanceof ghidra.program.model.listing.AutoParameterImpl) {
// Modify the function signature to change the data type of the auto-parameter
updateFunctionParameter(function, var.getName(), ptrStruct);
successMessage.append("Updated function parameter '").append(var.getName())
.append("' with structure type: ").append(structDT.getName());
} else {
// Update local variable
HighFunctionDBUtil.updateDBVariable(highVar.getSymbol(), null, ptrStruct, SourceType.USER_DEFINED);
successMessage.append("Updated local variable '").append(var.getName())
.append("' with structure type: ").append(structDT.getName());
}
Msg.info(this, "Successfully created and applied structure: " + structDT.getName());
}
/**
* Find function by name or address
*/
private Function findFunction(Program program, String identifier) {
// Try as address first
try {
Address addr = program.getAddressFactory().getAddress(identifier);
if (addr != null) {
return program.getFunctionManager().getFunctionAt(addr);
}
} catch (Exception e) {
// Not an address, try as name
}
// Try as function name
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(identifier)) {
return function;
}
}
return null;
}
/**
* Find high variable by name in high function
*/
private HighVariable findHighVariable(HighFunction highFunction, String variableName) {
var localSymbolMap = highFunction.getLocalSymbolMap();
var symbols = localSymbolMap.getSymbols();
while (symbols.hasNext()) {
var symbol = symbols.next();
if (symbol.getName().equals(variableName)) {
return symbol.getHighVariable();
}
}
return null;
}
/**
* Find variable in function by name
*/
private Variable findVariable(Function function, String variableName) {
// Check parameters
for (Parameter param : function.getParameters()) {
if (param.getName().equals(variableName)) {
return param;
}
}
// Check local variables
for (Variable var : function.getLocalVariables()) {
if (var.getName().equals(variableName)) {
return var;
}
}
return null;
}
/**
* Update function parameter with new data type
*/
private void updateFunctionParameter(Function function, String paramName,
DataType newType) throws InvalidInputException, DuplicateNameException {
Parameter[] parameters = function.getParameters();
Parameter[] newParams = new Parameter[parameters.length];
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].getName().equals(paramName)) {
newParams[i] = new ParameterImpl(
parameters[i].getName(),
newType,
parameters[i].getVariableStorage(),
function.getProgram(),
SourceType.USER_DEFINED
);
} else {
newParams[i] = parameters[i];
}
}
function.updateFunction(
function.getCallingConventionName(),
null, // Keep return type
FunctionUpdateType.CUSTOM_STORAGE,
true,
SourceType.USER_DEFINED,
newParams
);
}
}
@@ -0,0 +1,132 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.util.task.TaskMonitor;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that decompiles a function to readable C-like code.
*/
public class DecompileFunctionTool implements McpTool {
@Override
public String getName() {
return "decompile_function";
}
@Override
public String getDescription() {
return "Decompile a function to readable C-like code using function name or address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"address", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String addressStr = (String) arguments.get("address");
if (functionName == null && addressStr == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Either function_name or address must be provided")
.build();
}
Function function = null;
// Find function by name or address
if (functionName != null) {
function = findFunctionByName(currentProgram, functionName);
} else if (addressStr != null) {
try {
Address addr = currentProgram.getAddressFactory().getAddress(addressStr);
function = currentProgram.getFunctionManager().getFunctionAt(addr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
}
if (function == null) {
String target = functionName != null ? functionName : addressStr;
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + target)
.build();
}
// Decompile the function
String decompiledCode = decompileFunction(currentProgram, function);
return McpSchema.CallToolResult.builder()
.addTextContent(decompiledCode)
.build();
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
private String decompileFunction(Program program, Function function) {
DecompInterface decompiler = new DecompInterface();
try {
decompiler.openProgram(function.getProgram());
DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
if (results.isTimedOut()) {
return "Decompilation timed out for function: " + function.getName();
}
if (results.isValid() == false) {
return "Decompilation error for function " + function.getName() + ": " + results.getErrorMessage();
}
String decompiledCode = results.getDecompiledFunction().getC();
if (decompiledCode == null || decompiledCode.trim().isEmpty()) {
return "No decompiled code available for function: " + function.getName();
}
return "Decompiled function " + function.getName() + ":\n\n" + decompiledCode;
} catch (Exception e) {
return "Error decompiling function " + function.getName() + ": " + e.getMessage();
} finally {
decompiler.dispose();
}
}
}
@@ -0,0 +1,138 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.CommentType;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.InstructionIterator;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that disassembles a function to assembly instructions.
*/
public class DisassembleFunctionTool implements McpTool {
@Override
public String getName() {
return "disassemble_function";
}
@Override
public String getDescription() {
return "Disassemble a function to assembly instructions";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"address", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String addressStr = (String) arguments.get("address");
if (functionName == null && addressStr == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Either function_name or address parameter is required")
.build();
}
Function function = null;
if (functionName != null) {
function = findFunctionByName(currentProgram, functionName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + functionName)
.build();
}
} else {
// Find function by address
try {
Address address = currentProgram.getAddressFactory().getAddress(addressStr);
function = currentProgram.getFunctionManager().getFunctionContaining(address);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No function found at address: " + addressStr)
.build();
}
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
}
StringBuilder result = new StringBuilder();
result.append("Disassembly of function: ").append(function.getName()).append("\n");
result.append("Entry Point: ").append(function.getEntryPoint()).append("\n\n");
// Iterate through instructions in the function
InstructionIterator instrIter = currentProgram.getListing().getInstructions(function.getBody(), true);
int instructionCount = 0;
while (instrIter.hasNext()) {
Instruction instruction = instrIter.next();
result.append(instruction.getAddress()).append(": ");
result.append(instruction.getMnemonicString());
// Add operands
for (int i = 0; i < instruction.getNumOperands(); i++) {
if (i == 0) {
result.append(" ");
} else {
result.append(", ");
}
result.append(instruction.getDefaultOperandRepresentation(i));
}
// Add any comments
String comment = instruction.getComment(CommentType.EOL);
if (comment != null && !comment.trim().isEmpty()) {
result.append(" ; ").append(comment.trim());
}
result.append("\n");
instructionCount++;
}
result.append("\nTotal instructions: ").append(instructionCount);
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,170 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that gets cross-references for a specific function.
*/
public class FunctionXrefsTool implements McpTool {
@Override
public String getName() {
return "function_xrefs";
}
@Override
public String getDescription() {
return "Get cross-references to and from a specific function";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"direction", new McpSchema.JsonSchema("string", null, null, null, null, null),
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of("function_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String direction = (String) arguments.get("direction"); // "to", "from", or "both"
if (functionName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_name parameter is required")
.build();
}
// Parse optional parameters
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
if (direction == null) {
direction = "both"; // Default to both directions
}
// Find the function
Function function = findFunctionByName(currentProgram, functionName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + functionName)
.build();
}
StringBuilder result = new StringBuilder();
result.append("Cross-references for function: ").append(functionName).append("\n");
result.append("Entry Point: ").append(function.getEntryPoint()).append("\n\n");
int totalCount = 0;
int count = 0;
// Get XREFs TO the function (callers)
if ("to".equals(direction) || "both".equals(direction)) {
result.append("=== References TO function (callers) ===\n");
Set<Function> callingFunctions = function.getCallingFunctions(null);
for (Function callerFunc : callingFunctions) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
result.append("- ").append(callerFunc.getEntryPoint())
.append(" (").append(callerFunc.getName()).append(")\n");
count++;
}
if (callingFunctions.isEmpty()) {
result.append("No callers found.\n");
}
result.append("\n");
}
// Get XREFs FROM the function (callees)
if ("from".equals(direction) || "both".equals(direction)) {
result.append("=== References FROM function (callees) ===\n");
Set<Function> calledFunctions = function.getCalledFunctions(null);
for (Function calledFunc : calledFunctions) {
if (count >= limit) {
break;
}
totalCount++;
// Apply offset (continue from previous count)
if (totalCount <= offset) {
continue;
}
result.append("- ").append(calledFunc.getEntryPoint())
.append(" (").append(calledFunc.getName()).append(")\n");
count++;
}
if (calledFunctions.isEmpty()) {
result.append("No called functions found.\n");
}
}
if (totalCount == 0) {
result.append("No cross-references found for function: ").append(functionName);
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" references");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,80 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.GhidrAssistMCPPlugin;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that gets the current cursor address in Ghidra.
*/
public class GetCurrentAddressTool implements McpTool {
@Override
public String getName() {
return "get_current_address";
}
@Override
public String getDescription() {
return "Get the current cursor address in Ghidra";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
// Fallback for when plugin reference is not available
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Current address functionality requires plugin context. " +
"Program minimum address: " + currentProgram.getMinAddress())
.build();
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram, GhidrAssistMCPPlugin plugin) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
if (plugin == null) {
return execute(arguments, currentProgram);
}
Address currentAddress = plugin.getCurrentAddress();
if (currentAddress == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No current address available (cursor may not be positioned in the listing)")
.build();
}
StringBuilder result = new StringBuilder();
result.append("Current Address: ").append(currentAddress).append("\n");
result.append("Program: ").append(currentProgram.getName()).append("\n");
result.append("Address Space: ").append(currentAddress.getAddressSpace().getName());
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,96 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.GhidrAssistMCPPlugin;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that gets the current function in Ghidra.
*/
public class GetCurrentFunctionTool implements McpTool {
@Override
public String getName() {
return "get_current_function";
}
@Override
public String getDescription() {
return "Get the current function containing the cursor in Ghidra";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
// Fallback for when plugin reference is not available
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Current function functionality requires plugin context. Use get_function_by_address with specific addresses instead.")
.build();
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram, GhidrAssistMCPPlugin plugin) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
if (plugin == null) {
return execute(arguments, currentProgram);
}
Function currentFunction = plugin.getCurrentFunction();
if (currentFunction == null) {
Address currentAddress = plugin.getCurrentAddress();
if (currentAddress == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No current address available (cursor may not be positioned in the listing)")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("No function contains the current address: " + currentAddress)
.build();
}
StringBuilder result = new StringBuilder();
result.append("Current Function Information:\n\n");
result.append("Name: ").append(currentFunction.getName()).append("\n");
result.append("Entry Point: ").append(currentFunction.getEntryPoint()).append("\n");
result.append("Address Range: ").append(currentFunction.getBody().getMinAddress())
.append(" - ").append(currentFunction.getBody().getMaxAddress()).append("\n");
result.append("Parameter Count: ").append(currentFunction.getParameterCount()).append("\n");
result.append("Calling Convention: ").append(currentFunction.getCallingConventionName()).append("\n");
// Add signature if available
String signature = currentFunction.getSignature().getPrototypeString();
if (signature != null && !signature.isEmpty()) {
result.append("Signature: ").append(signature).append("\n");
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,92 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that gets function information by address.
*/
public class GetFunctionByAddressTool implements McpTool {
@Override
public String getName() {
return "get_function_by_address";
}
@Override
public String getDescription() {
return "Get function information for the function containing a specific address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("address"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
if (addressStr == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address parameter is required")
.build();
}
// Parse the address
Address address;
try {
address = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Find the function containing this address
Function function = currentProgram.getFunctionManager().getFunctionContaining(address);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No function found containing address: " + addressStr)
.build();
}
StringBuilder result = new StringBuilder();
result.append("Function at address ").append(addressStr).append(":\n\n");
result.append("Name: ").append(function.getName()).append("\n");
result.append("Entry Point: ").append(function.getEntryPoint()).append("\n");
result.append("Address Range: ").append(function.getBody().getMinAddress())
.append(" - ").append(function.getBody().getMaxAddress()).append("\n");
result.append("Parameter Count: ").append(function.getParameterCount()).append("\n");
result.append("Return Type: ").append(function.getReturnType().getDisplayName()).append("\n");
result.append("Calling Convention: ").append(function.getCallingConventionName()).append("\n");
result.append("Signature: ").append(function.getSignature()).append("\n");
if (function.getComment() != null) {
result.append("Comment: ").append(function.getComment()).append("\n");
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,77 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that provides detailed information about a specific function.
*/
public class GetFunctionInfoTool implements McpTool {
@Override
public String getName() {
return "get_function_info";
}
@Override
public String getDescription() {
return "Get detailed information about a specific function";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of("function_name", new McpSchema.JsonSchema("string", null, null, null, null, null)),
List.of("function_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
if (functionName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function name is required")
.build();
}
String info = getFunctionInfo(currentProgram, functionName);
return McpSchema.CallToolResult.builder()
.addTextContent(info)
.build();
}
private String getFunctionInfo(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (var function : functions) {
if (function.getName().equals(functionName)) {
StringBuilder info = new StringBuilder();
info.append("Function Information:\n");
info.append("Name: ").append(function.getName()).append("\n");
info.append("Entry Point: ").append(function.getEntryPoint()).append("\n");
info.append("Body: ").append(function.getBody()).append("\n");
info.append("Parameter Count: ").append(function.getParameterCount()).append("\n");
info.append("Return Type: ").append(function.getReturnType()).append("\n");
info.append("Signature: ").append(function.getSignature()).append("\n");
return info.toString();
}
}
return "Function not found: " + functionName;
}
}
@@ -0,0 +1,153 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import ghidra.program.model.data.CategoryPath;
import ghidra.program.model.data.Composite;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.data.Structure;
import ghidra.program.model.data.Union;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists classes/structures defined in the program.
*/
public class ListClassesTool implements McpTool {
@Override
public String getName() {
return "list_classes";
}
@Override
public String getDescription() {
return "List classes/structures defined in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"include_builtin", new McpSchema.JsonSchema("boolean", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional parameters
int offset = 0;
int limit = 100; // Default limit
boolean includeBuiltin = false;
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
if (arguments.get("include_builtin") instanceof Boolean) {
includeBuiltin = (Boolean) arguments.get("include_builtin");
}
StringBuilder result = new StringBuilder();
result.append("Classes/Structures in program");
if (!includeBuiltin) {
result.append(" (user-defined only)");
}
result.append(":\n\n");
DataTypeManager dtm = currentProgram.getDataTypeManager();
Iterator<DataType> dataTypes = dtm.getAllDataTypes();
int count = 0;
int totalCount = 0;
while (dataTypes.hasNext()) {
DataType dataType = dataTypes.next();
// Only include composite types (structures, unions, classes)
if (!(dataType instanceof Composite)) {
continue;
}
// Skip built-in types if not requested
if (!includeBuiltin) {
CategoryPath categoryPath = dataType.getCategoryPath();
if (categoryPath != null && categoryPath.toString().startsWith("/")) {
// Skip system/built-in types that are in root categories
String pathStr = categoryPath.toString();
if (pathStr.equals("/") || pathStr.startsWith("/windows") ||
pathStr.startsWith("/generic") || pathStr.startsWith("/builtin")) {
continue;
}
}
}
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
Composite composite = (Composite) dataType;
String typeName = "Unknown";
if (dataType instanceof Structure) {
typeName = "Struct";
} else if (dataType instanceof Union) {
typeName = "Union";
}
result.append("- ").append(dataType.getName())
.append(" [").append(typeName).append("]")
.append(" (").append(composite.getNumComponents()).append(" fields, ")
.append(dataType.getLength()).append(" bytes)");
if (dataType.getCategoryPath() != null) {
result.append(" in ").append(dataType.getCategoryPath());
}
result.append("\n");
count++;
}
if (totalCount == 0) {
if (includeBuiltin) {
result.append("No composite types found in the program.");
} else {
result.append("No user-defined classes/structures found in the program.");
}
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" classes/structures");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,139 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.DataIterator;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists defined data in the program.
*/
public class ListDataTool implements McpTool {
@Override
public String getName() {
return "list_data";
}
@Override
public String getDescription() {
return "List defined data elements in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"data_type_filter", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional parameters
int offset = 0;
int limit = 100; // Default limit
String dataTypeFilter = (String) arguments.get("data_type_filter");
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Defined Data Elements");
if (dataTypeFilter != null) {
result.append(" (filtered by: ").append(dataTypeFilter).append(")");
}
result.append(":\n\n");
DataIterator dataIter = currentProgram.getListing().getDefinedData(true);
int count = 0;
int totalCount = 0;
while (dataIter.hasNext()) {
Data data = dataIter.next();
// Apply data type filter if specified
if (dataTypeFilter != null) {
DataType dataType = data.getDataType();
if (dataType == null || !dataType.getName().toLowerCase().contains(dataTypeFilter.toLowerCase())) {
continue;
}
}
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
DataType dataType = data.getDataType();
String typeName = dataType != null ? dataType.getName() : "unknown";
String value = data.getDefaultValueRepresentation();
if (value != null && value.length() > 50) {
value = value.substring(0, 47) + "...";
}
result.append("@ ").append(data.getAddress())
.append(" [").append(typeName).append("]");
if (data.hasStringValue()) {
result.append(" String: ").append(value != null ? value : "null");
} else if (value != null) {
result.append(" Value: ").append(value);
}
// Add symbol name if available
if (data.getPrimarySymbol() != null) {
result.append(" (").append(data.getPrimarySymbol().getName()).append(")");
}
result.append("\n");
count++;
}
if (totalCount == 0) {
if (dataTypeFilter != null) {
result.append("No data elements found matching filter: ").append(dataTypeFilter);
} else {
result.append("No defined data elements found in the program.");
}
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" data elements");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,110 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.symbol.SymbolType;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists exported functions and symbols.
*/
public class ListExportsTool implements McpTool {
@Override
public String getName() {
return "list_exports";
}
@Override
public String getDescription() {
return "List exported functions and symbols";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Exported Functions and Symbols:\n\n");
SymbolIterator symbolIter = currentProgram.getSymbolTable().getSymbolIterator();
int count = 0;
int totalCount = 0;
while (symbolIter.hasNext()) {
Symbol symbol = symbolIter.next();
// Only include global symbols that are potentially exported
if (symbol.isGlobal() && !symbol.isExternal() &&
(symbol.getSymbolType() == SymbolType.FUNCTION ||
symbol.getSymbolType() == SymbolType.LABEL)) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
result.append("- ").append(symbol.getName())
.append(" @ ").append(symbol.getAddress())
.append(" (").append(symbol.getSymbolType()).append(")")
.append("\n");
count++;
}
}
if (totalCount == 0) {
result.append("No exported symbols found in the program.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" exports");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,59 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists all functions in the currently loaded program.
*/
public class ListFunctionsTool implements McpTool {
@Override
public String getName() {
return "list_functions";
}
@Override
public String getDescription() {
return "List all functions in the current program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object", Map.of(), List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functions = listFunctions(currentProgram);
return McpSchema.CallToolResult.builder()
.addTextContent(functions)
.build();
}
private String listFunctions(Program program) {
StringBuilder functions = new StringBuilder();
functions.append("Functions in program:\n");
program.getFunctionManager().getFunctions(true).forEach(function -> {
functions.append("- ").append(function.getName())
.append(" @ ").append(function.getEntryPoint())
.append("\n");
});
return functions.toString();
}
}
@@ -0,0 +1,116 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.symbol.SymbolType;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists imported functions and symbols.
*/
public class ListImportsTool implements McpTool {
@Override
public String getName() {
return "list_imports";
}
@Override
public String getDescription() {
return "List imported functions and symbols";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Imported Functions and Symbols:\n\n");
SymbolIterator symbolIter = currentProgram.getSymbolTable().getSymbolIterator();
int count = 0;
int totalCount = 0;
while (symbolIter.hasNext()) {
Symbol symbol = symbolIter.next();
// Only include external symbols (imports)
if (symbol.isExternal() &&
(symbol.getSymbolType() == SymbolType.FUNCTION ||
symbol.getSymbolType() == SymbolType.LABEL)) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
String libraryName = "unknown";
if (symbol.getParentNamespace() != null) {
libraryName = symbol.getParentNamespace().getName();
}
result.append("- ").append(symbol.getName())
.append(" from ").append(libraryName)
.append(" @ ").append(symbol.getAddress())
.append(" (").append(symbol.getSymbolType()).append(")")
.append("\n");
count++;
}
}
if (totalCount == 0) {
result.append("No imported symbols found in the program.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" imports");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,102 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists all methods/functions in the program.
*/
public class ListMethodsTool implements McpTool {
@Override
public String getName() {
return "list_methods";
}
@Override
public String getDescription() {
return "List all methods/functions in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Methods/Functions in program:\n\n");
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
int count = 0;
int totalCount = 0;
while (functions.hasNext()) {
Function function = functions.next();
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
result.append("- ").append(function.getName())
.append(" @ ").append(function.getEntryPoint())
.append(" (").append(function.getParameterCount()).append(" params)")
.append("\n");
count++;
}
if (totalCount == 0) {
result.append("No functions found in the program.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" functions");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,128 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists namespaces in the program.
*/
public class ListNamespacesTool implements McpTool {
@Override
public String getName() {
return "list_namespaces";
}
@Override
public String getDescription() {
return "List namespaces in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Namespaces in program:\n\n");
// Get all symbols and find unique namespaces
SymbolIterator symbolIter = currentProgram.getSymbolTable().getSymbolIterator();
java.util.Set<Namespace> uniqueNamespaces = new java.util.HashSet<>();
while (symbolIter.hasNext()) {
Symbol symbol = symbolIter.next();
Namespace namespace = symbol.getParentNamespace();
if (namespace != null && !namespace.isGlobal()) {
uniqueNamespaces.add(namespace);
}
}
int count = 0;
int totalCount = 0;
for (Namespace namespace : uniqueNamespaces) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
result.append("- ").append(namespace.getName());
// Show parent namespace if not global
if (namespace.getParentNamespace() != null && !namespace.getParentNamespace().isGlobal()) {
result.append(" (in ").append(namespace.getParentNamespace().getName()).append(")");
}
// Count symbols in this namespace
try {
int symbolCount = 0;
SymbolIterator symbols = currentProgram.getSymbolTable().getSymbols(namespace);
while (symbols.hasNext()) {
symbols.next();
symbolCount++;
}
result.append(" [").append(symbolCount).append(" symbols]");
} catch (Exception e) {
// Ignore errors counting symbols
}
result.append("\n");
count++;
}
if (totalCount == 0) {
result.append("No user-defined namespaces found in the program.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" namespaces");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,98 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists memory segments/blocks in the program.
*/
public class ListSegmentsTool implements McpTool {
@Override
public String getName() {
return "list_segments";
}
@Override
public String getDescription() {
return "List memory segments/blocks in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Memory Segments/Blocks:\n\n");
MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
int count = 0;
int totalCount = blocks.length;
for (int i = offset; i < blocks.length && count < limit; i++) {
MemoryBlock block = blocks[i];
String permissions = "";
if (block.isRead()) permissions += "R";
if (block.isWrite()) permissions += "W";
if (block.isExecute()) permissions += "X";
result.append("- ").append(block.getName())
.append(" @ ").append(block.getStart())
.append("-").append(block.getEnd())
.append(" (").append(String.format("0x%x", block.getSize())).append(" bytes)")
.append(" [").append(permissions).append("]")
.append(" Type: ").append(block.getType())
.append("\n");
count++;
}
if (totalCount == 0) {
result.append("No memory blocks found in the program.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" segments");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,122 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.DataIterator;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that lists string data found in the program.
*/
public class ListStringsTool implements McpTool {
@Override
public String getName() {
return "list_strings";
}
@Override
public String getDescription() {
return "List string data found in the program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"min_length", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
// Parse optional parameters
int offset = 0;
int limit = 100; // Default limit
int minLength = 4; // Default minimum string length
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
if (arguments.get("min_length") instanceof Number) {
minLength = ((Number) arguments.get("min_length")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Strings in program (min length: ").append(minLength).append("):\n\n");
DataIterator dataIter = currentProgram.getListing().getDefinedData(true);
int count = 0;
int totalCount = 0;
while (dataIter.hasNext()) {
Data data = dataIter.next();
// Check if this is string data
if (data.hasStringValue()) {
String stringValue = data.getDefaultValueRepresentation();
// Apply minimum length filter
if (stringValue != null && stringValue.length() >= minLength) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
// Clean up the string representation for display
String displayString = stringValue;
if (displayString.length() > 80) {
displayString = displayString.substring(0, 77) + "...";
}
result.append("@ ").append(data.getAddress())
.append(" (").append(stringValue.length()).append(" chars): ")
.append(displayString)
.append("\n");
count++;
}
}
}
if (totalCount == 0) {
result.append("No strings found in the program with minimum length ").append(minLength).append(".");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" strings");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,60 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that provides basic information about the currently loaded program.
*/
public class ProgramInfoTool implements McpTool {
@Override
public String getName() {
return "get_program_info";
}
@Override
public String getDescription() {
return "Get information about the currently loaded program";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object", Map.of(), List.of(), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String info = buildProgramInfo(currentProgram);
return McpSchema.CallToolResult.builder()
.addTextContent(info)
.build();
}
private String buildProgramInfo(Program program) {
StringBuilder info = new StringBuilder();
info.append("Program Information:\n");
info.append("Name: ").append(program.getName()).append("\n");
info.append("Language: ").append(program.getLanguage().getLanguageDescription()).append("\n");
info.append("Address Space: ").append(program.getAddressFactory().getDefaultAddressSpace().getName()).append("\n");
info.append("Image Base: ").append(program.getImageBase()).append("\n");
info.append("Min Address: ").append(program.getMinAddress()).append("\n");
info.append("Max Address: ").append(program.getMaxAddress()).append("\n");
info.append("Function Count: ").append(program.getFunctionManager().getFunctionCount()).append("\n");
return info.toString();
}
}
@@ -0,0 +1,139 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.model.symbol.Symbol;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that renames data elements (variables, labels) at a specific address.
*/
public class RenameDataTool implements McpTool {
@Override
public String getName() {
return "rename_data";
}
@Override
public String getDescription() {
return "Rename data elements (variables, labels) at a specific address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"new_name", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("address", "new_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
String newName = (String) arguments.get("new_name");
if (addressStr == null || newName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address and new_name parameters are required")
.build();
}
// Parse the address
Address address;
try {
address = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Try to find data at this address
Data data = currentProgram.getListing().getDataAt(address);
if (data != null) {
// Rename the data symbol
Symbol primarySymbol = data.getPrimarySymbol();
if (primarySymbol != null) {
String oldName = primarySymbol.getName();
try {
primarySymbol.setName(newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully renamed data at " + addressStr +
" from '" + oldName + "' to '" + newName + "'")
.build();
} catch (DuplicateNameException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Symbol with name '" + newName + "' already exists")
.build();
} catch (InvalidInputException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid symbol name: " + newName)
.build();
}
}
// Create a new symbol for the data
try {
currentProgram.getSymbolTable().createLabel(address, newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully created label '" + newName + "' at " + addressStr)
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error creating label: " + e.getMessage())
.build();
}
}
// No data at address, try to find any symbol and rename it
Symbol[] symbols = currentProgram.getSymbolTable().getSymbols(address);
if (symbols.length > 0) {
Symbol symbol = symbols[0]; // Use first symbol
String oldName = symbol.getName();
try {
symbol.setName(newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully renamed symbol at " + addressStr +
" from '" + oldName + "' to '" + newName + "'")
.build();
} catch (DuplicateNameException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Symbol with name '" + newName + "' already exists")
.build();
} catch (InvalidInputException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid symbol name: " + newName)
.build();
}
}
// Create a new label at the address
try {
currentProgram.getSymbolTable().createLabel(address, newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully created label '" + newName + "' at " + addressStr)
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error creating label: " + e.getMessage())
.build();
}
}
}
@@ -0,0 +1,101 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that renames a function by its address.
*/
public class RenameFunctionByAddressTool implements McpTool {
@Override
public String getName() {
return "rename_function_by_address";
}
@Override
public String getDescription() {
return "Rename a function by its address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"new_name", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("address", "new_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
String newName = (String) arguments.get("new_name");
if (addressStr == null || newName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address and new_name parameters are required")
.build();
}
// Parse the address
Address address;
try {
address = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Find the function at this address
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null) {
// Try to find function containing this address
function = currentProgram.getFunctionManager().getFunctionContaining(address);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No function found at address: " + addressStr)
.build();
}
}
String oldName = function.getName();
// Rename the function
try {
function.setName(newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully renamed function at " + addressStr +
" from '" + oldName + "' to '" + newName + "'")
.build();
} catch (DuplicateNameException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function with name '" + newName + "' already exists")
.build();
} catch (InvalidInputException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid function name: " + newName)
.build();
}
}
}
@@ -0,0 +1,97 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.SourceType;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that renames a function.
*/
public class RenameFunctionTool implements McpTool {
@Override
public String getName() {
return "rename_function";
}
@Override
public String getDescription() {
return "Rename an existing function";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"old_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"new_name", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("old_name", "new_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String oldName = (String) arguments.get("old_name");
String newName = (String) arguments.get("new_name");
if (oldName == null || newName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Both old_name and new_name are required")
.build();
}
// Find the function by old name
Function function = findFunctionByName(currentProgram, oldName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + oldName)
.build();
}
// Check if new name already exists
Function existingFunction = findFunctionByName(currentProgram, newName);
if (existingFunction != null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function with name '" + newName + "' already exists")
.build();
}
// Perform the rename
try {
function.setName(newName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully renamed function '" + oldName + "' to '" + newName + "'")
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error renaming function: " + e.getMessage())
.build();
}
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,160 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that renames a local variable within a function.
*/
public class RenameVariableTool implements McpTool {
@Override
public String getName() {
return "rename_variable";
}
@Override
public String getDescription() {
return "Rename a local variable within a specific function";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"old_variable_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"new_variable_name", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("function_name", "old_variable_name", "new_variable_name"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String oldVariableName = (String) arguments.get("old_variable_name");
String newVariableName = (String) arguments.get("new_variable_name");
if (functionName == null || oldVariableName == null || newVariableName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_name, old_variable_name, and new_variable_name are all required")
.build();
}
// Find the function
Function function = findFunctionByName(currentProgram, functionName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + functionName)
.build();
}
// Get the high function and rename the variable
DecompInterface decompiler = new DecompInterface();
try {
decompiler.openProgram(currentProgram);
DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
if (results.isTimedOut()) {
return McpSchema.CallToolResult.builder()
.addTextContent("Decompilation timed out for function: " + functionName)
.build();
}
if (results.getErrorMessage() != null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Decompilation error: " + results.getErrorMessage())
.build();
}
HighFunction highFunction = results.getHighFunction();
if (highFunction == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Could not get high function for: " + functionName)
.build();
}
// Find and rename the variable
HighSymbol targetSymbol = null;
Iterator<HighSymbol> symbols = highFunction.getLocalSymbolMap().getSymbols();
while (symbols.hasNext()) {
HighSymbol symbol = symbols.next();
if (symbol.getName().equals(oldVariableName)) {
targetSymbol = symbol;
break;
}
}
if (targetSymbol == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Variable '" + oldVariableName + "' not found in function '" + functionName + "'")
.build();
}
// Rename the variable using the underlying symbol
try {
if (targetSymbol.getSymbol() != null) {
targetSymbol.getSymbol().setName(newVariableName, SourceType.USER_DEFINED);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully renamed variable '" + oldVariableName + "' to '" + newVariableName + "' in function '" + functionName + "'")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Cannot rename variable '" + oldVariableName + "' - no underlying symbol available")
.build();
} catch (DuplicateNameException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Variable with name '" + newVariableName + "' already exists in function '" + functionName + "'")
.build();
} catch (InvalidInputException e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid variable name: " + newVariableName)
.build();
}
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error renaming variable: " + e.getMessage())
.build();
} finally {
decompiler.dispose();
}
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,123 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that searches for functions by name pattern.
*/
public class SearchFunctionsTool implements McpTool {
@Override
public String getName() {
return "search_functions";
}
@Override
public String getDescription() {
return "Search for functions by name pattern (supports partial matching)";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"pattern", new McpSchema.JsonSchema("string", null, null, null, null, null),
"case_sensitive", new McpSchema.JsonSchema("boolean", null, null, null, null, null),
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of("pattern"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String pattern = (String) arguments.get("pattern");
if (pattern == null || pattern.trim().isEmpty()) {
return McpSchema.CallToolResult.builder()
.addTextContent("pattern parameter is required")
.build();
}
// Parse optional parameters
boolean caseSensitive = true;
if (arguments.get("case_sensitive") instanceof Boolean) {
caseSensitive = (Boolean) arguments.get("case_sensitive");
}
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
StringBuilder result = new StringBuilder();
result.append("Functions matching pattern: \"").append(pattern).append("\"");
result.append(" (case ").append(caseSensitive ? "sensitive" : "insensitive").append(")\n\n");
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
int count = 0;
int totalCount = 0;
String searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
while (functions.hasNext()) {
Function function = functions.next();
String functionName = caseSensitive ? function.getName() : function.getName().toLowerCase();
// Check if the function name contains the pattern
if (functionName.contains(searchPattern)) {
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
result.append("- ").append(function.getName())
.append(" @ ").append(function.getEntryPoint())
.append(" (").append(function.getParameterCount()).append(" params)")
.append("\n");
count++;
}
}
if (totalCount == 0) {
result.append("No functions found matching pattern: \"").append(pattern).append("\"");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" matching functions");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,90 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that sets a comment on a function (appears in decompiler).
*/
public class SetDecompilerCommentTool implements McpTool {
@Override
public String getName() {
return "set_decompiler_comment";
}
@Override
public String getDescription() {
return "Set a comment on a function (appears in decompiler view)";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"comment", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("function_name", "comment"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String comment = (String) arguments.get("comment");
if (functionName == null || comment == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_name and comment parameters are required")
.build();
}
// Find the function
Function function = findFunctionByName(currentProgram, functionName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + functionName)
.build();
}
try {
// Set the function comment (appears in decompiler)
function.setComment(comment);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully set decompiler comment on function '" + functionName +
"': \"" + comment + "\"")
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error setting function comment: " + e.getMessage())
.build();
}
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,137 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.CodeUnit;
import ghidra.program.model.listing.CommentType;
import ghidra.program.model.listing.Program;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that sets a comment on an instruction or data in the disassembly.
*/
public class SetDisassemblyCommentTool implements McpTool {
@Override
public String getName() {
return "set_disassembly_comment";
}
@Override
public String getDescription() {
return "Set a comment on an instruction or data in the disassembly";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"comment", new McpSchema.JsonSchema("string", null, null, null, null, null),
"comment_type", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("address", "comment"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
String comment = (String) arguments.get("comment");
String commentTypeStr = (String) arguments.get("comment_type");
if (addressStr == null || comment == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address and comment parameters are required")
.build();
}
// Parse the address
Address address;
try {
address = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Determine comment type (default to EOL comment)
CommentType commentType = CommentType.EOL;
if (commentTypeStr != null) {
switch (commentTypeStr.toLowerCase()) {
case "pre":
case "pre_comment":
commentType = CommentType.PRE;
break;
case "post":
case "post_comment":
commentType = CommentType.POST;
break;
case "plate":
case "plate_comment":
commentType = CommentType.PLATE;
break;
case "repeatable":
case "repeatable_comment":
commentType = CommentType.REPEATABLE;
break;
case "eol":
case "eol_comment":
default:
commentType = CommentType.EOL;
break;
}
}
// Get the code unit at the address
CodeUnit codeUnit = currentProgram.getListing().getCodeUnitAt(address);
if (codeUnit == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No code unit found at address: " + addressStr)
.build();
}
try {
// Set the comment
codeUnit.setComment(commentType, comment);
String commentTypeName = getCommentTypeName(commentType);
return McpSchema.CallToolResult.builder()
.addTextContent("Successfully set " + commentTypeName + " comment at " + addressStr +
": \"" + comment + "\"")
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error setting comment: " + e.getMessage())
.build();
}
}
private String getCommentTypeName(CommentType commentType) {
switch (commentType) {
case PRE:
return "pre";
case POST:
return "post";
case PLATE:
return "plate";
case REPEATABLE:
return "repeatable";
case EOL:
default:
return "EOL";
}
}
}
@@ -0,0 +1,242 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.SwingUtilities;
import ghidra.app.cmd.function.ApplyFunctionSignatureCmd;
import ghidra.app.util.parser.FunctionSignatureParser;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.data.FunctionDefinitionDataType;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.Msg;
import ghidra.util.task.ConsoleTaskMonitor;
import ghidrassistmcp.GhidrAssistMCPPlugin;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that sets a function's prototype/signature.
*/
public class SetFunctionPrototypeTool implements McpTool {
@Override
public String getName() {
return "set_function_prototype";
}
@Override
public String getDescription() {
return "Set a function's prototype/signature";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"prototype", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("function_address", "prototype"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
// Fallback for when plugin reference is not available
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Function prototype setting requires plugin context for proper transaction handling")
.build();
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram, GhidrAssistMCPPlugin plugin) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionAddrStr = (String) arguments.get("function_address");
String prototype = (String) arguments.get("prototype");
if (functionAddrStr == null || functionAddrStr.isEmpty()) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_address parameter is required")
.build();
}
if (prototype == null || prototype.isEmpty()) {
return McpSchema.CallToolResult.builder()
.addTextContent("prototype parameter is required")
.build();
}
// Use proper prototype setting with transaction handling
PrototypeResult result = setFunctionPrototype(currentProgram, functionAddrStr, prototype);
return McpSchema.CallToolResult.builder()
.addTextContent(result.success ?
"Successfully set function prototype: " + prototype :
"Failed to set function prototype: " + result.errorMessage)
.build();
}
/**
* Result class for prototype operations
*/
private static class PrototypeResult {
final boolean success;
final String errorMessage;
PrototypeResult(boolean success, String errorMessage) {
this.success = success;
this.errorMessage = errorMessage;
}
}
/**
* Set a function's prototype with proper error handling using ApplyFunctionSignatureCmd
*/
private PrototypeResult setFunctionPrototype(Program program, String functionAddrStr, String prototype) {
// Input validation
if (program == null) return new PrototypeResult(false, "No program loaded");
if (functionAddrStr == null || functionAddrStr.isEmpty()) {
return new PrototypeResult(false, "Function address is required");
}
if (prototype == null || prototype.isEmpty()) {
return new PrototypeResult(false, "Function prototype is required");
}
final StringBuilder errorMessage = new StringBuilder();
final AtomicBoolean success = new AtomicBoolean(false);
try {
SwingUtilities.invokeAndWait(() ->
applyFunctionPrototype(program, functionAddrStr, prototype, success, errorMessage));
} catch (Exception e) {
String msg = "Failed to set function prototype on Swing thread: " + e.getMessage();
errorMessage.append(msg);
Msg.error(this, msg, e);
}
return new PrototypeResult(success.get(), errorMessage.toString());
}
/**
* Helper method that applies the function prototype within a transaction
*/
private void applyFunctionPrototype(Program program, String functionAddrStr, String prototype,
AtomicBoolean success, StringBuilder errorMessage) {
try {
// Get the address and function
Address addr = program.getAddressFactory().getAddress(functionAddrStr);
if (addr == null) {
String msg = "Invalid address format: " + functionAddrStr;
errorMessage.append(msg);
Msg.error(this, msg);
return;
}
Function func = program.getFunctionManager().getFunctionAt(addr);
if (func == null) {
String msg = "Could not find function at address: " + functionAddrStr;
errorMessage.append(msg);
Msg.error(this, msg);
return;
}
Msg.info(this, "Setting prototype for function " + func.getName() + ": " + prototype);
// Store original prototype as a comment for reference
addPrototypeComment(program, func, prototype);
// Use proper function signature parsing and application
parseFunctionSignatureAndApply(program, addr, prototype, success, errorMessage);
} catch (Exception e) {
String msg = "Error setting function prototype: " + e.getMessage();
errorMessage.append(msg);
Msg.error(this, msg, e);
}
}
/**
* Parse and apply the function signature with error handling
*/
private void parseFunctionSignatureAndApply(Program program, Address addr, String prototype,
AtomicBoolean success, StringBuilder errorMessage) {
// Use ApplyFunctionSignatureCmd to parse and apply the signature
int txProto = program.startTransaction("Set function prototype");
try {
// Get data type manager
DataTypeManager dtm = program.getDataTypeManager();
// Create function signature parser
// Note: Since we don't have access to the tool here, we'll create parser without DataTypeManagerService
FunctionSignatureParser parser = new FunctionSignatureParser(dtm, null);
// Parse the prototype into a function signature
FunctionDefinitionDataType sig = parser.parse(null, prototype);
if (sig == null) {
String msg = "Failed to parse function prototype: " + prototype;
errorMessage.append(msg);
Msg.error(this, msg);
return;
}
// Create and apply the command
ApplyFunctionSignatureCmd cmd = new ApplyFunctionSignatureCmd(
addr, sig, SourceType.USER_DEFINED);
// Apply the command to the program
boolean cmdResult = cmd.applyTo(program, new ConsoleTaskMonitor());
if (cmdResult) {
success.set(true);
Msg.info(this, "Successfully applied function signature");
} else {
String msg = "Command failed: " + cmd.getStatusMsg();
errorMessage.append(msg);
Msg.error(this, msg);
}
} catch (Exception e) {
String msg = "Error applying function signature: " + e.getMessage();
errorMessage.append(msg);
Msg.error(this, msg, e);
} finally {
program.endTransaction(txProto, success.get());
}
}
/**
* Add prototype as a comment for reference
*/
private void addPrototypeComment(Program program, Function function, String prototype) {
try {
String currentComment = function.getComment();
String newComment = "Applied prototype: " + prototype;
if (currentComment != null && !currentComment.isEmpty()) {
newComment = currentComment + "\n" + newComment;
}
function.setComment(newComment);
} catch (Exception e) {
Msg.warn(this, "Could not add prototype comment: " + e.getMessage());
}
}
}
@@ -0,0 +1,178 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DataTypeManager;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.HighVariable;
import ghidra.util.task.TaskMonitor;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that sets the data type of a local variable within a function.
*/
public class SetLocalVariableTypeTool implements McpTool {
@Override
public String getName() {
return "set_local_variable_type";
}
@Override
public String getDescription() {
return "Set the data type of a local variable within a function";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"function_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"variable_name", new McpSchema.JsonSchema("string", null, null, null, null, null),
"data_type", new McpSchema.JsonSchema("string", null, null, null, null, null)
),
List.of("function_name", "variable_name", "data_type"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String functionName = (String) arguments.get("function_name");
String variableName = (String) arguments.get("variable_name");
String dataTypeName = (String) arguments.get("data_type");
if (functionName == null || variableName == null || dataTypeName == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("function_name, variable_name, and data_type are all required")
.build();
}
// Find the function
Function function = findFunctionByName(currentProgram, functionName);
if (function == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Function not found: " + functionName)
.build();
}
// Find the data type
DataTypeManager dtm = currentProgram.getDataTypeManager();
DataType dataType = dtm.getDataType("/" + dataTypeName);
if (dataType == null) {
// Try finding by name without path
dataType = dtm.getDataType(dataTypeName);
}
if (dataType == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Data type not found: " + dataTypeName +
". Use built-in types like 'int', 'char', 'void*', etc.")
.build();
}
// Get the high function and find the variable
DecompInterface decompiler = new DecompInterface();
try {
decompiler.openProgram(currentProgram);
DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
if (results.isTimedOut()) {
return McpSchema.CallToolResult.builder()
.addTextContent("Decompilation timed out for function: " + functionName)
.build();
}
if (results.getErrorMessage() != null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Decompilation error: " + results.getErrorMessage())
.build();
}
HighFunction highFunction = results.getHighFunction();
if (highFunction == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Could not get high function for: " + functionName)
.build();
}
// Find the variable
HighSymbol targetSymbol = null;
Iterator<HighSymbol> symbols = highFunction.getLocalSymbolMap().getSymbols();
while (symbols.hasNext()) {
HighSymbol symbol = symbols.next();
if (symbol.getName().equals(variableName)) {
targetSymbol = symbol;
break;
}
}
if (targetSymbol == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("Variable '" + variableName + "' not found in function '" + functionName + "'")
.build();
}
// Setting variable types in the decompiler is complex and may not persist
// For now, return information about the variable and desired type
try {
HighVariable highVar = targetSymbol.getHighVariable();
if (highVar != null) {
DataType currentType = highVar.getDataType();
String currentTypeName = currentType != null ? currentType.getName() : "unknown";
return McpSchema.CallToolResult.builder()
.addTextContent("Variable '" + variableName + "' in function '" + functionName +
"' currently has type: " + currentTypeName +
". Requested type: " + dataType.getName() +
" (Type setting not fully implemented - changes may not persist)")
.build();
}
return McpSchema.CallToolResult.builder()
.addTextContent("Cannot access type for variable '" + variableName +
"' - no high variable available")
.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error accessing variable type: " + e.getMessage())
.build();
}
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Error processing function: " + e.getMessage())
.build();
} finally {
decompiler.dispose();
}
}
private Function findFunctionByName(Program program, String functionName) {
var functionManager = program.getFunctionManager();
var functions = functionManager.getFunctions(true);
for (Function function : functions) {
if (function.getName().equals(functionName)) {
return function;
}
}
return null;
}
}
@@ -0,0 +1,111 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Reference;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that finds cross-references FROM a specific address.
*/
public class XrefsFromTool implements McpTool {
@Override
public String getName() {
return "xrefs_from";
}
@Override
public String getDescription() {
return "Get cross-references FROM a specific memory address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of("address"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
if (addressStr == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address parameter is required")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
// Parse the address
Address sourceAddress;
try {
sourceAddress = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Get cross-references FROM this address
StringBuilder result = new StringBuilder();
result.append("Cross-references FROM address ").append(addressStr).append(":\n\n");
Reference[] references = currentProgram.getReferenceManager().getReferencesFrom(sourceAddress);
int count = 0;
int totalCount = references.length;
for (int i = offset; i < references.length && count < limit; i++) {
Reference ref = references[i];
Address toAddr = ref.getToAddress();
String refType = ref.getReferenceType().toString();
result.append("To: ").append(toAddr)
.append(" (").append(refType).append(")\n");
count++;
}
if (totalCount == 0) {
result.append("No cross-references found from this address.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" references");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
@@ -0,0 +1,123 @@
/*
*
*/
package ghidrassistmcp.tools;
import java.util.List;
import java.util.Map;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidrassistmcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP tool that finds cross-references TO a specific address.
*/
public class XrefsToTool implements McpTool {
@Override
public String getName() {
return "xrefs_to";
}
@Override
public String getDescription() {
return "Get cross-references TO a specific memory address";
}
@Override
public McpSchema.JsonSchema getInputSchema() {
return new McpSchema.JsonSchema("object",
Map.of(
"address", new McpSchema.JsonSchema("string", null, null, null, null, null),
"offset", new McpSchema.JsonSchema("integer", null, null, null, null, null),
"limit", new McpSchema.JsonSchema("integer", null, null, null, null, null)
),
List.of("address"), null, null, null);
}
@Override
public McpSchema.CallToolResult execute(Map<String, Object> arguments, Program currentProgram) {
if (currentProgram == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("No program currently loaded")
.build();
}
String addressStr = (String) arguments.get("address");
if (addressStr == null) {
return McpSchema.CallToolResult.builder()
.addTextContent("address parameter is required")
.build();
}
// Parse optional offset and limit
int offset = 0;
int limit = 100; // Default limit
if (arguments.get("offset") instanceof Number) {
offset = ((Number) arguments.get("offset")).intValue();
}
if (arguments.get("limit") instanceof Number) {
limit = ((Number) arguments.get("limit")).intValue();
}
// Parse the address
Address targetAddress;
try {
targetAddress = currentProgram.getAddressFactory().getAddress(addressStr);
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent("Invalid address format: " + addressStr)
.build();
}
// Get cross-references TO this address
StringBuilder result = new StringBuilder();
result.append("Cross-references TO address ").append(addressStr).append(":\n\n");
ReferenceIterator refIter = currentProgram.getReferenceManager().getReferencesTo(targetAddress);
int count = 0;
int totalCount = 0;
while (refIter.hasNext()) {
Reference ref = refIter.next();
totalCount++;
// Apply offset
if (totalCount <= offset) {
continue;
}
// Apply limit
if (count >= limit) {
break;
}
Address fromAddr = ref.getFromAddress();
String refType = ref.getReferenceType().toString();
result.append("From: ").append(fromAddr)
.append(" (").append(refType).append(")\n");
count++;
}
if (totalCount == 0) {
result.append("No cross-references found to this address.");
} else {
result.append("\nShowing ").append(count).append(" of ").append(totalCount).append(" references");
if (offset > 0) {
result.append(" (offset: ").append(offset).append(")");
}
}
return McpSchema.CallToolResult.builder()
.addTextContent(result.toString())
.build();
}
}
+2
View File
@@ -0,0 +1,2 @@
The "src/resources/images" directory is intended to hold all image/icon files used by
this module.